Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c1972be
fix: select only RCB-backed static DataSets
masarray Sep 5, 2026
cc60008
fix: resolve indexed static RCB instances safely
masarray Sep 5, 2026
20e30e7
fix: preserve SCL RCB authority and resolve indexed live instances
masarray Sep 5, 2026
2830f3a
test: guard RCB-backed static selection and indexed instances
masarray Sep 5, 2026
02c51cb
fix: union SCL and live static membership authority
masarray Sep 5, 2026
f9633fb
fix: make static authority projection compile explicitly
masarray Sep 5, 2026
3e1141e
fix: keep opened SCL DataSet inventory authoritative across reconnect
masarray Sep 5, 2026
944e0e4
test: preserve SCL DataSet authority across fast reconnect
masarray Sep 5, 2026
7e1faac
test: update deterministic static RCB authority guards
masarray Sep 5, 2026
29fee6f
fix(static-dataset): pin receiver-first ARIEC report activation
masarray Sep 5, 2026
689925b
test(static-dataset): guard receiver-first engine pin
masarray Sep 5, 2026
8e620be
fix(static-dataset): choose non-occupied indexed RCB instance
masarray Sep 5, 2026
163ae04
fix(static): make SCL report controls authoritative
masarray Sep 5, 2026
2831ddb
test(static-dataset): guard non-occupied indexed RCB selection
masarray Sep 5, 2026
42477c0
chore(static-dataset): keep physical candidate on proven PR111 engine
masarray Sep 5, 2026
68cfc8d
test(static-dataset): keep engine lineage guard on PR111 candidate
masarray Sep 5, 2026
5bfddf9
fix(static): keep SCL RCB authority across all candidates
masarray Sep 5, 2026
e0a2edf
test(static): guard SCL authority and candidate evaluation
masarray Sep 5, 2026
1cfaa05
test(static): enforce design-first RCB authority
masarray Sep 5, 2026
c955d00
fix(static): include owner evidence in RCB selection
masarray Sep 5, 2026
9fd5d84
test(static): cover owner-aware RCB occupancy
masarray Sep 5, 2026
3ce8b3a
Fix static planner regression guard false positive
masarray Sep 5, 2026
64587c3
Tighten static planner no-first-RCB regression guard
masarray Sep 5, 2026
00a5dc8
Fix P0 regression guard comment
masarray Sep 5, 2026
be61b06
Pin ARSAS to PR111 semantic projection hardening
masarray Sep 5, 2026
aee3c34
Align TotPF regression with PR111 P1 engine pin
masarray Sep 5, 2026
26ee180
Stabilize FAT live grid scrolling on Build 1868 baseline
masarray Sep 5, 2026
1989b9c
Guard Build 1868 FAT schema while stabilizing live scroll
masarray Sep 5, 2026
b593e56
Harden Build 1868 FAT recovery regression gates
masarray Sep 5, 2026
2484aac
Fix FAT virtualization crash by applying mode before first measure
masarray Sep 5, 2026
a7c5881
Guard FAT virtualization against post-measure mode changes
masarray Sep 5, 2026
64fe513
Find FAT grid through logical tree before WPF layout
masarray Sep 5, 2026
1a34206
Restore golden FAT virtualization regression contract
masarray Sep 5, 2026
b66e552
Remove unsafe runtime FAT virtualization mutation
masarray Sep 5, 2026
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
10 changes: 5 additions & 5 deletions Services/Iec61850DataSetSignalInventoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ public static Iec61850DataSetSignalInventoryMergeResult EnsureMandatorySignals(
{
ArgumentNullException.ThrowIfNull(device);

// Signal Selection is also opened directly from an offline CID/SCD workspace.
// In that workflow LiveDiscoveryModel is intentionally null; the SCL design model
// is the authoritative inventory and must not be ignored. Prefer the live model
// only after a real association/discovery has produced one.
var authoritativeModel = device.LiveDiscoveryModel ?? device.SclWorkspace?.DesignModel;
// When an SCL workspace is open, its configured DataSet membership remains the
// engineering authority across cached/fast reconnects. A partial live discovery
// model must not erase FCDA/FCD rows that were proven by the opened CID/SCD. For an
// online-only IED with no SCL workspace, live discovery remains the authority.
var authoritativeModel = device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel;
if (authoritativeModel is null)
return EmptyResult();

Expand Down
144 changes: 97 additions & 47 deletions Services/Iec61850StaticDataSetAuthoritySelection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,76 +6,126 @@ namespace ArIED61850Tester.Services;
/// <summary>
/// Builds the exact Static DataSet selection used by the report-only workflow.
///
/// A DataSetReference on a browsed/runtime alias is not sufficient authority: several
/// Static report-only mode is RCB-backed by definition. A static DataSet that is not
/// referenced by an authoritative configured BRCB/URCB is valid engineering inventory,
/// but it is not a live acquisition source and therefore must not inflate the monitor with
/// permanently unavailable rows. When an SCL workspace is open, its ReportControl bindings
/// are the configuration authority; live discovery is verification only. For online-only
/// operation with no SCL workspace, live discovery becomes the configuration authority.
///
/// A DataSetReference on a browsed/runtime alias is also not sufficient authority: several
/// aliases can point at the same static FCDA/FCD member (for example cVal/instCVal or
/// structured measurement descendants). Static mode must select one presentation row
/// for each engine-authoritative DataSet membership, preserving the literal member
/// identity that appears in SCL/ARIEC.
/// structured measurement descendants). Static mode selects one presentation row for each
/// engine-authoritative membership, preserving the literal member identity from SCL/ARIEC.
/// </summary>
public static class Iec61850StaticDataSetAuthoritySelection
{
public static IReadOnlySet<SignalDefinition> Build(Iec61850MonitorDevice device)
{
ArgumentNullException.ThrowIfNull(device);

var model = device.LiveDiscoveryModel ?? device.SclWorkspace?.DesignModel;
if (model is null)
// Opened SCL is the engineering authority. A partial or richer live model may verify
// the configuration later, but it must not introduce extra static memberships that
// were never configured in the opened CID/SCD. Online-only mode falls back to live.
var authorityModel = device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel;
if (authorityModel is null)
return new HashSet<SignalDefinition>(ReferenceEqualityComparer.Instance);

var mandatory = Iec61850DataSetSignalInventoryProjection.GetMandatorySignals(model);
var reportBackedDataSets = BuildReportBackedDataSetReferences(device);
if (reportBackedDataSets.Count == 0)
return new HashSet<SignalDefinition>(ReferenceEqualityComparer.Instance);

var mandatory = Iec61850DataSetSignalInventoryProjection.GetMandatorySignals(authorityModel);
var selected = new HashSet<SignalDefinition>(ReferenceEqualityComparer.Instance);
var signals = device.Signals.ToArray();

foreach (var descriptor in mandatory)
{
var membership = descriptor.DataSetMemberships
// A descriptor may carry more than one membership. Do not arbitrarily take the
// first DataSet: choose only literal memberships that are backed by authoritative
// report-control configuration.
var memberships = descriptor.DataSetMemberships
.Where(item => reportBackedDataSets.Contains(NormalizeLiteral(item.DataSetReference)))
.OrderBy(item => item.DataSetReference, StringComparer.OrdinalIgnoreCase)
.ThenBy(item => item.MemberIndex)
.FirstOrDefault();
if (membership is null)
continue;

var memberReference = FirstNonEmpty(
membership.CanonicalMemberReference,
membership.OriginalMemberReference,
descriptor.DesignReference,
descriptor.ObservedReference,
descriptor.PrimaryValueReference);
if (string.IsNullOrWhiteSpace(memberReference) ||
string.IsNullOrWhiteSpace(membership.DataSetReference))
continue;

var candidates = signals
.Where(signal => !signal.IsControlSignal && signal.CanPublishToRuntime)
.Where(signal => LiteralEquals(signal.DataSetReference, membership.DataSetReference))
.Where(signal => LiteralEquals(signal.DisplayReference, memberReference))
.ToArray();
if (candidates.Length == 0)
continue;

// Prefer the runtime row already bound to ARIEC's resolved primary value.
// For an unresolved structured member, the inventory-created exact static row
// wins over a generic browsed alias. Never choose by fuzzy/prefix matching.
var chosen = candidates
.OrderByDescending(signal =>
!string.IsNullOrWhiteSpace(descriptor.PrimaryValueReference) &&
LiteralEquals(signal.ObjectReference, descriptor.PrimaryValueReference))
.ThenByDescending(signal =>
(signal.Source ?? string.Empty).Contains(
"mandatory static DataSet member",
StringComparison.OrdinalIgnoreCase))
.ThenByDescending(signal =>
string.Equals(signal.Category, "DataSet", StringComparison.OrdinalIgnoreCase))
.ThenByDescending(signal =>
string.Equals(signal.Confidence, "High", StringComparison.OrdinalIgnoreCase))
.First();

selected.Add(chosen);

foreach (var membership in memberships)
{
var memberReference = FirstNonEmpty(
membership.CanonicalMemberReference,
membership.OriginalMemberReference,
descriptor.DesignReference,
descriptor.ObservedReference,
descriptor.PrimaryValueReference);
if (string.IsNullOrWhiteSpace(memberReference) ||
string.IsNullOrWhiteSpace(membership.DataSetReference))
{
continue;
}

var candidates = signals
.Where(signal => !signal.IsControlSignal && signal.CanPublishToRuntime)
.Where(signal => LiteralEquals(signal.DataSetReference, membership.DataSetReference))
.Where(signal => LiteralEquals(signal.DisplayReference, memberReference))
.ToArray();
if (candidates.Length == 0)
continue;

// Prefer the runtime row already bound to ARIEC's resolved primary value.
// For an unresolved structured member, the inventory-created exact static row
// wins over a generic browsed alias. Never choose by fuzzy/prefix matching.
var chosen = candidates
.OrderByDescending(signal =>
!string.IsNullOrWhiteSpace(descriptor.PrimaryValueReference) &&
LiteralEquals(signal.ObjectReference, descriptor.PrimaryValueReference))
.ThenByDescending(signal =>
(signal.Source ?? string.Empty).Contains(
"mandatory static DataSet member",
StringComparison.OrdinalIgnoreCase))
.ThenByDescending(signal =>
string.Equals(signal.Category, "DataSet", StringComparison.OrdinalIgnoreCase))
.ThenByDescending(signal =>
string.Equals(signal.Confidence, "High", StringComparison.OrdinalIgnoreCase))
.First();

selected.Add(chosen);
}
}

return selected;
}

/// <summary>
/// Returns the literal DataSet references that have authoritative ReportControl backing.
/// Opened SCL configuration wins absolutely over extra live ReportControls. Live discovery
/// is used as configuration authority only when no SCL design model is open.
/// </summary>
public static IReadOnlySet<string> BuildReportBackedDataSetReferences(Iec61850MonitorDevice device)
{
ArgumentNullException.ThrowIfNull(device);

var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var configurationModel = device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel;
AddReportBackedDataSets(configurationModel, result);
return result;
}

private static void AddReportBackedDataSets(
LiveIedModelDiscoveryDocument? model,
HashSet<string> target)
{
if (model is null)
return;

foreach (var report in model.ReportControls)
{
var dataSetReference = NormalizeLiteral(report.DataSetReference);
if (!string.IsNullOrWhiteSpace(dataSetReference))
target.Add(dataSetReference);
}
}

private static bool LiteralEquals(string? left, string? right)
=> string.Equals(NormalizeLiteral(left), NormalizeLiteral(right), StringComparison.OrdinalIgnoreCase);

Expand Down
74 changes: 74 additions & 0 deletions Services/Iec61850StaticRcbReferenceMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace ArIED61850Tester.Services;

/// <summary>
/// Matches one configured SCL ReportControl declaration to the concrete RCB object exposed
/// by the live MMS server.
///
/// IEC 61850 SCL may describe one indexed ReportControl family while the server exposes
/// concrete instances such as Buffer01 / Buffer02. Exact identity always wins. The only
/// accepted fallback is a decimal indexed instance of the same literal RCB family; arbitrary
/// same-DataSet RCBs are never treated as substitutes.
/// </summary>
public static class Iec61850StaticRcbReferenceMatcher
{
public static bool IsExact(string? configuredReference, string? liveReference)
=> string.Equals(
Normalize(configuredReference),
Normalize(liveReference),
StringComparison.OrdinalIgnoreCase);

public static bool IsConfiguredOrIndexedInstance(string? configuredReference, string? liveReference)
{
var configured = Normalize(configuredReference);
var live = Normalize(liveReference);
if (string.IsNullOrWhiteSpace(configured) || string.IsNullOrWhiteSpace(live))
return false;
if (string.Equals(configured, live, StringComparison.OrdinalIgnoreCase))
return true;

SplitLeaf(configured, out var configuredParent, out var configuredLeaf);
SplitLeaf(live, out var liveParent, out var liveLeaf);
if (!string.Equals(configuredParent, liveParent, StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrWhiteSpace(configuredLeaf) ||
string.IsNullOrWhiteSpace(liveLeaf))
{
return false;
}

// If the configured declaration already ends in a digit, treat it as a concrete
// object and require exact identity. This avoids accidental prefix matches such as
// Buffer0 -> Buffer01.
if (char.IsDigit(configuredLeaf[^1]) ||
!liveLeaf.StartsWith(configuredLeaf, StringComparison.OrdinalIgnoreCase))
{
return false;
}

var suffix = liveLeaf[configuredLeaf.Length..];
return suffix.Length > 0 && suffix.All(char.IsDigit);
}

public static int MatchRank(string? configuredReference, string? liveReference)
{
if (IsExact(configuredReference, liveReference))
return 0;
return IsConfiguredOrIndexedInstance(configuredReference, liveReference) ? 1 : int.MaxValue;
}

public static string Normalize(string? reference)
=> (reference ?? string.Empty).Trim().Replace('$', '.');

private static void SplitLeaf(string reference, out string parent, out string leaf)
{
var separator = reference.LastIndexOf('.');
if (separator < 0)
{
parent = string.Empty;
leaf = reference;
return;
}

parent = reference[..separator];
leaf = reference[(separator + 1)..];
}
}
Loading
Loading