From c9b777fe095c88326d4118ad90fad645f764421f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:52:39 +0700 Subject: [PATCH 01/10] fix(browser): distinguish FC CO command parameters from unread values --- Models/IedBrowserReadPlan.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Models/IedBrowserReadPlan.cs b/Models/IedBrowserReadPlan.cs index 0624c2190..f783c2b34 100644 --- a/Models/IedBrowserReadPlan.cs +++ b/Models/IedBrowserReadPlan.cs @@ -31,6 +31,16 @@ public static IedBrowserReadSelectionKind SelectionKind(IedBrowserTreeRow? page) private static readonly HashSet ReadableFc = new( ["ST", "MX", "CF", "DC", "SP", "SG", "SE", "EX"], StringComparer.OrdinalIgnoreCase); + /// + /// FC CO denotes command parameters, not scalar MMS Read targets. + /// Keeping that separate from a genuinely missing read is essential + /// during FAT; no control operation is implied by Browser navigation. + /// + public static string UnobservedValue(string? functionalConstraint) + => string.Equals(functionalConstraint, "CO", StringComparison.OrdinalIgnoreCase) + ? "Control only" + : "Not read"; + public static IReadOnlyList Build( LiveIedModelDiscoveryDocument? model, IedBrowserTreeRow? page, bool readAll, string? selectedDoReference = null) From 5b8152c9fe0c432cd370d9c6fc2906441ba0372a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:52:43 +0700 Subject: [PATCH 02/10] fix(browser): distinguish FC CO command parameters from unread values --- Models/IedBrowserStructuredDetails.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Models/IedBrowserStructuredDetails.cs b/Models/IedBrowserStructuredDetails.cs index d20ceffaa..8e66e57ad 100644 --- a/Models/IedBrowserStructuredDetails.cs +++ b/Models/IedBrowserStructuredDetails.cs @@ -336,7 +336,8 @@ private static IReadOnlyList BuildAt( : TimeChildren(depth + 1, attr.MmsReference, attr.FunctionalConstraint); result.Add(new IedBrowserDetailRow("Data", - $"{name} [{attr.FunctionalConstraint}]", "Not read", depth, + $"{name} [{attr.FunctionalConstraint}]", + IedBrowserReadPlan.UnobservedValue(attr.FunctionalConstraint), depth, $"IEC declaration · {attr.MmsReference} · {attr.TypeSource}") { // A structured intermediate row may have its own observed From 4381fda4c89c3c93a41d33ad2ddd6849a492b24f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:52:46 +0700 Subject: [PATCH 03/10] fix(browser): distinguish FC CO command parameters from unread values --- MainWindow.IedBrowser.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/MainWindow.IedBrowser.cs b/MainWindow.IedBrowser.cs index e94cffacf..952e72d37 100644 --- a/MainWindow.IedBrowser.cs +++ b/MainWindow.IedBrowser.cs @@ -532,9 +532,12 @@ private void RefreshIedBrowserVisibleValues() } else { - row.Value = "Not read"; + row.Value = IedBrowserReadPlan.UnobservedValue( + row.FunctionalConstraint); row.ObservationSource = ""; - row.ObservationTooltip = ""; + row.ObservationTooltip = row.FunctionalConstraint == "CO" + ? "FC CO: command parameters are not scalar MMS Read targets. Use the explicit controlled Signal Testing workflow." + : ""; } continue; } From 0255b838728dfe7a17f34644afeac750ec0a8679 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:52:49 +0700 Subject: [PATCH 04/10] test(browser): command FC remains control-only while ST read state is unchanged --- ...214IedBrowserControlOnlyReadStatusTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/ARSAS.Tests/M214IedBrowserControlOnlyReadStatusTests.cs diff --git a/tests/ARSAS.Tests/M214IedBrowserControlOnlyReadStatusTests.cs b/tests/ARSAS.Tests/M214IedBrowserControlOnlyReadStatusTests.cs new file mode 100644 index 000000000..4c8ed3fd3 --- /dev/null +++ b/tests/ARSAS.Tests/M214IedBrowserControlOnlyReadStatusTests.cs @@ -0,0 +1,48 @@ +using AR.Iec61850.Discovery; +using ArIED61850Tester.Models; + +namespace ARSAS.Tests; + +public sealed class M214IedBrowserControlOnlyReadStatusTests +{ + [Fact] + public void CommandParameters_AreNotDisplayedAsMissingMmsReads() + { + const string objectRef = "LD/CSWI1.Pos"; + var attributes = new[] + { + new LiveIedDataAttributeModel + { + ObjectReference = objectRef, + AttributePath = "Pos.Oper.ctlVal", + MmsReference = "LD/CSWI1$CO$Pos$Oper$ctlVal", + FunctionalConstraint = "CO" + }, + new LiveIedDataAttributeModel + { + ObjectReference = objectRef, + AttributePath = "Pos.stVal", + MmsReference = "LD/CSWI1$ST$Pos$stVal", + FunctionalConstraint = "ST" + } + }; + var roots = IedBrowserStructuredDetails.AttributeRows(attributes, "Pos"); + var control = Assert.Single(roots, row => + row.Name == "Oper [CO]"); + Assert.Equal("Control only", control.Value); + Assert.True(control.HasChildren); + Assert.Equal("Control only", Assert.Single(control.LoadChildren!()).Value); + var status = Assert.Single(roots, row => row.Name == "stVal [ST]"); + Assert.Equal("Not read", status.Value); + } + + [Theory] + [InlineData("CO", "Control only")] + [InlineData("co", "Control only")] + [InlineData("ST", "Not read")] + [InlineData("SG", "Not read")] + [InlineData("CF", "Not read")] + [InlineData("", "Not read")] + public void UnobservedLabel_HasExactFcSemantics(string fc, string expected) + => Assert.Equal(expected, IedBrowserReadPlan.UnobservedValue(fc)); +} From 451fc5e567098378e7562c47be1512927b1b52f2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:55:30 +0700 Subject: [PATCH 05/10] test(gui): assert operator-approved route positions without weakening workspace safety --- .../ARSAS.Tests/P109IedBrowserAndEventDrawerContractTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ARSAS.Tests/P109IedBrowserAndEventDrawerContractTests.cs b/tests/ARSAS.Tests/P109IedBrowserAndEventDrawerContractTests.cs index 9d2234b59..5ec98732d 100644 --- a/tests/ARSAS.Tests/P109IedBrowserAndEventDrawerContractTests.cs +++ b/tests/ARSAS.Tests/P109IedBrowserAndEventDrawerContractTests.cs @@ -6,12 +6,12 @@ namespace ARSAS.Tests; public sealed class P109IedBrowserAndEventDrawerContractTests { [Fact] - public void BrowserIsAnEighthPermanentNativeTab_WithoutRenumberingExistingWorkflows() + public void BrowserIsFirstOperatorDestination_WithoutRenumberingItsNativeWorkspace() { var xaml = Read("MainWindow.xaml"); var code = Read("MainWindow.IedBrowser.cs"); Assert.Contains("x:Name=\"IedBrowserTab\" Header=\"IED Browser\"", xaml); - Assert.Contains("x:Name=\"NavIedBrowserButton\" Grid.Column=\"7\"", xaml); + Assert.Contains("x:Name=\"NavIedBrowserButton\" Grid.Column=\"0\"", xaml); Assert.Contains("x:Name=\"NativeFatTab\" Header=\"FAT\"", xaml); Assert.Contains("x:Name=\"IedBrowserTreeGrid\"", xaml); Assert.Contains("x:Name=\"IedBrowserDetailList\"", xaml); From 9899d87b6036e8b2b3f1bcb88677f5307ce1a37e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:55:33 +0700 Subject: [PATCH 06/10] test(gui): assert operator-approved route positions without weakening workspace safety --- tests/ARSAS.Tests/OperatorHeaderIconRegressionTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ARSAS.Tests/OperatorHeaderIconRegressionTests.cs b/tests/ARSAS.Tests/OperatorHeaderIconRegressionTests.cs index cc2f8f11e..c5636194c 100644 --- a/tests/ARSAS.Tests/OperatorHeaderIconRegressionTests.cs +++ b/tests/ARSAS.Tests/OperatorHeaderIconRegressionTests.cs @@ -37,10 +37,10 @@ public void SixOperatorSvgGeometries_AreDeclaredOnce_WithUsableContours() } [Theory] - [InlineData("NavIedBrowserButton", "7", "IED Browser", "OperatorIedBrowserIcon")] - [InlineData("NavNativeFatButton", "6", "FAT", "OperatorFatIcon")] - [InlineData("NavAlarmButton", "3", "Alarm", "OperatorAlarmIcon")] - [InlineData("NavEventsButton", "2", "Event Log", "OperatorEventLogIcon")] + [InlineData("NavIedBrowserButton", "0", "IED Browser", "OperatorIedBrowserIcon")] + [InlineData("NavNativeFatButton", "6", "Signal Testing", "OperatorFatIcon")] + [InlineData("NavAlarmButton", "3", "Annunciator", "OperatorAlarmIcon")] + [InlineData("NavEventsButton", "2", "Event Logs", "OperatorEventLogIcon")] public void HeaderTabs_PreserveClickIndices_AndTrackButtonForeground( string buttonName, string expectedIndex, string caption, string icon) { From d5ec5e453814f916224a3bee5d69d4eab9c648ca Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:55:36 +0700 Subject: [PATCH 07/10] test(gui): assert operator-approved route positions without weakening workspace safety --- tests/ARSAS.Tests/MaterialIconConsistencyTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ARSAS.Tests/MaterialIconConsistencyTests.cs b/tests/ARSAS.Tests/MaterialIconConsistencyTests.cs index f663cc37a..768c59481 100644 --- a/tests/ARSAS.Tests/MaterialIconConsistencyTests.cs +++ b/tests/ARSAS.Tests/MaterialIconConsistencyTests.cs @@ -17,14 +17,14 @@ public sealed class MaterialIconConsistencyTests private static readonly XNamespace X = "http://schemas.microsoft.com/winfx/2006/xaml"; [Theory] - [InlineData("NavExplorerButton", "0", "OperatorIecTopologyIcon")] - [InlineData("NavLiveButton", "1", "OperatorLiveMonitorIcon")] + [InlineData("NavExplorerButton", "1", "OperatorIecTopologyIcon")] + [InlineData("NavSmvButton", "5", "OperatorLiveMonitorIcon")] [InlineData("NavEventsButton", "2", "OperatorEventLogIcon")] [InlineData("NavAlarmButton", "3", "OperatorAlarmIcon")] [InlineData("NavGooseButton", "4", "OperatorIecWirelessIcon")] - [InlineData("NavDiagnosticsButton", "5", "OperatorDiagnosticsIcon")] + [InlineData("NavDiagnosticsButton", "7", "OperatorDiagnosticsIcon")] [InlineData("NavNativeFatButton", "6", "OperatorFatIcon")] - [InlineData("NavIedBrowserButton", "7", "OperatorIedBrowserIcon")] + [InlineData("NavIedBrowserButton", "0", "OperatorIedBrowserIcon")] public void EveryNavigationDestinationOwnsOneGlyphAndSameClickHandler( string buttonName, string index, string glyph) { From bd763550a3313ad6c13156a21d77aaa0a652a49c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:55:39 +0700 Subject: [PATCH 08/10] test(gui): assert operator-approved route positions without weakening workspace safety --- .../ARSAS.Tests/ProductionFatEngineeringTabRegressionTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ARSAS.Tests/ProductionFatEngineeringTabRegressionTests.cs b/tests/ARSAS.Tests/ProductionFatEngineeringTabRegressionTests.cs index d6e1f00d8..d8a809970 100644 --- a/tests/ARSAS.Tests/ProductionFatEngineeringTabRegressionTests.cs +++ b/tests/ARSAS.Tests/ProductionFatEngineeringTabRegressionTests.cs @@ -60,13 +60,13 @@ public void SeventhFatDestination_IsCanonicalMainWindowSiblingWithOneNavigationO Assert.Equal("NavButton_Click", (string?)fatButton.Attribute("Click")); Assert.Equal("{StaticResource SegmentedNavButton}", (string?)fatButton.Attribute("Style")); Assert.Contains(fatButton.Descendants(p + "TextBlock"), - label => (string?)label.Attribute("Text") == "FAT"); + label => (string?)label.Attribute("Text") == "Signal Testing"); Assert.Contains(fatButton.Descendants(p + "Path"), path => (string?)path.Attribute("Data") == "{StaticResource OperatorFatIcon}"); Assert.Contains("Grid.ColumnSpan=\"8\"", xaml, StringComparison.Ordinal); Assert.Contains("x:Name=\"NativeFatTab\" Header=\"FAT\"", xaml, StringComparison.Ordinal); - Assert.Contains("Math.Clamp(index, 0, IedBrowserWorkspaceIndex)", mainSource, StringComparison.Ordinal); + Assert.Contains("EngineeringNavigationRoute.TabForButton(index)", mainSource, StringComparison.Ordinal); Assert.Contains("var cellWidth = contentWidth / 8d", mainSource, StringComparison.Ordinal); Assert.Contains("NavNativeFatButton", mainSource, StringComparison.Ordinal); From 220154547a06955f5b9505ac4c7825be021fc69c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:55:43 +0700 Subject: [PATCH 09/10] test(gui): assert operator-approved route positions without weakening workspace safety --- .../ARSAS.Tests/AlarmAnnunciatorIntegrationRegressionTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ARSAS.Tests/AlarmAnnunciatorIntegrationRegressionTests.cs b/tests/ARSAS.Tests/AlarmAnnunciatorIntegrationRegressionTests.cs index 2d9ee2e3a..fd4834af1 100644 --- a/tests/ARSAS.Tests/AlarmAnnunciatorIntegrationRegressionTests.cs +++ b/tests/ARSAS.Tests/AlarmAnnunciatorIntegrationRegressionTests.cs @@ -24,9 +24,9 @@ public void Header_UsesSevenWorkflowDestinations_WithoutDuplicateRuntimeStatusCh Assert.Contains(navGrid.Descendants(p + "Button"), button => (string?)button.Attribute(x + "Name") == "NavAlarmButton" && (string?)button.Attribute("Tag") == "3" && - ((string?)button.Attribute("Content") == "Alarm" || + ((string?)button.Attribute("Content") == "Annunciator" || button.Descendants(p + "TextBlock").Any(label => - (string?)label.Attribute("Text") == "Alarm"))); + (string?)label.Attribute("Text") == "Annunciator"))); Assert.Contains(navGrid.Descendants(p + "Button"), button => (string?)button.Attribute(x + "Name") == "NavGooseButton" && (string?)button.Attribute("Tag") == "4"); From d110116662b5b431c9230b5f52a59658084c1ded Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 17:57:29 +0700 Subject: [PATCH 10/10] fix(gui): retain literal IED Browser caption in compact navigation --- MainWindow.NavigationLayoutFix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MainWindow.NavigationLayoutFix.cs b/MainWindow.NavigationLayoutFix.cs index d5ba6517a..f087445d1 100644 --- a/MainWindow.NavigationLayoutFix.cs +++ b/MainWindow.NavigationLayoutFix.cs @@ -39,7 +39,7 @@ internal static class MainWindowNavigationLayoutFix private static readonly string[] CompactLabels = [ - "Browser", + "IED Browser", "Devices", "Events", "Alarm",