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
77 changes: 77 additions & 0 deletions MainWindow.GooseIedScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.ComponentModel;
using System.Windows.Data;
using ArIED61850Tester.Models;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

public partial class MainWindow
{
// 0 = selected IED; 1 = all IEDs. Never filters the capture/decoder.
private int _gooseIedScopeIndex;
private ListCollectionView? _gooseEventScopeView;
private ListCollectionView? _gooseStreamScopeView;

public ICollectionView GooseVisibleEvents => _gooseEventScopeView ??= CreateGooseEventsView();
public ICollectionView GooseVisibleStreams => _gooseStreamScopeView ??= CreateGooseStreamsView();

public int GooseIedScopeIndex
{
get => _gooseIedScopeIndex;
set
{
var scope = value == 1 ? 1 : 0;
if (!Set(ref _gooseIedScopeIndex, scope)) return;
Raise(nameof(GooseSelectedScopeHint));
RefreshGooseScopeViews();
}
}

public string GooseSelectedScopeHint => GooseIedScopeIndex == 1
? "Display messages from every captured GOOSE publisher, including unresolved identities."
: SelectedDevice is null
? "No IED selected; showing all GOOSE publishers."
: $"Only confirmed {SelectedDevice.Name} GOOSE streams. Unresolved publishers remain in All IED mode.";

private ListCollectionView CreateGooseEventsView()
{
var view = new ListCollectionView(GooseEvents);
view.Filter = value => value is GooseEventRow row && IsGooseIedInScope(row.IedName);
return view;
}

private ListCollectionView CreateGooseStreamsView()
{
var view = new ListCollectionView(GooseStreams);
view.Filter = value => value is GooseStreamRow row && IsGooseIedInScope(row.ModelIedName);
return view;
}

private bool IsGooseIedInScope(string? publisherIedName)
=> GooseIedScopePolicy.Matches(GooseIedScopeIndex,
SelectedDevice?.Name, SelectedDevice?.SclIedName, publisherIedName);

private void RefreshGooseScopeViews()
{
// Refresh only the operator views. The 500-event ring and up to 256
// original publisher streams continue collecting all captured traffic.
_gooseEventScopeView?.Refresh();
_gooseStreamScopeView?.Refresh();
Raise(nameof(GooseSelectedScopeHint));
_settingGooseLiveSelection = true;
try
{
if (SelectedGooseEvent is not null &&
!IsGooseIedInScope(SelectedGooseEvent.IedName))
SelectedGooseEvent = null;
if (FollowLatestGooseEvents)
SelectedGooseEvent = GooseVisibleEvents.Cast<GooseEventRow>().LastOrDefault();
if (SelectedGooseEvent is null &&
SelectedGooseStream is not null &&
!IsGooseIedInScope(SelectedGooseStream.ModelIedName))
SelectedGooseStream = GooseVisibleStreams.Cast<GooseStreamRow>().FirstOrDefault();
}
finally { _settingGooseLiveSelection = false; }
RaiseGoosePresentationState();
}
}
13 changes: 13 additions & 0 deletions MainWindow.GooseSubscriber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ private void RebindGooseRowsFromLatestFrames()
row.Apply(BuildGooseStreamSnapshot(captured, _gooseBindingCatalog));
}

if (_goosePresentationInstalled)
RefreshGooseScopeViews();
Raise(nameof(GooseSelectedStreamText));
Raise(nameof(GooseNoLeafValuesVisibility));
Raise(nameof(GooseSelectedLeafCountText));
Expand Down Expand Up @@ -423,6 +425,7 @@ private void FlushGooseSubscriberUi()
return;

var processed = 0;
var scopeIdentityChanged = false;
foreach (var pair in _pendingGooseFrames.ToArray())
{
if (processed >= 64)
Expand All @@ -447,14 +450,24 @@ private void FlushGooseSubscriberUi()
SelectedGooseStream ??= row;
}

var previousIedName = row.ModelIedName;
row.Apply(snapshot);
if (!string.Equals(previousIedName, row.ModelIedName, StringComparison.OrdinalIgnoreCase))
scopeIdentityChanged = true;
// In follow-live mode the inspector shows current stream state
// between meaningful events; selecting history freezes the frame.
if (FollowLatestGooseEvents && _selectedGooseEvent?.StreamKey == snapshot.StreamKey)
SelectedGooseStream = row;
processed++;
}

