Skip to content
Closed
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: 4 additions & 1 deletion MainWindow.GooseSubscriber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,10 @@ private static GooseStreamSnapshot BuildGooseStreamSnapshot(
streamEvent.ChangedValueCount,
frame.Pdu.Test,
frame.Pdu.NeedsCommissioning,
leaves);
leaves,
frame.Vlan is { } tagged ? tagged.VlanId.ToString(CultureInfo.InvariantCulture) : "Untagged",
frame.Vlan is { } priority ? priority.PriorityCodePoint.ToString(CultureInfo.InvariantCulture) : "—",
rawValueCount);
}

private static string FirstNonEmpty(params string?[] values)
Expand Down
2 changes: 2 additions & 0 deletions MainWindow.GooseTimeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ private GooseEventRow BuildGooseEventRow(GooseSubscriberFrameSnapshot captured,
EventText = eventText,
EventTone = tone,
Publisher = BuildGoosePublisherName(stream),
IedName = string.IsNullOrWhiteSpace(stream.ModelIedName) ? "Unresolved" : stream.ModelIedName,
GooseId = string.IsNullOrWhiteSpace(stream.GoId) ? "Not provided" : stream.GoId,
StateSequenceText = $"{stream.StateNumberText} / {stream.SequenceNumberText}",
SourceMac = stream.SourceMac,
DestinationMac = stream.DestinationMac,
Expand Down
41 changes: 41 additions & 0 deletions MainWindow.IecNaturalSort.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

/// <summary>
/// Natural IEC sorting is attached to the actual native seven-column grid.
/// The earlier search-module heuristic expected six columns and never ran.
/// This changes only the WPF collection view, not the report/DataSet order.
/// </summary>
public partial class MainWindow
{
private readonly HashSet<DataGrid> _iecNaturallySortedGrids = new();

private void ExplorerLiveGrid_Loaded(object sender, RoutedEventArgs e)
{
if (sender is not DataGrid grid) return;
if (_iecNaturallySortedGrids.Add(grid))
{
var descriptor = DependencyPropertyDescriptor.FromProperty(
ItemsControl.ItemsSourceProperty, typeof(DataGrid));
descriptor?.AddValueChanged(grid, (_, _) =>
grid.Dispatcher.BeginInvoke(new Action(() => ApplyExplorerNaturalSort(grid))));
}
ApplyExplorerNaturalSort(grid);
}

private static void ApplyExplorerNaturalSort(DataGrid grid)
{
if (grid.ItemsSource is null) return;
if (CollectionViewSource.GetDefaultView(grid.ItemsSource) is not ListCollectionView view)
return;
if (ReferenceEquals(view.CustomSort, IecNaturalLiveMonitorSort.Instance))
return;
view.SortDescriptions.Clear();
view.CustomSort = IecNaturalLiveMonitorSort.Instance;
}
}
1 change: 1 addition & 0 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@
</Grid>

<DataGrid Grid.Row="2" ItemsSource="{Binding SelectedDevice.Points}" IsReadOnly="True"
Loaded="ExplorerLiveGrid_Loaded"
Style="{StaticResource ModernDataGrid}" FrozenColumnCount="2"
EnableRowVirtualization="True" EnableColumnVirtualization="True"
VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling"
Expand Down
18 changes: 18 additions & 0 deletions Models/GoosePresentationModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ public string DisplaySecondary

public string ModelIedDisplay => string.IsNullOrWhiteSpace(ModelIedName) ? "Not resolved" : ModelIedName;
public string GooseIdDisplay => string.IsNullOrWhiteSpace(GoId) ? "Not provided" : GoId;
public string ApplicationIdDecimal
{
get
{
var raw = AppIdText.Trim();
return raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase) &&
ushort.TryParse(raw.AsSpan(2), NumberStyles.HexNumber,
CultureInfo.InvariantCulture, out var value)
? value.ToString(CultureInfo.InvariantCulture) : raw;
}
}
public string SimulationTestText => Test ? "True" : "False";
public string DataSetEntryCountText => WireDataSetEntryCount.ToString(CultureInfo.InvariantCulture);
public string DataSetShortName => string.IsNullOrWhiteSpace(DataSetReference) ? "Not provided" : ShortReference(DataSetReference);
public string GoCbRefShortName => string.IsNullOrWhiteSpace(GoCbRef) ? "Not provided" : ShortReference(GoCbRef);
public string StateSequenceText => $"{StateNumberText} / {SequenceNumberText}";
Expand All @@ -36,6 +49,9 @@ private void RaisePresentationProperties()
Raise(nameof(DisplayName));
Raise(nameof(DisplaySecondary));
Raise(nameof(ModelIedDisplay));
Raise(nameof(ApplicationIdDecimal));
Raise(nameof(SimulationTestText));
Raise(nameof(DataSetEntryCountText));
Raise(nameof(GooseIdDisplay));
Raise(nameof(DataSetShortName));
Raise(nameof(GoCbRefShortName));
Expand Down Expand Up @@ -78,6 +94,8 @@ public sealed class GooseEventRow : ObservableObject
public string DestinationMac { get; init; } = "—";
public string DataSetName { get; init; } = "—";
public string RelativeTime { get; init; } = "—";
public string IedName { get; init; } = "Unresolved";
public string GooseId { get; init; } = "Not provided";
public GooseStreamSnapshot? Snapshot { get; init; }

