Skip to content
Merged
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
21 changes: 17 additions & 4 deletions MainWindow.RcbExport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,30 @@ private static IReadOnlyList<RcbExportRow> 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);
Expand Down Expand Up @@ -361,6 +373,7 @@ private async Task<RcbExportCompletion> ExportLegacySasRcbAsync(
SchemaProfile = schema,
SelectedReportControl = new SclReportControlSelection(row.SourceSelectionKey, row.ExportName),
RemoveUnreferencedDataSets = false,
PreserveSourceReportControlIdentity = true,
ToolId = "ARIEC61850"
}), cancellationToken);

Expand Down Expand Up @@ -459,4 +472,4 @@ private static string LastReferenceSegment(string? reference)

private static string NormalizeRcbReference(string? reference)
=> (reference ?? string.Empty).Trim().Replace('$', '.').ToLowerInvariant();
}
}
30 changes: 10 additions & 20 deletions Models/RcbExportModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand All @@ -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)
Expand Down
12 changes: 5 additions & 7 deletions RcbExportFilterWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,13 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

<DataGridTemplateColumn Header="STATUS" Width="1.2*">
<DataGridTemplateColumn Header="STATUS" Width="82">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="12,0,8,0"
VerticalAlignment="Center" ToolTip="{Binding Reason}">
<Ellipse Width="7" Height="7" Fill="{Binding StatusBrush}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding StatusText}" Margin="7,0,0,0" FontWeight="SemiBold"
Foreground="{Binding StatusBrush}" VerticalAlignment="Center"/>
</StackPanel>
<Grid Margin="12,0,8,0" VerticalAlignment="Center" ToolTip="{Binding Reason}">
<Border Width="11" Height="11" CornerRadius="1" Background="{Binding StatusBrush}"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Expand Down
12 changes: 6 additions & 6 deletions engines/ARIEC61850.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
51 changes: 51 additions & 0 deletions tests/ARSAS.Tests/RcbIedScoutParityRegressionTests.cs
Original file line number Diff line number Diff line change
@@ -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("<Border Width=\"11\" Height=\"11\" CornerRadius=\"1\" Background=\"{Binding StatusBrush}\"", xaml, StringComparison.Ordinal);
Assert.DoesNotContain("Text=\"{Binding StatusText}\"", xaml, StringComparison.Ordinal);
}

private static string ReadRepoFile(string relativePath)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory != null)
{
var candidate = Path.Combine(directory.FullName, relativePath);
if (File.Exists(candidate))
return File.ReadAllText(candidate).Replace("\r\n", "\n", StringComparison.Ordinal);
directory = directory.Parent;
}

throw new FileNotFoundException(relativePath);
}
}
Loading