if (scopeIdentityChanged)
RefreshGooseScopeViews();
// The first captured publisher may be unresolved or belong to a
// different IED. Never leave that row in the selected-IED inspector.
if (SelectedGooseEvent is null && SelectedGooseStream is not null &&
!IsGooseIedInScope(SelectedGooseStream.ModelIedName))
SelectedGooseStream = GooseVisibleStreams.Cast<GooseStreamRow>().FirstOrDefault();
Raise(nameof(GooseCounterText));
Raise(nameof(GooseNoStreamsVisibility));
Raise(nameof(GooseNoLeafValuesVisibility));
Expand Down
12 changes: 7 additions & 5 deletions MainWindow.GooseTimeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public bool FollowLatestGooseEvents
if (value && GooseEvents.Count > 0)
{
_settingGooseLiveSelection = true;
try { SelectedGooseEvent = GooseEvents[^1]; }
try { SelectedGooseEvent = GooseVisibleEvents.Cast<GooseEventRow>().LastOrDefault(); }
finally { _settingGooseLiveSelection = false; }
}
}
Expand Down Expand Up @@ -78,12 +78,12 @@ public GooseEventRow? SelectedGooseEvent
}
}

public string GoosePublisherCountText => $"{GooseStreams.Count:N0}";
public string GooseEventCountText => $"{GooseEvents.Count:N0}";
public string GoosePublisherCountText => $"{GooseVisibleStreams.Cast<GooseStreamRow>().Count():N0}";
public string GooseEventCountText => $"{GooseVisibleEvents.Cast<GooseEventRow>().Count():N0}";
public string GooseSelectedLeafCountText => SelectedGooseStream is null
? "0 signals"
: $"{SelectedGooseStream.EngineeringLeaves.Count:N0} signals · {SelectedGooseStream.Leaves.Count:N0} entries";
public Visibility GooseNoEventsVisibility => GooseEvents.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
public Visibility GooseNoEventsVisibility => GooseVisibleEvents.Cast<GooseEventRow>().Any() ? Visibility.Collapsed : Visibility.Visible;