public bool IsRecent { get => _isRecent; private set => Set(ref _isRecent, value); }
Expand Down
15 changes: 14 additions & 1 deletion Models/GooseSubscriberModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public sealed partial class GooseStreamRow : ObservableObject
private int _changedValueCount;
private bool _test;
private bool _needsCommissioning;
private string _vlanId = "Untagged";
private string _vlanPriority = "—";
private int _wireDataSetEntryCount;

public string StreamKey { get; init; } = string.Empty;
public ObservableCollection<GooseLeafValueRow> Leaves { get; } = new();
Expand All @@ -130,6 +133,9 @@ public sealed partial class GooseStreamRow : ObservableObject
public int ChangedValueCount { get => _changedValueCount; set => Set(ref _changedValueCount, Math.Max(0, value)); }
public bool Test { get => _test; set => Set(ref _test, value); }
public bool NeedsCommissioning { get => _needsCommissioning; set => Set(ref _needsCommissioning, value); }
public string VlanId { get => _vlanId; private set => Set(ref _vlanId, value); }
public string VlanPriority { get => _vlanPriority; private set => Set(ref _vlanPriority, value); }
public int WireDataSetEntryCount { get => _wireDataSetEntryCount; private set => Set(ref _wireDataSetEntryCount, value); }

public string IdentityText => !string.IsNullOrWhiteSpace(GoCbRef) ? GoCbRef : (!string.IsNullOrWhiteSpace(GoId) ? GoId : AppIdText);
public string FlagsText => Test && NeedsCommissioning ? "TEST • ndsCom" : Test ? "TEST" : NeedsCommissioning ? "ndsCom" : "Normal";
Expand All @@ -144,6 +150,10 @@ public void Apply(GooseStreamSnapshot snapshot)
SourceMac = snapshot.SourceMac;
DestinationMac = snapshot.DestinationMac;
VlanText = snapshot.VlanText;
VlanId = snapshot.VlanId;
VlanPriority = snapshot.VlanPriority;
WireDataSetEntryCount = snapshot.WireDataSetEntryCount == 0
? snapshot.Leaves.Count : snapshot.WireDataSetEntryCount;
StateNumberText = snapshot.StateNumberText;
SequenceNumberText = snapshot.SequenceNumberText;
SequenceStatus = snapshot.SequenceStatus;
Expand Down Expand Up @@ -252,4 +262,7 @@ public sealed record GooseStreamSnapshot(
int ChangedValueCount,
bool Test,
bool NeedsCommissioning,
IReadOnlyList<GooseLeafValueSnapshot> Leaves);
IReadOnlyList<GooseLeafValueSnapshot> Leaves,
string VlanId = "Untagged",
string VlanPriority = "—",
int WireDataSetEntryCount = 0);
57 changes: 55 additions & 2 deletions Services/GooseTypedValueInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ public static IReadOnlyList<GooseLeafValueSnapshot> Project(IReadOnlyList<GooseL
var pairedQualities = new HashSet<int>();
foreach (var value in wireLeaves)
{
if (!IsValue(value)) continue;
if (!IsValue(value) &&
!(IsModelBound(value) && value.SignalName.EndsWith(".stVal", StringComparison.OrdinalIgnoreCase)))
continue;
var valueRef = Owner(value.SignalReference, out _);
var exact = qualities.Where(q =>
string.Equals(valueRef, Owner(q.SignalReference, out _), StringComparison.OrdinalIgnoreCase)).ToArray();
Expand All @@ -136,14 +138,41 @@ public static IReadOnlyList<GooseLeafValueSnapshot> Project(IReadOnlyList<GooseL
q.FunctionalConstraint.Equals(value.FunctionalConstraint, StringComparison.OrdinalIgnoreCase) &&
string.Equals(ownerName, Owner(q.SignalName, out var qKind), StringComparison.OrdinalIgnoreCase) &&
valueKind == "value" && qKind == "quality" &&
CompatibleOwners(valueRef, Owner(q.SignalReference, out _))).ToArray();
(CompatibleOwners(valueRef, Owner(q.SignalReference, out _)) ||
CompatibleOwners(DoScopedReference(value.SignalReference),
DoScopedReference(q.SignalReference)))).ToArray();
if (adjacent.Length == 1)
{
var occurrences = wireLeaves.Count(row => IsValue(row) &&
string.Equals(ownerName, Owner(row.SignalName, out _), StringComparison.OrdinalIgnoreCase));
if (occurrences == 1) candidate = adjacent[0];
}
}
if (candidate is null && IsModelBound(value) &&
value.SignalName.EndsWith(".stVal", StringComparison.OrdinalIgnoreCase))
{
// Some GE F650 SCL models publish typed q in the next FCDA but
// use different naming conventions for MMS reference vs display.
// Only two *unique*, adjacent, model-bound wire members with
// identical IEC LN.DO and a decoded quality status may be joined.
var shortOwner = Owner(value.SignalName, out var valueKind);
var neighbors = wireLeaves.Where(q =>
q.DataSetIndex == value.DataSetIndex + 1 &&
IsModelBound(q) &&
q.SignalName.EndsWith(".q", StringComparison.OrdinalIgnoreCase) &&
Owner(q.SignalName, out var qKind) == shortOwner &&
qKind == "quality" && valueKind == "value" &&
IsReadableQuality(q.Value) &&
(string.IsNullOrWhiteSpace(q.FunctionalConstraint) ||
string.IsNullOrWhiteSpace(value.FunctionalConstraint) ||
q.FunctionalConstraint.Equals(value.FunctionalConstraint, StringComparison.OrdinalIgnoreCase)) &&
(CompatibleOwners(valueRef, Owner(q.SignalReference, out _)) ||
CompatibleOwners(DoScopedReference(value.SignalReference),
DoScopedReference(q.SignalReference)))).ToArray();
if (neighbors.Length == 1 &&
wireLeaves.Count(row => row.SignalName.Equals(value.SignalName, StringComparison.OrdinalIgnoreCase)) == 1)
candidate = neighbors[0];
}
if (candidate is null || !pairedQualities.Add(candidate.DataSetIndex))
continue;
pairs[value.DataSetIndex] = candidate;
Expand All @@ -166,6 +195,30 @@ public static IReadOnlyList<GooseLeafValueSnapshot> Project(IReadOnlyList<GooseL
return result;
}

