From bf1505c508dd13fa157d9c84774815fe440484d8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 16 Sep 2026 16:57:21 +0700 Subject: [PATCH 1/5] fix(rcb): simplify client occupancy status indicators --- Models/RcbExportModels.cs | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/Models/RcbExportModels.cs b/Models/RcbExportModels.cs index 2d4417a28..4acd26de0 100644 --- a/Models/RcbExportModels.cs +++ b/Models/RcbExportModels.cs @@ -8,6 +8,9 @@ namespace ArIED61850Tester.Models; public sealed class RcbExportRow : ObservableObject { + private static readonly Brush ReadyIndicatorBrush = BrushFrom(22, 163, 74); + private static readonly Brush OccupiedIndicatorBrush = BrushFrom(234, 179, 8); + private bool _isSelected; private int _memberCount; private MmsRcbOperationalAvailability _availability = MmsRcbOperationalAvailability.Unknown; @@ -51,6 +54,7 @@ public MmsRcbOperationalAvailability Availability if (!Set(ref _availability, value)) return; Raise(nameof(IsSelectable)); Raise(nameof(RequiresConfirmation)); + Raise(nameof(IsClientOccupied)); Raise(nameof(StatusGlyph)); Raise(nameof(StatusBrush)); } @@ -61,30 +65,16 @@ public MmsRcbOperationalAvailability Availability public string Reason { get => _reason; set => Set(ref _reason, value?.Trim() ?? string.Empty); } public string Owner { get => _owner; set => Set(ref _owner, value?.Trim() ?? string.Empty); } - // Availability/ownership is evidence for the operator, not an export lock. - // Every discovered RCB remains selectable so the exported engineering model - // can truthfully represent what the IED exposes, including InUse/NoDataSet. + // Availability/ownership remains engineering evidence and still drives confirmation. + // The table intentionally presents only a calm client-occupancy indicator: yellow means + // an active client owns/uses the RCB; every other state is green and details stay in tooltip/evidence. public bool IsSelectable => true; - + public bool IsClientOccupied => Availability is MmsRcbOperationalAvailability.InUse or MmsRcbOperationalAvailability.UsedByCaller; public bool RequiresConfirmation => HasEvidenceConflict || Availability is not MmsRcbOperationalAvailability.Available; public string MemberCountText => MemberCount > 0 ? $"{MemberCount:N0} FCDA" : "0 FCDA"; - public string StatusGlyph => Availability switch - { - MmsRcbOperationalAvailability.Available => "✅", - MmsRcbOperationalAvailability.UsedByCaller => "●", - MmsRcbOperationalAvailability.Unknown => "⚠", - _ => "❌" - }; - public Brush StatusBrush => HasEvidenceConflict - ? BrushFrom(201, 42, 50) - : Availability switch - { - MmsRcbOperationalAvailability.Available => BrushFrom(22, 163, 74), - MmsRcbOperationalAvailability.UsedByCaller => BrushFrom(37, 99, 235), - MmsRcbOperationalAvailability.Unknown => BrushFrom(202, 138, 4), - _ => BrushFrom(201, 42, 50) - }; + public string StatusGlyph => "■"; + public Brush StatusBrush => IsClientOccupied ? OccupiedIndicatorBrush : ReadyIndicatorBrush; public string SelectionIdentity => string.IsNullOrWhiteSpace(Reference) ? Name : Reference; public static string ToStatusText(MmsRcbOperationalAvailability availability) From 52b80258442a1440eaab7af4f1555f1e60a7a3cb Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 16 Sep 2026 16:57:54 +0700 Subject: [PATCH 2/5] fix(rcb): use calm square occupancy indicators --- RcbExportFilterWindow.xaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/RcbExportFilterWindow.xaml b/RcbExportFilterWindow.xaml index 64c9c95bf..3c2bff68e 100644 --- a/RcbExportFilterWindow.xaml +++ b/RcbExportFilterWindow.xaml @@ -183,15 +183,13 @@ - + - - - - + + + From 2a802ebce32d1b059471b1edd49443e95b13fe15 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 16 Sep 2026 16:58:51 +0700 Subject: [PATCH 3/5] fix(rcb): keep live instance UX and canonical source export --- MainWindow.RcbExport.cs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/MainWindow.RcbExport.cs b/MainWindow.RcbExport.cs index 44c95f25d..7d7c89f64 100644 --- a/MainWindow.RcbExport.cs +++ b/MainWindow.RcbExport.cs @@ -82,18 +82,30 @@ private static IReadOnlyList BuildRcbExportRows( if (sourceInventory == null) return liveRows; - // Never let an older/source SCL hide RCBs that the connected IED actually - // exposes. Source-backed rows are preferred for exact export identity, then - // unmatched live-discovery rows are appended as first-class export choices. + // Present concrete live instances when the IED exposes them, but never append the + // corresponding logical SCL ReportControl as a duplicate row. For example, a source + // `Buffer` with RptEnabled max=2 may appear online as Buffer01 + Buffer02. The source + // logical identity remains export authority; the concrete instance names are UX/runtime evidence. var rows = BuildSourceBackedRcbRows(device, sourceInventory, availability).ToList(); var seen = rows .Select(row => NormalizeRcbReference(row.Reference)) .Where(reference => !string.IsNullOrWhiteSpace(reference)) .ToHashSet(StringComparer.OrdinalIgnoreCase); + var sourceBackedSelectionKeys = rows + .Where(row => row.IsSourceBacked && !string.IsNullOrWhiteSpace(row.SourceSelectionKey)) + .Select(row => row.SourceSelectionKey) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + var logicalSourceReferences = sourceInventory.ReportControls + .Where(descriptor => sourceBackedSelectionKeys.Contains(descriptor.SelectionKey)) + .Select(descriptor => NormalizeRcbReference(descriptor.DisplayReference)) + .Where(reference => !string.IsNullOrWhiteSpace(reference)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var liveRow in liveRows) { var key = NormalizeRcbReference(liveRow.Reference); + if (logicalSourceReferences.Contains(key)) + continue; if (!seen.Add(key)) continue; rows.Add(liveRow); @@ -361,6 +373,7 @@ private async Task ExportLegacySasRcbAsync( SchemaProfile = schema, SelectedReportControl = new SclReportControlSelection(row.SourceSelectionKey, row.ExportName), RemoveUnreferencedDataSets = false, + PreserveSourceReportControlIdentity = true, ToolId = "ARIEC61850" }), cancellationToken); @@ -459,4 +472,4 @@ private static string LastReferenceSegment(string? reference) private static string NormalizeRcbReference(string? reference) => (reference ?? string.Empty).Trim().Replace('$', '.').ToLowerInvariant(); -} +} \ No newline at end of file From 97d1a81ef64af32f7dcd451b2f2d2abf591eedaa Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Wed, 16 Sep 2026 16:59:09 +0700 Subject: [PATCH 4/5] test(rcb): lock IEDScout projection and status UX --- .../RcbIedScoutParityRegressionTests.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/ARSAS.Tests/RcbIedScoutParityRegressionTests.cs diff --git a/tests/ARSAS.Tests/RcbIedScoutParityRegressionTests.cs b/tests/ARSAS.Tests/RcbIedScoutParityRegressionTests.cs new file mode 100644 index 000000000..11e70c409 --- /dev/null +++ b/tests/ARSAS.Tests/RcbIedScoutParityRegressionTests.cs @@ -0,0 +1,51 @@ +namespace ARSAS.Tests; + +public sealed class RcbIedScoutParityRegressionTests +{ + [Fact] + public void SourceBackedRuntimeInstances_Suppress_Logical_Scl_Duplicate() + { + var source = ReadRepoFile("MainWindow.RcbExport.cs"); + + Assert.Contains("logicalSourceReferences", source, StringComparison.Ordinal); + Assert.Contains("logicalSourceReferences.Contains(key)", source, StringComparison.Ordinal); + Assert.Contains("sourceBackedSelectionKeys", source, StringComparison.Ordinal); + } + + [Fact] + public void SourceBackedExport_Preserves_Canonical_Scl_ReportControl_Identity() + { + var source = ReadRepoFile("MainWindow.RcbExport.cs"); + + Assert.Contains("PreserveSourceReportControlIdentity = true", source, StringComparison.Ordinal); + Assert.Contains("new SclReportControlSelection(row.SourceSelectionKey, row.ExportName)", source, StringComparison.Ordinal); + } + + [Fact] + public void RcbStatus_Is_SquareOnly_YellowForClientUse_AndGreenOtherwise() + { + var model = ReadRepoFile("Models/RcbExportModels.cs"); + var xaml = ReadRepoFile("RcbExportFilterWindow.xaml"); + + Assert.Contains("Availability is MmsRcbOperationalAvailability.InUse or MmsRcbOperationalAvailability.UsedByCaller", model, StringComparison.Ordinal); + Assert.Contains("IsClientOccupied ? OccupiedIndicatorBrush : ReadyIndicatorBrush", model, StringComparison.Ordinal); + Assert.Contains("BrushFrom(234, 179, 8)", model, StringComparison.Ordinal); + Assert.Contains("BrushFrom(22, 163, 74)", model, StringComparison.Ordinal); + Assert.Contains(" Date: Wed, 16 Sep 2026 17:04:06 +0700 Subject: [PATCH 5/5] chore(engine): pin source-backed RCB export identity fix --- engines/ARIEC61850.lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index bc0491d2e..e4e2a9548 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,13 +2,13 @@ "schemaVersion": 1, "repository": "masarray/ARIEC61850", "ref": "main", - "commit": "0023ef9a4373855497464ed3979e359c4041c95d", - "sourcePullRequest": 132, - "purpose": "Temporary ARSAS 1.6.36 combined-workstream qualification pin for the exact green ARIEC convergence head. ARIEC .NET CI #603 passed on this SHA, including provenance/source/license verification, restore, build, tests, and diagnostics. The convergence preserves the trusted-SCL golden-wire contracts used by ARSAS: SCL-authoritative DataSet/RCB identity, LDevice ldName and ReportControl indexed semantics, quoted Edition-1/vendor OSI-AP-Title compatibility, Domain/VMD reconciliation, bounded sequential initial FC-root Reads, receiver-before-write report registration, URCB Resv -> RptEna, BRCB direct RptEna with ResvTms retry-only, two whole-RCB verification reads, one-shot GI after routing is registered, GI fail-closed cleanup, no cyclic process polling, no network DataSet-directory browse, and no dynamic DataSet mutation on the trusted-SCL path. The same convergence also locks buffered BRCB latest-state semantics through the canonical runtime value plane and preserves SCL RptEnabled@max only as diagnostics metadata; it is never authority to synthesize concrete runtime RCB names.", + "commit": "3afc924c97627fe86adbe784c905e2f35dff0b1a", + "sourcePullRequest": 133, + "purpose": "Pins merged ARIEC61850 PR #133 on top of the exact PR #132 golden-wire tree. ARIEC .NET CI #605 passed source/provenance verification, restore, build, and tests. This adds only an opt-in source-backed Legacy SAS export boundary: ARSAS may display concrete live runtime RCB slots such as Buffer01/Buffer02 while CID/IID export retains the logical source ReportControl identity and RptEnabled indexing metadata. The option is default-off, so existing exact-runtime live-model export remains unchanged. All trusted-SCL acquisition, reporting and control contracts from PR #132 remain unchanged: SCL-authoritative DataSet/RCB identity, LDevice ldName and ReportControl indexed semantics, quoted Edition-1/vendor OSI-AP-Title compatibility, Domain/VMD reconciliation, bounded sequential initial FC-root Reads, receiver-before-write report registration, URCB Resv -> RptEna, BRCB direct RptEna with ResvTms retry-only, two whole-RCB verification reads, one-shot GI after routing is registered, GI fail-closed cleanup, no cyclic process polling, no network DataSet-directory browse, and no dynamic DataSet mutation on the trusted-SCL path. SCL RptEnabled@max remains declarative design metadata and is never authority to synthesize concrete runtime RCB names.", "previousTrialPin": { - "commit": "d50e5bcb9fd428fe3d80ac72f8d4015a575cfda5", - "sourcePullRequest": 125, - "purpose": "Previous ARSAS 1.6.36 trusted-SCL golden-wire trial pin retained for explicit ancestry." + "commit": "0023ef9a4373855497464ed3979e359c4041c95d", + "sourcePullRequest": 132, + "purpose": "Previous ARSAS 1.6.36 combined golden-wire convergence pin retained for explicit ancestry." }, "fieldProvenBaseline": { "commit": "11ab2304482600c19ba979f4fc9021ddb46b9af9",