Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/ProcessBus.App.Wpf/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -2002,7 +2002,7 @@
<Style TargetType="Border">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsRunning}" Value="True">
<DataTrigger Binding="{Binding IsSelectedSvReceiving}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
Expand All @@ -2014,7 +2014,7 @@
<Style TargetType="Ellipse">
<Setter Property="Opacity" Value="0.55"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsRunning}" Value="True">
<DataTrigger Binding="{Binding IsSelectedSvReceiving}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="LiveDotPulseStoryboard">
<Storyboard AutoReverse="True" RepeatBehavior="Forever">
Expand Down Expand Up @@ -2205,6 +2205,7 @@
Command="{Binding CopyEvidenceCommand}"
ToolTip="Copy original engineering values and passive capture evidence"/>
<ComboBox Height="28" MinWidth="140"
IsEnabled="{Binding CanSelectEngineeringScale}"
Style="{StaticResource CompactComboBoxStyle}"
ItemsSource="{Binding EngineeringScaleOptions}"
SelectedItem="{Binding SelectedEngineeringScale, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Expand Down
22 changes: 18 additions & 4 deletions src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
private string? _lastPhasorSourceStreamId;
private readonly Dictionary<string, SvEngineeringScale> _explicitSvScales = new(StringComparer.OrdinalIgnoreCase);
private const string RawScaleText = "Raw counts (uncalibrated)";
private const string NominalScaleText = "4I+4V nominal (1 mA / 10 mV per count)";
private const string NominalScaleText = "4I+4V nominal";
private string _waveformShowMode = "Both";
private string _waveformLayoutMode = "Overlay";
private string _waveformTimebase = "2 cycles";
Expand Down Expand Up @@ -110,6 +110,7 @@
_isRunning = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CanSwitchCaptureMode));
OnPropertyChanged(nameof(IsSelectedSvReceiving));
}
}

Expand Down Expand Up @@ -764,6 +765,8 @@
ApplySelectedStreamScale(value?.StreamId);
OnPropertyChanged(nameof(SelectedEngineeringScale));
OnPropertyChanged(nameof(EngineeringScaleExplanation));
OnPropertyChanged(nameof(CanSelectEngineeringScale));
OnPropertyChanged(nameof(IsSelectedSvReceiving));
RestoreSelectedStreamWorkspace(value);
OnPropertyChanged();
OnPropertyChanged(nameof(AnalogValues));
Expand Down Expand Up @@ -880,6 +883,11 @@
public IReadOnlyList<string> WaveformMarkerOptions { get; } = ["On", "Off"];
public IReadOnlyList<string> EngineeringScaleOptions { get; } = [NominalScaleText, RawScaleText];

// Before SV discovery this is a display preset only. No incoming samples,
// physical units or fabricated measurements are implied.
public bool CanSelectEngineeringScale => SelectedStream is not null;
public bool IsSelectedSvReceiving => IsRunning && SelectedStream is { IsActive: true };