private static string? DoScopedReference(string? input)
{
if (string.IsNullOrWhiteSpace(input)) return null;
var key = input.Trim().Replace('$', '.');
var slash = key.LastIndexOf('/');
var dot = key.IndexOf('.', slash + 1);
if (dot >= 0 && dot + 4 < key.Length &&
key.AsSpan(dot + 1, 3).Equals("ST.".AsSpan(), StringComparison.OrdinalIgnoreCase))
key = key.Remove(dot + 1, 3);
foreach (var suffix in new[] { ".stVal", ".q" })
if (key.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
key = key[..^suffix.Length];
return key;
}

private static bool IsReadableQuality(string value) =>
value.Equals("Good", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("Good ·", StringComparison.OrdinalIgnoreCase) ||
value.Equals("Invalid", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("Invalid ·", StringComparison.OrdinalIgnoreCase) ||
value.Equals("Questionable", StringComparison.OrdinalIgnoreCase) ||
value.StartsWith("Questionable ·", StringComparison.OrdinalIgnoreCase) ||
value.Equals("Reserved", StringComparison.OrdinalIgnoreCase);

private static bool IsModelBound(GooseLeafValueSnapshot leaf)
=> !leaf.BindingSource.Equals("Unbound", StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrWhiteSpace(leaf.SignalReference);
Expand Down
Loading
Loading