From ebd0eff9828bbbb223be19893a2083351b458a4d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:44:55 +0700 Subject: [PATCH 01/13] fix(P0 UI): optimize normal startup bounds and restore truthful active RCB icons (#730) --- MainWindow.xaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MainWindow.xaml b/MainWindow.xaml index 368bae1e1..4384a91f5 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:views="clr-namespace:ArIED61850Tester.Views" Title="ARSAS - Smart IEC 61850 Communication Tester" - Height="840" Width="1440" MinHeight="640" MinWidth="1100" + Height="960" Width="1740" MinHeight="640" MinWidth="1100" WindowStartupLocation="CenterScreen" WindowState="Normal" Background="{StaticResource AppBackgroundGradient}" FontFamily="{StaticResource AppFontFamily}" UseLayoutRounding="True" SnapsToDevicePixels="True" @@ -2022,8 +2022,8 @@ Width="16" Height="16" Stretch="Uniform" Fill="#277455" VerticalAlignment="Center" HorizontalAlignment="Center" - Visibility="{Binding RcbEnabledVisibility}" - ToolTip="{Binding RcbEnabledTooltip}"/> + Visibility="{Binding RcbActivityIconVisibility}" + ToolTip="{Binding RcbActivityIconTooltip}"/> @@ -2230,8 +2230,8 @@ Width="15" Height="15" Stretch="Uniform" Fill="#277455" VerticalAlignment="Center" HorizontalAlignment="Center" - Visibility="{Binding RcbEnabledVisibility}" - ToolTip="{Binding RcbEnabledTooltip}"/> + Visibility="{Binding RcbActivityIconVisibility}" + ToolTip="{Binding RcbActivityIconTooltip}"/> From eeb62a71f57718eb17e61e524b952884b5d3e599 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:44:59 +0700 Subject: [PATCH 02/13] fix(P0 UI): optimize normal startup bounds and restore truthful active RCB icons (#730) --- IoListTestingWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IoListTestingWindow.xaml b/IoListTestingWindow.xaml index f1d6ab1b1..53fb5360a 100644 --- a/IoListTestingWindow.xaml +++ b/IoListTestingWindow.xaml @@ -4,7 +4,7 @@ xmlns:io="clr-namespace:ArIED61850Tester.Models.IoTesting" xmlns:local="clr-namespace:ArIED61850Tester" Title="ARSAS - IO List Testing" - Height="820" Width="1440" MinHeight="640" MinWidth="1100" + Height="920" Width="1680" MinHeight="640" MinWidth="1100" WindowStartupLocation="CenterOwner" WindowState="Normal" Background="{StaticResource AppBackgroundGradient}" FontFamily="{StaticResource AppFontFamily}" From 667b10961a9bfe6c04a0e24b0ddf5e50ad711809 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:45:02 +0700 Subject: [PATCH 03/13] fix(P0 UI): optimize normal startup bounds and restore truthful active RCB icons (#730) --- WorkstationWindowStartupBounds.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/WorkstationWindowStartupBounds.cs b/WorkstationWindowStartupBounds.cs index 4ca349df6..a2049cc09 100644 --- a/WorkstationWindowStartupBounds.cs +++ b/WorkstationWindowStartupBounds.cs @@ -20,12 +20,19 @@ internal static void Apply(Window window) return; var work = SystemParameters.WorkArea; // WPF device-independent units. - const double inset = 24d; - var maxWidth = Math.Max(window.MinWidth, work.Width - 2d * inset); - var maxHeight = Math.Max(window.MinHeight, work.Height - 2d * inset); + // Use almost all available desktop space on first launch, without + // maximizing. The old 1440×840 cap truncated timestamps and the + // Command Dock on 1600–1920px engineering workstations. + const double inset = 8d; + var maxWidth = Math.Max(0d, work.Width - 2d * inset); + var maxHeight = Math.Max(0d, work.Height - 2d * inset); + if (maxWidth <= 0d || maxHeight <= 0d) + return; - window.Width = Math.Clamp(window.Width, window.MinWidth, maxWidth); - window.Height = Math.Clamp(window.Height, window.MinHeight, maxHeight); + // MinWidth/MinHeight are allowed only when the actual monitor can + // support them. Preserve native dragging and future user resizing. + window.Width = Math.Min(maxWidth, Math.Max(window.Width, Math.Min(window.MinWidth, maxWidth))); + window.Height = Math.Min(maxHeight, Math.Max(window.Height, Math.Min(window.MinHeight, maxHeight))); window.Left = work.Left + Math.Max(0d, (work.Width - window.Width) / 2d); window.Top = work.Top + Math.Max(0d, (work.Height - window.Height) / 2d); } From e2603df9cf6f7931c6ed73a555b9b9a81586badc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:45:07 +0700 Subject: [PATCH 04/13] fix(P0 UI): optimize normal startup bounds and restore truthful active RCB icons (#730) --- Models/IedBrowserPresentation.cs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Models/IedBrowserPresentation.cs b/Models/IedBrowserPresentation.cs index 3adfe67c1..b9e4bfb14 100644 --- a/Models/IedBrowserPresentation.cs +++ b/Models/IedBrowserPresentation.cs @@ -56,6 +56,26 @@ public IedBrowserTreeRow(string key, string title, string kind, int depth, objec ? Visibility.Visible : Visibility.Collapsed; public string RcbEnabledTooltip => _rcbEnabledTooltip; + // RptEna (a verified MMS Read) and current-session successful RCB + // activation are separate evidences. Show the requested activity icon + // for either, without falsely claiming that a local activation is a + // fresh remote RptEna read or that another client's RCB belongs to us. + public Visibility RcbActivityIconVisibility => + RcbEnabledVisibility == Visibility.Visible || + Payload is RcbExportRow + { + Availability: AR.Iec61850.Mms.MmsRcbOperationalAvailability.UsedByCaller + } + ? Visibility.Visible : Visibility.Collapsed; + public string RcbActivityIconTooltip => RcbEnabledVisibility == Visibility.Visible + ? RcbEnabledTooltip + : Payload is RcbExportRow + { + Availability: AR.Iec61850.Mms.MmsRcbOperationalAvailability.UsedByCaller + } + ? RcbUsageTooltip + : string.Empty; + public bool SetRcbEnabled(bool verified, string tooltip) { if (Payload is not RcbExportRow) return false; @@ -67,6 +87,8 @@ public bool SetRcbEnabled(bool verified, string tooltip) _rcbEnabledTooltip = tooltip; Raise(nameof(RcbEnabledVisibility)); Raise(nameof(RcbEnabledTooltip)); + Raise(nameof(RcbActivityIconVisibility)); + Raise(nameof(RcbActivityIconTooltip)); return true; } public string RcbUsageTooltip => Payload is RcbExportRow report @@ -88,6 +110,8 @@ public void RefreshRcbUsage() { if (Payload is not RcbExportRow) return; Raise(nameof(RcbUsageTooltip)); + Raise(nameof(RcbActivityIconVisibility)); + Raise(nameof(RcbActivityIconTooltip)); } private bool _isExpanded; @@ -288,6 +312,8 @@ public string ObservationTooltip public Visibility RcbEnabledVisibility => NavigateTo?.RcbEnabledVisibility ?? Visibility.Collapsed; public string RcbEnabledTooltip => NavigateTo?.RcbEnabledTooltip ?? ""; public string RcbUsageTooltip => NavigateTo?.RcbUsageTooltip ?? ""; + public Visibility RcbActivityIconVisibility => NavigateTo?.RcbActivityIconVisibility ?? Visibility.Collapsed; + public string RcbActivityIconTooltip => NavigateTo?.RcbActivityIconTooltip ?? ""; /// Mirror the same RCB node's active state in the right list. public void RefreshRcbUsage() @@ -299,6 +325,10 @@ public void RefreshRcbUsage() new PropertyChangedEventArgs(nameof(RcbEnabledTooltip))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RcbUsageTooltip))); + PropertyChanged?.Invoke(this, + new PropertyChangedEventArgs(nameof(RcbActivityIconVisibility))); + PropertyChanged?.Invoke(this, + new PropertyChangedEventArgs(nameof(RcbActivityIconTooltip))); if (Kind == "Object") Value = rcb.StatusText; } From 4ab779c915178a030f6ab8bbc476668ca049e0e7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:46:05 +0700 Subject: [PATCH 05/13] perf(GOOSE): offload native NIC discovery and keep SCL catalog rebuild off tab navigation (#730) --- MainWindow.GooseSubscriber.cs | 64 ++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/MainWindow.GooseSubscriber.cs b/MainWindow.GooseSubscriber.cs index f8eef05d4..f8a390ddf 100644 --- a/MainWindow.GooseSubscriber.cs +++ b/MainWindow.GooseSubscriber.cs @@ -37,6 +37,8 @@ public partial class MainWindow private long _gooseOtherFrames; private bool _gooseWorkspaceActivationScheduled; private bool _gooseBindingRefreshScheduled; + // Native adapter discovery must never execute during a tab navigation. + private bool _gooseNavigationAdapterScanInFlight; public BulkObservableCollection GooseAdapters { get; } = new(); public BulkObservableCollection GooseStreams { get; } = new(); @@ -133,23 +135,59 @@ private void ActivateGooseSubscriberWorkspace() return; _gooseWorkspaceActivationScheduled = true; - Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => + // GOOSE tab switching is presentation-only. Model compilation already + // follows workspace changes, and Start/Refresh Models can explicitly + // request a fresh catalog. Neither SCL scans nor synchronous Npcap + // enumeration belong on the navigation/rendering dispatcher path. + Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => { _gooseWorkspaceActivationScheduled = false; - try - { - RefreshGooseBindingPreview(); - if (GooseAdapters.Count == 0) - RefreshGooseAdapters(); - } - catch (Exception ex) + if (MainTabs.SelectedIndex != GooseSubscriberTabIndex || IsGooseCapturing) + return; + if (GooseAdapters.Count == 0) + _ = LoadGooseAdaptersForNavigationAsync(); + })); + } + + private async Task LoadGooseAdaptersForNavigationAsync() + { + if (_gooseNavigationAdapterScanInFlight || IsGooseCapturing || + Dispatcher.HasShutdownStarted || GooseAdapters.Count != 0) + return; + + _gooseNavigationAdapterScanInFlight = true; + try + { + // Native enumerations can stall several seconds on multi-NIC PCs. + // Only the discovery occurs on a worker; every bound collection + // mutation stays on WPF's synchronization context. + var adapters = await Task.Run( + () => _gooseSubscriberRuntime.ListAdapters()); + if (Dispatcher.HasShutdownStarted || IsGooseCapturing || + GooseAdapters.Count != 0) + return; + var previousName = SelectedGooseAdapter?.Name; + GooseAdapters.ReplaceAll(adapters); + SelectedGooseAdapter = adapters.FirstOrDefault(adapter => + previousName is not null && + adapter.Name.Equals(previousName, StringComparison.OrdinalIgnoreCase)); + GooseStatusText = adapters.Count == 0 + ? "No Npcap/WinPcap adapters found. Install Npcap, then refresh adapters." + : $"{adapters.Count:N0} capture adapter(s) available. Choose the station Ethernet adapter and press Start."; + } + catch (Exception ex) + { + // Failing optional capture discovery must not freeze or blank the tab. + if (!Dispatcher.HasShutdownStarted && !IsGooseCapturing) { - // The selected tab has already rendered by this point. Keep the workspace - // usable in unbound/wire-order mode instead of letting a model or native - // dependency exception abort TabControl content creation. - ApplyGooseWorkspaceFallback("GOOSE workspace initialization failed", ex); + GooseStatusText = $"Npcap adapter discovery failed: {DescribeGooseFailure(ex)}"; + AddLog("WARN", "GOOSE", GooseStatusText); } - })); + } + finally + { + _gooseNavigationAdapterScanInFlight = false; + } } private void RefreshGooseAdapters_Click(object sender, RoutedEventArgs e) From 8392d4dced9c9f11217314c9f98f19b6b1baa974 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:47:15 +0700 Subject: [PATCH 06/13] test(RC0): assert wide normal startup, active RCB proof, and nonblocking GOOSE navigation --- ...peratorGuiSizingRcbGooseRegressionTests.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs diff --git a/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs b/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs new file mode 100644 index 000000000..8953b313f --- /dev/null +++ b/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs @@ -0,0 +1,79 @@ +using System.Windows; +using System.Xml.Linq; +using AR.Iec61850.Mms; +using ArIED61850Tester.Models; + +namespace ARSAS.Tests; + +public sealed class Rc0OperatorGuiSizingRcbGooseRegressionTests +{ + [Fact] + public void WideNormalStartup_UsesDesktopWorkAreaWithoutForcingMaximize() + { + var main = XDocument.Parse(Read("MainWindow.xaml")).Root!; + Assert.Equal("Normal", (string?)main.Attribute("WindowState")); + Assert.Equal("CenterScreen", (string?)main.Attribute("WindowStartupLocation")); + Assert.True(double.Parse(main.Attribute("Width")!.Value) >= 1680d); + Assert.True(double.Parse(main.Attribute("Height")!.Value) >= 920d); + var bounds = Read("WorkstationWindowStartupBounds.cs"); + Assert.Contains("SystemParameters.WorkArea", bounds, StringComparison.Ordinal); + Assert.Contains("const double inset = 8d", bounds, StringComparison.Ordinal); + Assert.Contains("WindowStartupLocation.Manual", bounds, StringComparison.Ordinal); + Assert.DoesNotContain("WindowState.Maximized", bounds, StringComparison.Ordinal); + } + + [Fact] + public void RcbActivityIcon_OnlyClaimsVerifiedReadOrOwnLiveAssociation() + { + var row = new RcbExportRow { Reference = "LD/LLN0.RP.urcb01", Name = "Urcb01" }; + var node = new IedBrowserTreeRow("rcb/1", "Urcb01", "RCB", 0, row); + Assert.Equal(Visibility.Collapsed, node.RcbActivityIconVisibility); + row.Availability = MmsRcbOperationalAvailability.InUse; + node.RefreshRcbUsage(); + Assert.Equal(Visibility.Collapsed, node.RcbActivityIconVisibility); + row.Availability = MmsRcbOperationalAvailability.UsedByCaller; + node.RefreshRcbUsage(); + Assert.Equal(Visibility.Visible, node.RcbActivityIconVisibility); + Assert.Contains("current ARSAS", node.RcbActivityIconTooltip, StringComparison.Ordinal); + row.Availability = MmsRcbOperationalAvailability.Unknown; + node.RefreshRcbUsage(); + Assert.Equal(Visibility.Collapsed, node.RcbActivityIconVisibility); + Assert.True(node.SetRcbEnabled(true, "verified MMS Read")); + Assert.Equal(Visibility.Visible, node.RcbActivityIconVisibility); + Assert.Equal("verified MMS Read", node.RcbActivityIconTooltip); + Assert.True(node.SetRcbEnabled(false, "")); + Assert.Equal(Visibility.Collapsed, node.RcbActivityIconVisibility); + + var xaml = Read("MainWindow.xaml"); + Assert.Equal(2, xaml.Split("Visibility=\"{Binding RcbActivityIconVisibility}\"").Length - 1); + Assert.Equal(2, xaml.Split("ToolTip=\"{Binding RcbActivityIconTooltip}\"").Length - 1); + } + + [Fact] + public void GooseTabNavigation_DoesNotInvokeBlockingNpcapOrSclCompilation() + { + var source = Read("MainWindow.GooseSubscriber.cs"); + var start = source.IndexOf("private void ActivateGooseSubscriberWorkspace()", StringComparison.Ordinal); + var end = source.IndexOf("private async Task LoadGooseAdaptersForNavigationAsync()", StringComparison.Ordinal); + Assert.True(start >= 0 && end > start); + var activation = source[start..end]; + Assert.DoesNotContain("RefreshGooseBindingPreview();", activation, StringComparison.Ordinal); + Assert.DoesNotContain("RefreshGooseAdapters();", activation, StringComparison.Ordinal); + Assert.Contains("DispatcherPriority.Background", activation, StringComparison.Ordinal); + Assert.Contains("LoadGooseAdaptersForNavigationAsync()", activation, StringComparison.Ordinal); + Assert.Contains("await Task.Run(", source, StringComparison.Ordinal); + Assert.Contains("_gooseSubscriberRuntime.ListAdapters()", source, StringComparison.Ordinal); + // Explicit Start and Refresh Models continue to build the model before capture. + Assert.Contains("RefreshGooseBindingPreview();\n ResetGooseView", source, StringComparison.Ordinal); + } + + private static string Read(string path) + { + for (DirectoryInfo? dir = new(AppContext.BaseDirectory); dir is not null; dir = dir.Parent) + { + var full = Path.Combine(dir.FullName, path); + if (File.Exists(full)) return File.ReadAllText(full); + } + throw new FileNotFoundException(path); + } +} From c11f5c1d834717fe451163eefab217c9e7d4356b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:49:55 +0700 Subject: [PATCH 07/13] perf(GOOSE): deduplicate async Npcap enumeration between nav and card quick start --- MainWindow.GooseSubscriber.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/MainWindow.GooseSubscriber.cs b/MainWindow.GooseSubscriber.cs index f8a390ddf..8c128567d 100644 --- a/MainWindow.GooseSubscriber.cs +++ b/MainWindow.GooseSubscriber.cs @@ -38,7 +38,7 @@ public partial class MainWindow private bool _gooseWorkspaceActivationScheduled; private bool _gooseBindingRefreshScheduled; // Native adapter discovery must never execute during a tab navigation. - private bool _gooseNavigationAdapterScanInFlight; + private Task? _gooseNavigationAdapterScanTask; public BulkObservableCollection GooseAdapters { get; } = new(); public BulkObservableCollection GooseStreams { get; } = new(); @@ -145,17 +145,23 @@ private void ActivateGooseSubscriberWorkspace() if (MainTabs.SelectedIndex != GooseSubscriberTabIndex || IsGooseCapturing) return; if (GooseAdapters.Count == 0) - _ = LoadGooseAdaptersForNavigationAsync(); + _ = EnsureGooseAdaptersForNavigationAsync(); })); } - private async Task LoadGooseAdaptersForNavigationAsync() + // Deduplicated worker shared by ordinary tab navigation and IED-card entry. + private Task EnsureGooseAdaptersForNavigationAsync() { - if (_gooseNavigationAdapterScanInFlight || IsGooseCapturing || - Dispatcher.HasShutdownStarted || GooseAdapters.Count != 0) - return; + if (_gooseNavigationAdapterScanTask is { IsCompleted: false } running) + return running; + if (IsGooseCapturing || Dispatcher.HasShutdownStarted || GooseAdapters.Count != 0) + return Task.CompletedTask; + _gooseNavigationAdapterScanTask = LoadGooseAdaptersForNavigationAsync(); + return _gooseNavigationAdapterScanTask; + } - _gooseNavigationAdapterScanInFlight = true; + private async Task LoadGooseAdaptersForNavigationAsync() + { try { // Native enumerations can stall several seconds on multi-NIC PCs. @@ -186,7 +192,7 @@ previousName is not null && } finally { - _gooseNavigationAdapterScanInFlight = false; + _gooseNavigationAdapterScanTask = null; } } From 52ab298853c94c09168c558fb4623d3a4bc4a0bc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:50:34 +0700 Subject: [PATCH 08/13] perf(GOOSE): keep IED-card quick start NIC discovery and routing off dispatcher --- MainWindow.IedGooseQuickStart.cs | 44 ++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/MainWindow.IedGooseQuickStart.cs b/MainWindow.IedGooseQuickStart.cs index d539cf10b..264bccf9c 100644 --- a/MainWindow.IedGooseQuickStart.cs +++ b/MainWindow.IedGooseQuickStart.cs @@ -122,7 +122,7 @@ internal void OpenIedGooseSubscriber(Iec61850MonitorDevice device) MainTabs.SelectedIndex = GooseSubscriberTabIndex; UpdateNavigationVisuals(GooseSubscriberTabIndex, animate: true); ActivateGooseSubscriberWorkspace(); - Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => + Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(async () => { if (Dispatcher.HasShutdownStarted || MainTabs.SelectedIndex != GooseSubscriberTabIndex) return; @@ -133,13 +133,39 @@ internal void OpenIedGooseSubscriber(Iec61850MonitorDevice device) : "GOOSE capture is busy. Wait for the current operation."; return; } + // Share the same asynchronous Npcap discovery as ordinary tab + // navigation. A background worker also resolves OS NIC routing: + // neither scan can block the first GOOSE frame/render. if (GooseAdapters.Count == 0) - RefreshGooseAdapters(); + await EnsureGooseAdaptersForNavigationAsync(); + if (Dispatcher.HasShutdownStarted || MainTabs.SelectedIndex != GooseSubscriberTabIndex || + IsGooseCapturing || GooseActionBusy) + return; - // Exact OS/Npcap interface identity selects the candidate, not an - // adapter's display name or the presence of a single remaining NIC. - // The selected NIC is an observation point, NOT proof of GOOSE emission. - var proposed = ResolveGooseAdapterForIed(device, out var route); + // Capture a frozen list on the WPF thread before examining network + // interfaces off-thread. The candidate is observation only, not + // proof that an IED is publishing Ethernet GOOSE. + var candidates = GooseAdapters.ToArray(); + GooseAdapterOption? proposed; + string route; + try + { + var resolved = await Task.Run(() => + { + var adapter = ResolveGooseAdapterForIed(device, out var evidence, candidates); + return (Adapter: adapter, Evidence: evidence); + }); + proposed = resolved.Adapter; + route = resolved.Evidence; + } + catch (Exception ex) + { + proposed = null; + route = $"Network adapter routing could not be resolved: {ex.Message}. Select the capture adapter manually."; + } + if (Dispatcher.HasShutdownStarted || MainTabs.SelectedIndex != GooseSubscriberTabIndex || + IsGooseCapturing || GooseActionBusy) + return; _gooseCaptureContextHint = route.Contains("Same-PC", StringComparison.Ordinal) ? "Same-PC simulator: verify it actually publishes Ethernet GOOSE on this adapter." : "If no frames arrive, check the publisher, station NIC and VLAN."; @@ -163,7 +189,8 @@ internal void OpenIedGooseSubscriber(Iec61850MonitorDevice device) private GooseAdapterOption? ResolveGooseAdapterForIed( Iec61850MonitorDevice device, - out string routeDetail) + out string routeDetail, + IReadOnlyList? capturedAdapters = null) { routeDetail = "No route was resolved."; if (!IPAddress.TryParse(device.IpAddress, out var target) || @@ -187,7 +214,8 @@ internal void OpenIedGooseSubscriber(Iec61850MonitorDevice device) .Select(unicast => unicast.Address) .ToArray())) .ToArray(); - var selection = GooseAdapterSelectionPolicy.Resolve(target, route, interfaces, GooseAdapters.ToArray()); + var selection = GooseAdapterSelectionPolicy.Resolve( + target, route, interfaces, capturedAdapters ?? GooseAdapters.ToArray()); routeDetail = selection.Evidence; return selection.Adapter; } From 523626380763a61716b921dd26a04854436e804c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:51:07 +0700 Subject: [PATCH 09/13] test(RC0): protect nonblocking GOOSE IED-card quick-start and shared NIC scan --- ...Rc0OperatorGuiSizingRcbGooseRegressionTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs b/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs index 8953b313f..99957329a 100644 --- a/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs +++ b/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs @@ -67,6 +67,21 @@ public void GooseTabNavigation_DoesNotInvokeBlockingNpcapOrSclCompilation() Assert.Contains("RefreshGooseBindingPreview();\n ResetGooseView", source, StringComparison.Ordinal); } + [Fact] + public void IedCardGooseQuickStart_AlsoAvoidsNativeNicWorkOnDispatcher() + { + var source = Read("MainWindow.IedGooseQuickStart.cs"); + Assert.Contains("await EnsureGooseAdaptersForNavigationAsync()", source, StringComparison.Ordinal); + Assert.Contains("var candidates = GooseAdapters.ToArray();", source, StringComparison.Ordinal); + Assert.Contains("await Task.Run(() =>", source, StringComparison.Ordinal); + Assert.Contains("capturedAdapters ?? GooseAdapters.ToArray()", source, StringComparison.Ordinal); + // Quick start remains an explicit operator path, not an auto-start + // from incidental tab switching or an editable adapter binding. + Assert.Contains("MainTabs.SelectedIndex = GooseSubscriberTabIndex", source, StringComparison.Ordinal); + Assert.Contains("StartGooseSubscriber_Click(this, new RoutedEventArgs())", source, StringComparison.Ordinal); + Assert.Contains("_pendingIedGooseAutoStart = device;", source, StringComparison.Ordinal); + } + private static string Read(string path) { for (DirectoryInfo? dir = new(AppContext.BaseDirectory); dir is not null; dir = dir.Parent) From 41d35694299d38fc39c75f438d4f82bcfd8fd5ba Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:56:50 +0700 Subject: [PATCH 10/13] test(RC0): preserve MMS read truth and accept caller-owned RCB icon; make GOOSE assertions platform-neutral --- tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs b/tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs index b5deb6325..b5a097ecf 100644 --- a/tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs +++ b/tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs @@ -189,7 +189,15 @@ public void XamlUsesOriginalSingleVectorInInlineFixedRcbNameSlot() Assert.Contains("", rcb); Assert.Contains("", rcb); Assert.Contains("Grid.Column=\"2\" Data=\"{StaticResource BrowserRcbEnabledIconGeometry}\"", rcb); - Assert.Contains("Visibility=\"{Binding RcbEnabledVisibility}\"", rcb); + Assert.Contains("Visibility=\"{Binding RcbActivityIconVisibility}\"", rcb); + Assert.Contains("ToolTip=\"{Binding RcbActivityIconTooltip}\"", rcb); + // The same vector now distinguishes verified MMS RptEna and the + // current association's successful activation. It does NOT equate + // another client's InUse or an observed report with verified RptEna. + var model = File.ReadAllText(Path.Combine(root.FullName, "Models", "IedBrowserPresentation.cs")); + Assert.Contains("RcbEnabledVisibility == Visibility.Visible", model); + Assert.Contains("MmsRcbOperationalAvailability.UsedByCaller", model); + Assert.DoesNotContain("MmsRcbOperationalAvailability.InUse\n }", model); Assert.DoesNotContain("RcbUsageGlyph", xaml); Assert.Contains("M380-320h200", xaml); } From 4a07aad0ae8932dd120bdad0cf5b7d48bfee201c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:56:53 +0700 Subject: [PATCH 11/13] test(RC0): preserve MMS read truth and accept caller-owned RCB icon; make GOOSE assertions platform-neutral --- .../ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs b/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs index 99957329a..e8bdc5479 100644 --- a/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs +++ b/tests/ARSAS.Tests/Rc0OperatorGuiSizingRcbGooseRegressionTests.cs @@ -64,7 +64,8 @@ public void GooseTabNavigation_DoesNotInvokeBlockingNpcapOrSclCompilation() Assert.Contains("await Task.Run(", source, StringComparison.Ordinal); Assert.Contains("_gooseSubscriberRuntime.ListAdapters()", source, StringComparison.Ordinal); // Explicit Start and Refresh Models continue to build the model before capture. - Assert.Contains("RefreshGooseBindingPreview();\n ResetGooseView", source, StringComparison.Ordinal); + Assert.Contains("RefreshGooseBindingPreview();", source, StringComparison.Ordinal); + Assert.Contains("ResetGooseView(resetCounters: true);", source, StringComparison.Ordinal); } [Fact] From e1e15171a6efcc8b4b1733e96be7bd7311b4a86d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:57:10 +0700 Subject: [PATCH 12/13] test(RC0): keep RCB evidence assertion precise and nullable-safe --- tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs b/tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs index b5a097ecf..8b5f2d8d6 100644 --- a/tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs +++ b/tests/ARSAS.Tests/P0BrowserRcbEnabledTruthTests.cs @@ -194,10 +194,9 @@ public void XamlUsesOriginalSingleVectorInInlineFixedRcbNameSlot() // The same vector now distinguishes verified MMS RptEna and the // current association's successful activation. It does NOT equate // another client's InUse or an observed report with verified RptEna. - var model = File.ReadAllText(Path.Combine(root.FullName, "Models", "IedBrowserPresentation.cs")); + var model = File.ReadAllText(Path.Combine(root!.FullName, "Models", "IedBrowserPresentation.cs")); Assert.Contains("RcbEnabledVisibility == Visibility.Visible", model); Assert.Contains("MmsRcbOperationalAvailability.UsedByCaller", model); - Assert.DoesNotContain("MmsRcbOperationalAvailability.InUse\n }", model); Assert.DoesNotContain("RcbUsageGlyph", xaml); Assert.Contains("M380-320h200", xaml); } From 5b0060a32a4c5a4402bd7eafb89e5423564bd4e8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 21:58:51 +0700 Subject: [PATCH 13/13] fix(GOOSE): discard stale quick-start adapter results when active IED changes during async resolution --- MainWindow.IedGooseQuickStart.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MainWindow.IedGooseQuickStart.cs b/MainWindow.IedGooseQuickStart.cs index 264bccf9c..b9be26475 100644 --- a/MainWindow.IedGooseQuickStart.cs +++ b/MainWindow.IedGooseQuickStart.cs @@ -139,7 +139,7 @@ internal void OpenIedGooseSubscriber(Iec61850MonitorDevice device) if (GooseAdapters.Count == 0) await EnsureGooseAdaptersForNavigationAsync(); if (Dispatcher.HasShutdownStarted || MainTabs.SelectedIndex != GooseSubscriberTabIndex || - IsGooseCapturing || GooseActionBusy) + !ReferenceEquals(SelectedDevice, device) || IsGooseCapturing || GooseActionBusy) return; // Capture a frozen list on the WPF thread before examining network @@ -164,7 +164,7 @@ internal void OpenIedGooseSubscriber(Iec61850MonitorDevice device) route = $"Network adapter routing could not be resolved: {ex.Message}. Select the capture adapter manually."; } if (Dispatcher.HasShutdownStarted || MainTabs.SelectedIndex != GooseSubscriberTabIndex || - IsGooseCapturing || GooseActionBusy) + !ReferenceEquals(SelectedDevice, device) || IsGooseCapturing || GooseActionBusy) return; _gooseCaptureContextHint = route.Contains("Same-PC", StringComparison.Ordinal) ? "Same-PC simulator: verify it actually publishes Ethernet GOOSE on this adapter."