From e568e690f4cf888689fa09345ac5761b05b12c3e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Tue, 13 Oct 2026 16:02:18 +0700 Subject: [PATCH 001/109] release(P10.9): restore proven RCB exporter and seven-tab shell; retain Event properties drawer --- FaultRecordUxBehavior.cs | 4 +- MainWindow.IedBrowser.cs | 302 ------------------ MainWindow.MultiRcbExport.cs | 2 +- MainWindow.NavigationLayoutFix.cs | 31 +- MainWindow.RcbCapability.cs | 2 +- MainWindow.RcbExport.cs | 21 +- MainWindow.xaml | 154 +-------- MainWindow.xaml.cs | 19 +- Models/RcbExportModels.cs | 3 - Models/RcbInspectionEvidence.cs | 119 ------- RcbMultiExportWindow.cs | 189 +---------- SasOperationalUiPolicy.cs | 2 +- Services/RcbSclReportIdCatalog.cs | 61 ---- ...rmAnnunciatorIntegrationRegressionTests.cs | 12 +- .../MainWindowTopBarLayoutRegressionTests.cs | 17 +- ...09IedBrowserAndEventDrawerContractTests.cs | 85 ----- ...09ScopedReleaseNoBrowserRegressionTests.cs | 48 +++ .../P109ZFinalInspectorRegressionTests.cs | 86 ----- ...ductionFatEngineeringTabRegressionTests.cs | 6 +- .../VisualSystemP0RegressionTests.cs | 6 +- 20 files changed, 102 insertions(+), 1067 deletions(-) delete mode 100644 MainWindow.IedBrowser.cs delete mode 100644 Models/RcbInspectionEvidence.cs delete mode 100644 Services/RcbSclReportIdCatalog.cs delete mode 100644 tests/ARSAS.Tests/P109IedBrowserAndEventDrawerContractTests.cs create mode 100644 tests/ARSAS.Tests/P109ScopedReleaseNoBrowserRegressionTests.cs delete mode 100644 tests/ARSAS.Tests/P109ZFinalInspectorRegressionTests.cs diff --git a/FaultRecordUxBehavior.cs b/FaultRecordUxBehavior.cs index 2812514d1..e16d42d3e 100644 --- a/FaultRecordUxBehavior.cs +++ b/FaultRecordUxBehavior.cs @@ -235,8 +235,8 @@ private static void RefreshCapabilityState( rcbButton, hasRcbInventory ? CapabilityState.Available : CapabilityState.Unavailable, hasRcbInventory - ? $"Browse Report Control Blocks and details for {device.Name}" - : $"Open SCL or complete live discovery before browsing RCBs for {device.Name}"); + ? $"Select and export one RCB for legacy SAS import from {device.Name}" + : $"Open SCL or complete live discovery before using RCB Export for {device.Name}"); } private static void ApplyState(Button button, CapabilityState state, string toolTip) diff --git a/MainWindow.IedBrowser.cs b/MainWindow.IedBrowser.cs deleted file mode 100644 index 6a68379c3..000000000 --- a/MainWindow.IedBrowser.cs +++ /dev/null @@ -1,302 +0,0 @@ -using System.ComponentModel; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Threading; -using AR.Iec61850.Discovery; -using AR.Iec61850.Mms; -using AR.Iec61850.Scl.Export; -using ArIED61850Tester.Models; - -namespace ArIED61850Tester; - -/// -/// Model-first, read-only IEC 61850 browser. Refreshing the browser is purely local. -/// An explicit button is the only way to request a separate read-only MMS RCB check. -/// No discovery, report activation, write, reservation, or reconnect on navigation. -/// -public partial class MainWindow -{ - private const int IedBrowserWorkspaceIndex = 7; - private Iec61850MonitorDevice? _iedBrowserObservedDevice; - private MmsRcbAvailabilityResult? _iedBrowserSnapshot; - private CancellationTokenSource? _iedBrowserReadOperation; - - internal void OpenIedBrowserRcb(Iec61850MonitorDevice device) - { - ArgumentNullException.ThrowIfNull(device); - SelectedDevice = device; - MainTabs.SelectedIndex = IedBrowserWorkspaceIndex; - IedBrowserSections.SelectedIndex = 0; - RefreshIedBrowser(); - } - - private void IedBrowserRefresh_Click(object sender, RoutedEventArgs e) - { - // Read from existing memory/source SCL only, never an MMS request. - RefreshIedBrowser(); - } - - private void IedBrowserExportRcb_Click(object sender, RoutedEventArgs e) - { - if (SelectedDevice is not { } device || device.IsBusy || device.IsDemo) - return; - IedEditRcbMulti_Click(new Button { Tag = device }, new RoutedEventArgs()); - } - - private async void IedBrowserReadRcb_Click(object sender, RoutedEventArgs e) - { - var device = SelectedDevice; - if (device == null || !device.IsConnected || device.IsBusy || _iedBrowserReadOperation != null) - return; - - var sourceDevice = device; - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(35)); - _iedBrowserReadOperation = cts; - IedBrowserReadRcbButton.IsEnabled = false; - IedBrowserSubtitle.Text = "Reading RCB availability through an explicit read-only MMS association…"; - - try - { - var result = await _rcbAvailabilityProbe.CheckAsync(device, cts.Token); - if (cts.IsCancellationRequested || !ReferenceEquals(SelectedDevice, sourceDevice) || - !ReferenceEquals(_iedBrowserObservedDevice, sourceDevice) || !sourceDevice.IsConnected || - MainTabs.SelectedIndex != IedBrowserWorkspaceIndex) - return; - - _iedBrowserSnapshot = result; - RefreshIedBrowser(); - } - catch (OperationCanceledException) - { - if (ReferenceEquals(SelectedDevice, sourceDevice)) - IedBrowserSubtitle.Text = "RCB check cancelled or timed out; no RCB modified."; - } - catch (Exception ex) - { - if (ReferenceEquals(SelectedDevice, sourceDevice)) - IedBrowserSubtitle.Text = $"Read-only RCB check failed: {ex.Message}"; - } - finally - { - if (ReferenceEquals(_iedBrowserReadOperation, cts)) - _iedBrowserReadOperation = null; - cts.Dispose(); - if (ReferenceEquals(SelectedDevice, sourceDevice)) - IedBrowserReadRcbButton.IsEnabled = sourceDevice.IsConnected && !sourceDevice.IsBusy; - } - } - - private void CancelIedBrowserRead() - { - _iedBrowserReadOperation?.Cancel(); - } - - private void ObserveIedBrowserDevice(Iec61850MonitorDevice? device) - { - if (ReferenceEquals(_iedBrowserObservedDevice, device)) return; - CancelIedBrowserRead(); - if (_iedBrowserObservedDevice != null) - _iedBrowserObservedDevice.PropertyChanged -= IedBrowserDevice_PropertyChanged; - _iedBrowserObservedDevice = device; - _iedBrowserSnapshot = null; - if (device != null) - device.PropertyChanged += IedBrowserDevice_PropertyChanged; - } - - private void IedBrowserDevice_PropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName is not (nameof(Iec61850MonitorDevice.IsConnected) - or nameof(Iec61850MonitorDevice.IsBusy))) - return; - if (!Dispatcher.CheckAccess()) - { - Dispatcher.BeginInvoke(DispatcherPriority.DataBind, - new Action(() => IedBrowserDevice_PropertyChanged(sender, e))); - return; - } - - if (!ReferenceEquals(sender, _iedBrowserObservedDevice)) - return; - - // A disconnected/replaced association can never supply "live" inspector values. - if (_iedBrowserObservedDevice?.IsConnected != true) - { - CancelIedBrowserRead(); - _iedBrowserSnapshot = null; - } - if (MainTabs.SelectedIndex == IedBrowserWorkspaceIndex) - RefreshIedBrowser(); - } - - private void RefreshIedBrowser() - { - if (IedBrowserTab == null || MainTabs.SelectedIndex != IedBrowserWorkspaceIndex) - return; - var device = SelectedDevice; - ObserveIedBrowserDevice(device); - IedBrowserRcbGrid.ItemsSource = null; - IedBrowserRcbProperties.ItemsSource = null; - IedBrowserRcbEvidence.Text = "Select a Report Control Block to inspect properties."; - IedBrowserDataSetGrid.ItemsSource = null; - IedBrowserDataSetMembers.ItemsSource = null; - IedBrowserDataSetEvidence.Text = "Select a DataSet to inspect the exact FCDA members."; - IedBrowserModelTree.Items.Clear(); - IedBrowserModelProperties.ItemsSource = null; - IedBrowserSignalsGrid.ItemsSource = null; - - if (device == null) - { - IedBrowserSubtitle.Text = "Select an IED in Explorer. Inspection does not initiate a connection."; - IedBrowserReadRcbButton.IsEnabled = false; - IedBrowserExportRcbButton.IsEnabled = false; - return; - } - - IedBrowserReadRcbButton.IsEnabled = device.IsConnected && !device.IsBusy && - _iedBrowserReadOperation == null; - IedBrowserExportRcbButton.IsEnabled = !device.IsBusy && !device.IsDemo; - - var model = device.LiveDiscoveryModel ?? device.SclWorkspace?.DesignModel; - IedBrowserSubtitle.Text = $"{device.Name} · {device.EndpointText} · " + - (device.IsConnected ? "Connected" : "Offline model") + - $" · {model?.LogicalDevices.Count ?? 0} LD / {model?.DataSets.Count ?? 0} DataSets / " + - $"{model?.ReportControls.Count ?? 0} RCB"; - - SclReportControlInventoryResult? sourceInventory = null; - if (!string.IsNullOrWhiteSpace(device.SclSourcePath) && File.Exists(device.SclSourcePath)) - { - try - { - sourceInventory = SclReportControlFilter.InspectFile( - device.SclSourcePath, EffectiveSclIedName(device), device.SclAccessPointName); - } - catch (Exception ex) - { - IedBrowserRcbEvidence.Text = $"Source SCL RCB inventory: {ex.Message}"; - } - } - - var rows = BuildRcbExportRows(device, sourceInventory, _iedBrowserSnapshot); - if (rows.Count == 0 && model != null) - rows = BuildLiveModelRcbRows(model, _iedBrowserSnapshot); - IedBrowserRcbGrid.ItemsSource = rows; - IedBrowserDataSetGrid.ItemsSource = model?.DataSets; - IedBrowserSignalsGrid.ItemsSource = device.Signals; - - if (model == null) - return; - - // Create only LD/LN nodes now. DO and DA children are materialized upon expansion - // so large 1000+ DO IEDs do not freeze the WPF dispatcher. - foreach (var ld in model.LogicalDevices) - { - var deviceNode = new TreeViewItem - { - Header = $"{ld.MmsDomain} · {ld.LogicalNodes.Count} LN", - Tag = ld - }; - foreach (var ln in ld.LogicalNodes) - { - var node = new TreeViewItem - { - Header = $"{ln.Name} · {ln.DataObjects.Count} DO", - Tag = ln - }; - if (ln.DataObjects.Count > 0) - node.Items.Add(new TreeViewItem { Header = "Loading…" }); - deviceNode.Items.Add(node); - } - IedBrowserModelTree.Items.Add(deviceNode); - } - } - - private void IedBrowserRcb_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (IedBrowserRcbGrid.SelectedItem is not RcbExportRow row) - return; - var connected = SelectedDevice?.IsConnected == true; - IedBrowserRcbEvidence.Text = row.Inspection.Summary(connected) + - " · Unavailable means not measured (not zero). No RCB write on inspect."; - IedBrowserRcbProperties.ItemsSource = row.Inspection.Properties(row, connected); - } - - private void IedBrowserDataSet_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (IedBrowserDataSetGrid.SelectedItem is not LiveIedDataSetModel set) - return; - IedBrowserDataSetEvidence.Text = $"{set.Reference} · {set.MemberCount} FCDA" + - (set.IsDeletable.HasValue ? $" · deletable={set.IsDeletable.Value}" : "") + - " · saved SCL/discovery membership"; - IedBrowserDataSetMembers.ItemsSource = set.Members; - } - - private void IedBrowserModelNode_Expanded(object sender, RoutedEventArgs e) - { - if (e.OriginalSource is not TreeViewItem item || - item.Items.Count != 1 || item.Items[0] is not TreeViewItem { Tag: null }) - return; - - if (item.Tag is LiveIedLogicalNodeModel ln) - { - item.Items.Clear(); - foreach (var data in ln.DataObjects) - { - var node = new TreeViewItem - { - Header = $"{data.Name} · {data.InferredCdc}", - Tag = data - }; - if (data.Attributes.Count > 0) - node.Items.Add(new TreeViewItem { Header = "Loading…" }); - item.Items.Add(node); - } - } - else if (item.Tag is LiveIedDataObjectModel obj) - { - item.Items.Clear(); - foreach (var attribute in obj.Attributes) - item.Items.Add(new TreeViewItem - { - Header = $"[{attribute.FunctionalConstraint}] {attribute.AttributePath}", - Tag = attribute - }); - } - } - - private void IedBrowserModelTree_SelectedItemChanged(object sender, - RoutedPropertyChangedEventArgs e) - { - if (e.NewValue is not TreeViewItem item) - return; - static RcbInspectionProperty P(string name, string value) - => new(name, string.IsNullOrWhiteSpace(value) ? "—" : value, "Saved model"); - IedBrowserModelProperties.ItemsSource = item.Tag switch - { - LiveIedLogicalDeviceModel ld => new[] - { - P("MMS Domain", ld.MmsDomain), P("LDevice instance", ld.Inst), - P("Logical Nodes", ld.LogicalNodes.Count.ToString()) - }, - LiveIedLogicalNodeModel ln => new[] - { - P("Logical Node", ln.Name), P("Class", ln.LnClass), - P("Type", ln.ProposedLnTypeId), P("Data Objects", ln.DataObjects.Count.ToString()) - }, - LiveIedDataObjectModel obj => new[] - { - P("Reference", obj.Reference), P("Name", obj.Name), - P("CDC (inferred)", obj.InferredCdc), - P("Confidence", obj.ConfidenceLevel.ToString()), - P("Data Attributes", obj.Attributes.Count.ToString()) - }, - LiveIedDataAttributeModel attr => new[] - { - P("Reference", attr.ObjectReference), P("DA path", attr.AttributePath), - P("FC", attr.FunctionalConstraint), P("MMS Reference", attr.MmsReference), - P("MMS type", attr.MmsType), P("SCL bType", attr.SclBType), - P("Type source", attr.TypeSource), P("Confidence", attr.TypeConfidence.ToString()) - }, - _ => Array.Empty() - }; - } -} diff --git a/MainWindow.MultiRcbExport.cs b/MainWindow.MultiRcbExport.cs index 4699b2893..d44c5d470 100644 --- a/MainWindow.MultiRcbExport.cs +++ b/MainWindow.MultiRcbExport.cs @@ -122,7 +122,7 @@ private void IedEditRcbMulti_Click(object sender, RoutedEventArgs e) latestAvailability, cancellationToken) .ConfigureAwait(true); - }, device) + }) { Owner = this }; diff --git a/MainWindow.NavigationLayoutFix.cs b/MainWindow.NavigationLayoutFix.cs index 17aee0191..2051bf249 100644 --- a/MainWindow.NavigationLayoutFix.cs +++ b/MainWindow.NavigationLayoutFix.cs @@ -21,9 +21,9 @@ internal static class MainWindowNavigationLayoutFix { private const double WideBreakpoint = 1700d; private const double MediumBreakpoint = 1380d; - private const double WideNavWidth = 1150d; - private const double MediumNavWidth = 1010d; - private const double CompactNavWidth = 842d; + private const double WideNavWidth = 990d; + private const double MediumNavWidth = 900d; + private const double CompactNavWidth = 720d; private static readonly string[] FullLabels = [ @@ -31,10 +31,7 @@ internal static class MainWindowNavigationLayoutFix "Live Monitor", "Event Log", "Alarm", - "GOOSE Subscriber", - "Diagnostics", - "FAT", - "IED Browser" + "GOOSE Subscriber" ]; private static readonly string[] CompactLabels = @@ -43,10 +40,7 @@ internal static class MainWindowNavigationLayoutFix "Live", "Events", "Alarm", - "GOOSE", - "Diagnostics", - "FAT", - "Browser" + "GOOSE" ]; [ModuleInitializer] @@ -98,7 +92,7 @@ private static void OnMainWindowButtonClick(object sender, RoutedEventArgs e) if (sender is not MainWindow window || e.Source is not Button button) return; - if (button.Name is not ("NavExplorerButton" or "NavLiveButton" or "NavEventsButton" or "NavAlarmButton" or "NavGooseButton" or "NavDiagnosticsButton" or "NavNativeFatButton" or "NavIedBrowserButton")) + if (button.Name is not ("NavExplorerButton" or "NavLiveButton" or "NavEventsButton" or "NavAlarmButton" or "NavGooseButton" or "NavDiagnosticsButton" or "NavNativeFatButton")) return; // A repeated click on the already-selected tab does not raise SelectionChanged, @@ -160,7 +154,7 @@ private static void ApplyResponsiveLayout(MainWindow window) // Diagnostics owns a StackPanel containing its text plus the red alert // badge. Never replace that Content tree while changing layout density. - if (index != 5 && index < labels.Length) + if (index < labels.Length) button.Content = labels[index]; button.MinHeight = 40; @@ -187,8 +181,7 @@ private static void ApplyResponsiveLayout(MainWindow window) window.FindName("NavAlarmButton") as Button, window.FindName("NavGooseButton") as Button, window.FindName("NavDiagnosticsButton") as Button, - window.FindName("NavNativeFatButton") as Button, - window.FindName("NavIedBrowserButton") as Button + window.FindName("NavNativeFatButton") as Button ]; private static void UpdatePillGeometry(MainWindow window, double shellWidth) @@ -197,9 +190,9 @@ private static void UpdatePillGeometry(MainWindow window, double shellWidth) return; // Border padding owns 10 px horizontally. The nav grid itself is divided into - // eight equal star columns, so this is the exact width used by each button cell. + // seven equal star columns, so this is the exact width used by each button cell. var contentWidth = Math.Max(0d, shellWidth - 10d); - var cellWidth = contentWidth / 8d; + var cellWidth = contentWidth / 7d; pill.Width = Math.Max(1d, cellWidth - 2d); pill.Height = 36; pill.HorizontalAlignment = HorizontalAlignment.Left; @@ -231,8 +224,8 @@ private static void PositionPill(MainWindow window, bool animate) if (contentWidth <= 0d) contentWidth = Math.Max(0d, shell.Width - shell.Padding.Left - shell.Padding.Right); - var cellWidth = contentWidth / 8d; - var target = Math.Clamp(tabs.SelectedIndex, 0, 7) * cellWidth; + var cellWidth = contentWidth / 7d; + var target = Math.Clamp(tabs.SelectedIndex, 0, 6) * cellWidth; pill.Width = Math.Max(1d, cellWidth - 2d); translate.BeginAnimation(TranslateTransform.XProperty, null); diff --git a/MainWindow.RcbCapability.cs b/MainWindow.RcbCapability.cs index c31d3304e..0760e69b2 100644 --- a/MainWindow.RcbCapability.cs +++ b/MainWindow.RcbCapability.cs @@ -9,6 +9,6 @@ public partial class MainWindow internal void OpenRcbExportFilter(Iec61850MonitorDevice device) { ArgumentNullException.ThrowIfNull(device); - OpenIedBrowserRcb(device); + IedEditRcb_Click(new Button { Tag = device }, new RoutedEventArgs()); } } diff --git a/MainWindow.RcbExport.cs b/MainWindow.RcbExport.cs index 2657cc910..fbe25ad7d 100644 --- a/MainWindow.RcbExport.cs +++ b/MainWindow.RcbExport.cs @@ -121,7 +121,6 @@ private static IReadOnlyList BuildSourceBackedRcbRows( { var rows = new List(); var mappedKeys = new HashSet(StringComparer.OrdinalIgnoreCase); - var sourceReportIds = RcbSclReportIdCatalog.Read(device.SclSourcePath, inventory.ReportControls); if (availability != null) { @@ -130,16 +129,14 @@ private static IReadOnlyList BuildSourceBackedRcbRows( var descriptor = FindSourceDescriptor(snapshot, inventory.ReportControls); if (descriptor == null) continue; mappedKeys.Add(descriptor.SelectionKey); - rows.Add(CreateSourceBackedRow(descriptor, snapshot, device.IsConnected, - sourceReportIds.GetValueOrDefault(descriptor.SelectionKey, string.Empty))); + rows.Add(CreateSourceBackedRow(descriptor, snapshot)); } } foreach (var descriptor in inventory.ReportControls) { if (mappedKeys.Contains(descriptor.SelectionKey)) continue; - rows.Add(CreateSourceBackedRow(descriptor, snapshot: null, device.IsConnected, - sourceReportIds.GetValueOrDefault(descriptor.SelectionKey, string.Empty))); + rows.Add(CreateSourceBackedRow(descriptor, snapshot: null, device.IsConnected)); } return rows; } @@ -147,8 +144,7 @@ private static IReadOnlyList BuildSourceBackedRcbRows( private static RcbExportRow CreateSourceBackedRow( SclReportControlDescriptor descriptor, MmsRcbAvailabilitySnapshot? snapshot, - bool connected = true, - string sourceReportId = "") + bool connected = true) { var probeState = snapshot?.DataSetProbeState ?? MmsRcbDataSetProbeState.NotAttempted; var effectiveDataSetReference = RcbExportEvidencePolicy.EffectiveDataSetReference( @@ -221,8 +217,7 @@ private static RcbExportRow CreateSourceBackedRow( Owner = snapshot?.Owner ?? string.Empty, HasEvidenceConflict = evidenceConflict, IsSourceBacked = true, - IsIndexedSource = descriptor.Indexed, - Inspection = RcbInspectionEvidence.FromSource(sourceReportId, snapshot) + IsIndexedSource = descriptor.Indexed }; } @@ -347,8 +342,7 @@ private static IReadOnlyList BuildLiveModelRcbRows( dataSet != null, members), Owner = snapshot?.Owner ?? string.Empty, - IsSourceBacked = false, - Inspection = RcbInspectionEvidence.FromDiscovery(reportControl, snapshot) + IsSourceBacked = false }); } return rows; @@ -505,8 +499,6 @@ private async Task ExportLegacySasRcbAsync( } var filteredModel = SclReportControlFilter.FilterLiveModel(exportModel, row.Reference); - var verifiedIdentity = await TrustedSclIdentityAuthority.TryMatchAsync( - device,filteredModel,cancellationToken); var liveResult = await Task.Run(() => AuthoritativeLiveIedSclExporter.WriteFiles( filteredModel, outputPath, @@ -515,8 +507,7 @@ private async Task ExportLegacySasRcbAsync( Profile = "full-model", SchemaProfile = schema, IpAddress = device.IpAddress, - IedNameOverride = verifiedIdentity?.IedName ?? device.Name, - VerifiedIdentity = verifiedIdentity, + IedNameOverride = device.Name, IncludeLowConfidenceTypes = true, IncludeRuntimeStateComment = false }), cancellationToken); diff --git a/MainWindow.xaml b/MainWindow.xaml index 452c75f76..654b31bb7 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -94,9 +94,8 @@ - - @@ -126,7 +125,6 @@