[ModuleInitializer]
internal static void RegisterGoosePresentationIntegration()
Expand Down Expand Up @@ -151,6 +151,8 @@ private void GoosePresentation_PropertyChanged(object? sender, PropertyChangedEv
{
if (args.PropertyName == nameof(SelectedGooseStream))
Raise(nameof(GooseSelectedLeafCountText));
if (args.PropertyName == nameof(SelectedDevice))
RefreshGooseScopeViews();
}

private static void OnRefreshGooseAdaptersRequested(object sender, RoutedEventArgs args)
Expand Down Expand Up @@ -273,7 +275,7 @@ private void GooseTimelineUiFlushTimer_Tick(object? sender, EventArgs args)
while (GooseEvents.Count > MaxGooseTimelineEvents)
GooseEvents.RemoveAt(0);

if (FollowLatestGooseEvents)
if (FollowLatestGooseEvents && IsGooseIedInScope(eventRow.IedName))
{
_settingGooseLiveSelection = true;
try { SelectedGooseEvent = eventRow; }
Expand Down
30 changes: 20 additions & 10 deletions MainWindow.IecNaturalSort.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
using System;
using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using ArIED61850Tester.Models;
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.
/// Numeric-aware sorting is installed on each real grid, not guessed by
/// column counts. Never mutates original RCB, FCDA or control signal order.
/// </summary>
public partial class MainWindow
{
private readonly HashSet<DataGrid> _iecNaturallySortedGrids = new();

private void ExplorerLiveGrid_Loaded(object sender, RoutedEventArgs e)
=> AttachIecNaturalGridSort(sender as DataGrid, IecNaturalLiveMonitorSort.Instance);

private void CommandGrid_Loaded(object sender, RoutedEventArgs e)
=> AttachIecNaturalGridSort(sender as DataGrid, IecNaturalCommandSort.Instance);

private void GlobalLiveGrid_Loaded(object sender, RoutedEventArgs e)
=> AttachIecNaturalGridSort(sender as DataGrid, IecNaturalGlobalMonitorSort.Instance);

private void AttachIecNaturalGridSort(DataGrid? grid, IComparer comparer)
{
if (sender is not DataGrid grid) return;
if (grid is null) return;
if (_iecNaturallySortedGrids.Add(grid))
{
var descriptor = DependencyPropertyDescriptor.FromProperty(
ItemsControl.ItemsSourceProperty, typeof(DataGrid));
descriptor?.AddValueChanged(grid, (_, _) =>
grid.Dispatcher.BeginInvoke(new Action(() => ApplyExplorerNaturalSort(grid))));
grid.Dispatcher.BeginInvoke(new Action(() => ApplyIecNaturalSort(grid, comparer))));
}
ApplyExplorerNaturalSort(grid);
ApplyIecNaturalSort(grid, comparer);
}

private static void ApplyExplorerNaturalSort(DataGrid grid)
private static void ApplyIecNaturalSort(DataGrid grid, IComparer comparer)
{
if (grid.ItemsSource is null) return;
if (CollectionViewSource.GetDefaultView(grid.ItemsSource) is not ListCollectionView view)
return;
if (ReferenceEquals(view.CustomSort, IecNaturalLiveMonitorSort.Instance))
return;
if (ReferenceEquals(view.CustomSort, comparer)) return;
view.SortDescriptions.Clear();
view.CustomSort = IecNaturalLiveMonitorSort.Instance;
view.CustomSort = comparer;
}
}
4 changes: 2 additions & 2 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@
</Grid.RowDefinitions>

<Grid Grid.Row="0">
<DataGrid ItemsSource="{Binding SelectedDevice.CommandSignals}" IsReadOnly="False" CanUserAddRows="False" CanUserDeleteRows="False"
<DataGrid ItemsSource="{Binding SelectedDevice.CommandSignals}" Loaded="CommandGrid_Loaded" IsReadOnly="False" CanUserAddRows="False" CanUserDeleteRows="False"
Style="{StaticResource CommandDataGrid}" RowStyle="{StaticResource CommandDataGridRow}"
CellStyle="{StaticResource CommandDataGridCell}" ColumnHeaderStyle="{StaticResource CommandDataGridHeader}"
AutoGenerateColumns="False" MaxHeight="320" MinHeight="56" RowHeight="48" MinRowHeight="46" FrozenColumnCount="1"
Expand Down Expand Up @@ -1032,7 +1032,7 @@
</Button>
</Grid>
</Grid>
<DataGrid x:Name="GlobalLiveGrid" ItemsSource="{Binding GlobalPoints}" IsReadOnly="True" Style="{StaticResource ModernDataGrid}"
<DataGrid x:Name="GlobalLiveGrid" ItemsSource="{Binding GlobalPoints}" Loaded="GlobalLiveGrid_Loaded" IsReadOnly="True" Style="{StaticResource ModernDataGrid}"
FrozenColumnCount="3" EnableRowVirtualization="True" EnableColumnVirtualization="True"
VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.CanContentScroll="True" ScrollViewer.HorizontalScrollBarVisibility="Auto">
Expand Down
21 changes: 21 additions & 0 deletions Services/GooseIedScopePolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace ArIED61850Tester.Services;

/// <summary>
/// Identity-scoped presentation selection only: never infer an IED owner from
/// a MAC address, an IP, or an unverified GOOSE ID suffix.
/// </summary>
public static class GooseIedScopePolicy
{
public static bool Matches(int scopeIndex, string? selectedDeviceName,
string? selectedSclIedName, string? publisherIedName)
{
if (scopeIndex == 1 ||
(string.IsNullOrWhiteSpace(selectedDeviceName) &&
string.IsNullOrWhiteSpace(selectedSclIedName))) return true;
if (string.IsNullOrWhiteSpace(publisherIedName) ||
publisherIedName.Equals("Unresolved", StringComparison.OrdinalIgnoreCase))
return false;
return publisherIedName.Equals(selectedDeviceName?.Trim(), StringComparison.OrdinalIgnoreCase) ||
publisherIedName.Equals(selectedSclIedName?.Trim(), StringComparison.OrdinalIgnoreCase);
}
}
Loading
Loading