private SvEngineeringScale ResolveSelectedStreamScale(string streamId)
{
if (_explicitSvScales.TryGetValue(streamId, out var explicitMode))
Expand All @@ -899,8 +907,9 @@

public string SelectedEngineeringScale
{
get => SelectedStream is not null &&
ResolveSelectedStreamScale(SelectedStream.StreamId) == SvEngineeringScale.Nominal4I4V
get => (SelectedStream is null
? SvEngineeringScaleFactors.IdlePresentationDefault
: ResolveSelectedStreamScale(SelectedStream.StreamId)) == SvEngineeringScale.Nominal4I4V
? NominalScaleText : RawScaleText;
set
{
Expand All @@ -925,7 +934,7 @@
}

public string EngineeringScaleExplanation => SelectedStream is null
? "Select an SV stream to see its measurement units."
? "Waiting for publisher. Nominal 4I+4V is a display preset, not a measurement; no values until recognized SV data arrives."
: SelectedEngineeringScale == NominalScaleText
? "Assumed 4I+4V nominal: 1 mA / 10 mV per count. Display estimate only; not calibrated or SCL-verified. Choose Raw counts anytime."
: "Raw SV counts: no electrical units inferred. Use nominal 4I+4V only when its layout is confirmed.";
Expand Down Expand Up @@ -2182,6 +2191,8 @@
_selectedStream = refreshed;
RestoreSelectedStreamWorkspace(refreshed);
OnPropertyChanged(nameof(SelectedStream));
OnPropertyChanged(nameof(IsSelectedSvReceiving));
OnPropertyChanged(nameof(CanSelectEngineeringScale));
OnPropertyChanged(nameof(AnalogValues));
OnPropertyChanged(nameof(Phasors));
OnPropertyChanged(nameof(Waveform));
Expand Down Expand Up @@ -2218,6 +2229,8 @@
OnPropertyChanged(nameof(SelectedStream));
OnPropertyChanged(nameof(SelectedEngineeringScale));
OnPropertyChanged(nameof(EngineeringScaleExplanation));
OnPropertyChanged(nameof(CanSelectEngineeringScale));
OnPropertyChanged(nameof(IsSelectedSvReceiving));
}

private void RestoreSelectedStreamWorkspace(SvStreamItem? stream)
Expand Down Expand Up @@ -2546,6 +2559,7 @@
{
_lastGlobalRaiseUtc = now;
OnPropertyChanged(nameof(StreamStatusText));
OnPropertyChanged(nameof(IsSelectedSvReceiving));
OnPropertyChanged(nameof(OperatorCaptureSummaryText));
OnPropertyChanged(nameof(DataSourceName));
OnPropertyChanged(nameof(IsLiveMode));
Expand Down Expand Up @@ -4586,7 +4600,7 @@
public double? DeltaMs { get; init; }
public string SemanticChangedSummaryText { get; init; } = string.Empty;

public string TimeText => Source.LastSeenUtc.ToString("HH:mm:ss.fff");

Check warning on line 4603 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'DateTime.ToString(string)' could vary based on the current user's locale settings. Replace this call in 'GooseTrafficRow.TimeText.get' with a call to 'DateTime.ToString(string, IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
public string DeltaText => DeltaMs.HasValue ? $"{DeltaMs.Value:0.0}" : "-";

public string GoId => Source.GoId;
Expand Down Expand Up @@ -4631,11 +4645,11 @@
}
}
public string AppId => Source.AppId;
public string EntryTimeText => Source.LastSeenUtc.ToLocalTime().ToString("M/d/yyyy h:mm:ss.fff tt");

Check warning on line 4648 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'DateTime.ToString(string)' could vary based on the current user's locale settings. Replace this call in 'GooseTrafficRow.EntryTimeText.get' with a call to 'DateTime.ToString(string, IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
public string NeedsCommissionText => Source.NeedsCommission ? "true" : "false";
public string TestText => Source.IsTest ? "true" : "false";
public int DatasetEntryCount => Source.DataValues.Count;
public string DatasetEntryCountText => DatasetEntryCount.ToString();

Check warning on line 4652 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'int.ToString()' could vary based on the current user's locale settings. Replace this call in 'GooseTrafficRow.DatasetEntryCountText.get' with a call to 'int.ToString(IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
public string DestinationMac => Source.DestinationMac;
public string SourceMac => Source.SourceMac;
public string VlanId => Source.VlanId;
Expand Down Expand Up @@ -4771,7 +4785,7 @@
var age = DateTime.UtcNow - LastSeenUtc.Value;
return age.TotalSeconds < 60
? $"last {age.TotalSeconds:0.0}s"
: LastSeenUtc.Value.ToLocalTime().ToString("HH:mm:ss");

Check warning on line 4788 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'DateTime.ToString(string)' could vary based on the current user's locale settings. Replace this call in 'TrafficHealthTargetRow.LastSeenText.get' with a call to 'DateTime.ToString(string, IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
}
}

Expand Down Expand Up @@ -4852,7 +4866,7 @@
public string EntryCountText => $"{Entries.Count} entry(s)";
public string SummaryText => $"{IedName} - {TransportText}";
public string ExpectedKey => $"{Protocol}|{SourceFileName}|{IedName}|{ControlBlockReference}|{DisplayName}";
public string ExpectedConfRevText => ConfRev > 0 ? ConfRev.ToString() : "N/A";

Check warning on line 4869 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

The behavior of 'int.ToString()' could vary based on the current user's locale settings. Replace this call in 'SclStreamCatalogRow.ExpectedConfRevText.get' with a call to 'int.ToString(IFormatProvider)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)

public static SclStreamCatalogRow FromSv(string sourceFileName, SclSvStreamModel stream, string liveStatus)
=> new()
Expand Down Expand Up @@ -5081,7 +5095,7 @@
return ("No evidence captured yet", string.Empty);

var parts = evidence
.Split(new[] { "; " }, StringSplitOptions.RemoveEmptyEntries)

Check warning on line 5098 in src/ProcessBus.App.Wpf/ViewModels/MainWindowViewModel.cs

View workflow job for this annotation

GitHub Actions / Build verified Windows x64 candidate

Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1861)
.Select(part => part.Trim())
.Where(part => part.Length > 0)
.ToArray();
Expand Down
5 changes: 5 additions & 0 deletions src/ProcessBus.Core/Models/SvEngineeringScale.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ public enum SvEngineeringScale

public static class SvEngineeringScaleFactors
{
// UI preset shown before any publisher is detected. It is not applied to
// an unknown stream until its decoded profile is verified or the operator
// explicitly selects nominal. No electrical values exist during idle.
public const SvEngineeringScale IdlePresentationDefault = SvEngineeringScale.Nominal4I4V;

// This explicitly opted-in 4I+4V lab/nominal profile is 1 mA per
// current count and 10 mV per voltage count. It is NOT universal
// IEC 61850 SV scaling and must never be auto-selected by svID.
Expand Down
10 changes: 10 additions & 0 deletions tests/ProcessBus.Tests/SvNominalProfilePolicyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ namespace ProcessBus.Tests;

public sealed class SvNominalProfilePolicyTests
{
[Fact]
public void IdleUiPresetIsNominalButCannotInventMeasurementEvidence()
{
Assert.Equal(SvEngineeringScale.Nominal4I4V, SvEngineeringScaleFactors.IdlePresentationDefault);
var noSamples = new AnalogValuesSnapshot();
Assert.Null(noSamples.Ua.RmsValue);
Assert.Null(noSamples.Ia.RmsValue);
Assert.Null(noSamples.Ic.AngleDegrees);
}

[Theory]
[InlineData("Raw 4I4V instMag/q candidate profile", true)]
[InlineData("OMICRON raw 4I4V instMag/q profile / OMICRON_CMC_SV1", true)]
Expand Down
Loading