Skip to content
99 changes: 99 additions & 0 deletions MainWindow.SclLiveModelAuthority.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using ArIED61850Tester.Models;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

public partial class MainWindow
{
private bool _sclLiveModelAuthorityTrackingAttached;

static MainWindow()
{
EventManager.RegisterClassHandler(
typeof(MainWindow),
FrameworkElement.LoadedEvent,
new RoutedEventHandler(MainWindowAuthorityLoaded));
}

private static void MainWindowAuthorityLoaded(object sender, RoutedEventArgs e)
{
if (sender is MainWindow window)
window.AttachSclLiveModelAuthorityTracking();
}

private void AttachSclLiveModelAuthorityTracking()
{
if (_sclLiveModelAuthorityTrackingAttached)
return;

_sclLiveModelAuthorityTrackingAttached = true;
Devices.CollectionChanged += Devices_AuthorityCollectionChanged;
foreach (var device in Devices)
TrackAuthorityDevice(device);
}

private void Devices_AuthorityCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (var device in e.OldItems.OfType<Iec61850MonitorDevice>())
device.PropertyChanged -= AuthorityDevice_PropertyChanged;
}

if (e.NewItems != null)
{
foreach (var device in e.NewItems.OfType<Iec61850MonitorDevice>())
TrackAuthorityDevice(device);
}
}

private void TrackAuthorityDevice(Iec61850MonitorDevice device)
{
RegisterAuthorityModels(device);
device.PropertyChanged -= AuthorityDevice_PropertyChanged;
device.PropertyChanged += AuthorityDevice_PropertyChanged;
}

private static void AuthorityDevice_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is Iec61850MonitorDevice device)
RegisterAuthorityModels(device);
}

private static void RegisterAuthorityModels(Iec61850MonitorDevice device)
{
var workspace = device.SclWorkspace;
if (workspace != null)
{
SclLiveModelAuthorityRegistry.RegisterDesign(
workspace.IedName,
workspace.AccessPointName,
workspace.DesignModel);
}

var live = device.LiveDiscoveryModel;
if (live == null)
return;

RegisterLiveIdentity(device.Name, workspace?.AccessPointName, live);
RegisterLiveIdentity(live.IedName, workspace?.AccessPointName, live);
RegisterLiveIdentity(device.Name, device.SclAccessPointName, live);
RegisterLiveIdentity(live.IedName, device.SclAccessPointName, live);
RegisterLiveIdentity(device.Name, live.AccessPointName, live);
RegisterLiveIdentity(live.IedName, live.AccessPointName, live);
}

private static void RegisterLiveIdentity(
string? iedName,
string? accessPointName,
AR.Iec61850.Discovery.LiveIedModelDiscoveryDocument model)
{
if (string.IsNullOrWhiteSpace(iedName))
return;

SclLiveModelAuthorityRegistry.RegisterLive(iedName, accessPointName, model);
}
}
19 changes: 17 additions & 2 deletions Models/MonitorModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public SclIedWorkspace? SclWorkspace
{
if (ReferenceEquals(_sclWorkspace, value)) return;
_sclWorkspace = value;
RefreshAuthoritativeSclComparison();
RefreshComputed();
}
}
Expand All @@ -64,6 +65,7 @@ public LiveIedModelDiscoveryDocument? LiveDiscoveryModel
{
if (ReferenceEquals(_liveDiscoveryModel, value)) return;
_liveDiscoveryModel = value;
RefreshAuthoritativeSclComparison();
RefreshComputed();
}
}
Expand All @@ -73,12 +75,25 @@ public SclLiveModelComparisonResult? SclComparison
get => _sclComparison;
set
{
if (ReferenceEquals(_sclComparison, value)) return;
_sclComparison = value;
var effective = BuildAuthoritativeSclComparison() ?? value;
if (ReferenceEquals(_sclComparison, effective)) return;
_sclComparison = effective;
RefreshComputed();
}
}

private SclLiveModelComparisonResult? BuildAuthoritativeSclComparison()
=> _sclWorkspace != null && _liveDiscoveryModel != null
? SclLiveModelComparer.Compare(_sclWorkspace.DesignModel, _liveDiscoveryModel)
: null;

private void RefreshAuthoritativeSclComparison()
{
var comparison = BuildAuthoritativeSclComparison();
if (comparison != null)
_sclComparison = comparison;
}

public string SclSourcePath
{
get => _sclSourcePath;
Expand Down
37 changes: 37 additions & 0 deletions SclLiveSignalModelProjection.Authority.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using AR.Iec61850.Discovery;
using ArIED61850Tester.Models;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

/// <summary>
/// MainWindow compatibility shim. Engine-owned design/live models are authoritative;
/// SignalDefinition projection is used only for legacy/cached rows without engine model provenance.
/// </summary>
internal static class SclLiveSignalModelProjection
{
public static LiveIedModelDiscoveryDocument Build(
string iedName,
string accessPointName,
IReadOnlyList<SignalDefinition> signals)
{
ArgumentNullException.ThrowIfNull(signals);

var isDesignRows = signals.Any(signal =>
signal.Source.Equals("SCL design model", StringComparison.OrdinalIgnoreCase));

if (isDesignRows &&
SclLiveModelAuthorityRegistry.TryGetDesign(iedName, accessPointName, out var designModel))
{
return designModel;
}

if (!isDesignRows &&
SclLiveModelAuthorityRegistry.TryGetLive(iedName, accessPointName, out var liveModel))
{
return liveModel;
}

return Services.SclLiveSignalModelProjection.Build(iedName, accessPointName, signals);
}
}
53 changes: 53 additions & 0 deletions Services/SclLiveModelAuthorityRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Collections.Concurrent;
using AR.Iec61850.Discovery;

namespace ArIED61850Tester.Services;

/// <summary>
/// Application provenance registry for engine-owned design/live models.
/// It performs no IEC 61850 reference, CDC, FC, DataSet, or vendor interpretation.
/// </summary>
public static class SclLiveModelAuthorityRegistry
{
private static readonly ConcurrentDictionary<string, WeakReference<LiveIedModelDiscoveryDocument>> DesignModels
= new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, WeakReference<LiveIedModelDiscoveryDocument>> LiveModels
= new(StringComparer.OrdinalIgnoreCase);

public static void RegisterDesign(string? iedName, string? accessPointName, LiveIedModelDiscoveryDocument model)
{
ArgumentNullException.ThrowIfNull(model);
DesignModels[Key(iedName, accessPointName)] = new(model);
}

public static void RegisterLive(string? iedName, string? accessPointName, LiveIedModelDiscoveryDocument model)
{
ArgumentNullException.ThrowIfNull(model);
LiveModels[Key(iedName, accessPointName)] = new(model);
}

public static bool TryGetDesign(string? iedName, string? accessPointName, out LiveIedModelDiscoveryDocument model)
=> TryGet(DesignModels, Key(iedName, accessPointName), out model);

public static bool TryGetLive(string? iedName, string? accessPointName, out LiveIedModelDiscoveryDocument model)
=> TryGet(LiveModels, Key(iedName, accessPointName), out model);

private static bool TryGet(
ConcurrentDictionary<string, WeakReference<LiveIedModelDiscoveryDocument>> registry,
string key,
out LiveIedModelDiscoveryDocument model)
{
model = null!;
if (!registry.TryGetValue(key, out var weak))
return false;
if (weak.TryGetTarget(out model!))
return true;

registry.TryRemove(key, out _);
model = null!;
return false;
}

private static string Key(string? iedName, string? accessPointName)
=> $"{(iedName ?? string.Empty).Trim()}|{(accessPointName ?? string.Empty).Trim()}";
}
68 changes: 37 additions & 31 deletions Services/SclWorkspaceSignalMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace ArIED61850Tester.Services;

/// <summary>
/// Presentation adapter from the engine-owned SCL workspace model to ArIED signal rows.
/// This class deliberately contains no XML parsing or IEC 61850 type-template traversal.
/// This class deliberately contains no XML parsing, IEC 61850 type-template traversal,
/// FCD expansion, or CDC value-selection semantics.
/// </summary>
public static class SclWorkspaceSignalMapper
{
Expand All @@ -19,7 +20,11 @@ public static IReadOnlyList<SignalDefinition> BuildSignals(SclIedWorkspace works
{
ArgumentNullException.ThrowIfNull(workspace);

var dataSetBindings = BuildDataSetBindings(workspace.DesignModel);
// The engine is authoritative for FCD/FCDA -> DataAttribute semantics.
// ARSAS only projects the engine result into operator-facing rows.
var semanticBindings = Iec61850DataSetSemanticBindingResolver.Resolve(workspace.DesignModel);
var dataSetBindings = BuildDataSetBindings(semanticBindings);
var staticPrimaryReferences = BuildStaticPrimaryReferences(semanticBindings);
var reportBindings = BuildReportBindings(workspace.DesignModel);
var signals = new List<SignalDefinition>();

Expand All @@ -36,8 +41,9 @@ public static IReadOnlyList<SignalDefinition> BuildSignals(SclIedWorkspace works
}

return signals
.Where(signal => SasOperationalSignalPolicy.IsVisible(signal))
.GroupBy(signal => NormalizeReference(signal.ObjectReference), StringComparer.OrdinalIgnoreCase)
.Where(signal => SasOperationalSignalPolicy.IsVisible(signal) ||
staticPrimaryReferences.Contains(signal.ObjectReference))
.GroupBy(signal => NormalizePresentationReference(signal.ObjectReference), StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.OrderBy(signal => signal.SortPriority)
.ThenBy(signal => signal.LogicalNode, StringComparer.OrdinalIgnoreCase)
Expand All @@ -64,9 +70,8 @@ private static void AddRuntimeSignals(
continue;

var reference = attribute.ObjectReference;
var normalized = NormalizeReference(reference);
dataSetBindings.TryGetValue(normalized, out var dataSetReference);
reportBindings.TryGetValue(NormalizeReference(dataSetReference), out var reportReference);
dataSetBindings.TryGetValue(reference, out var dataSetReference);
reportBindings.TryGetValue(dataSetReference ?? string.Empty, out var reportReference);
var category = ResolveCategory(logicalNode.LnClass, dataObject.Name, dataObject.InferredCdc, fc);

signals.Add(new SignalDefinition
Expand Down Expand Up @@ -120,9 +125,8 @@ private static void AddControlSignal(
Leaf(attribute.AttributePath).Equals("ctlModel", StringComparison.OrdinalIgnoreCase));
var status = dataObject.Attributes.FirstOrDefault(attribute =>
Leaf(attribute.AttributePath).Equals("stVal", StringComparison.OrdinalIgnoreCase));
var normalizedStatus = NormalizeReference(status?.ObjectReference);
dataSetBindings.TryGetValue(normalizedStatus, out var dataSetReference);
reportBindings.TryGetValue(NormalizeReference(dataSetReference), out var reportReference);
dataSetBindings.TryGetValue(status?.ObjectReference ?? string.Empty, out var dataSetReference);
reportBindings.TryGetValue(dataSetReference ?? string.Empty, out var reportReference);

signals.Add(new SignalDefinition
{
Expand Down Expand Up @@ -153,29 +157,37 @@ private static void AddControlSignal(
});
}

private static Dictionary<string, string> BuildDataSetBindings(LiveIedModelDiscoveryDocument model)
private static Dictionary<string, string> BuildDataSetBindings(LiveIedDataSetSemanticBindingDocument semanticBindings)
{
var bindings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var dataSet in model.DataSets)
foreach (var member in semanticBindings.Members)
{
foreach (var member in dataSet.Members)
if (!member.IsResolved)
continue;

foreach (var attribute in member.ResolvedAttributes)
{
var key = NormalizeReference(member.Reference);
if (!string.IsNullOrWhiteSpace(key))
bindings.TryAdd(key, dataSet.Reference);
if (!string.IsNullOrWhiteSpace(attribute.Reference))
bindings.TryAdd(attribute.Reference, member.DataSetReference);
}
}

return bindings;
}

private static HashSet<string> BuildStaticPrimaryReferences(LiveIedDataSetSemanticBindingDocument semanticBindings)
=> semanticBindings.Members
.Select(member => member.PrimaryValueReference)
.Where(reference => !string.IsNullOrWhiteSpace(reference))
.ToHashSet(StringComparer.OrdinalIgnoreCase);

private static Dictionary<string, string> BuildReportBindings(LiveIedModelDiscoveryDocument model)
{
var bindings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var report in model.ReportControls)
{
var key = NormalizeReference(report.DataSetReference);
if (!string.IsNullOrWhiteSpace(key))
bindings.TryAdd(key, report.Reference);
if (!string.IsNullOrWhiteSpace(report.DataSetReference))
bindings.TryAdd(report.DataSetReference, report.Reference);
}
return bindings;
}
Expand Down Expand Up @@ -243,19 +255,13 @@ private static string Leaf(string? path)
return index >= 0 ? text[(index + 1)..] : text;
}

private static string NormalizeReference(string? reference)
/// <summary>
/// Presentation-only dedup normalization. It must not be used to infer
/// DataSet membership or IEC 61850 semantic targets; the engine owns that.
/// </summary>
private static string NormalizePresentationReference(string? reference)
{
var text = (reference ?? string.Empty).Trim();
var fcMarker = text.LastIndexOf(" [", StringComparison.Ordinal);
if (fcMarker >= 0)
text = text[..fcMarker];
text = text.Replace('$', '.').Replace("//", "/", StringComparison.Ordinal).Trim();

// SCL FCDA display references commonly use IED/LD/LN while MMS domains use IEDLD/LN.
var parts = text.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length >= 3)
text = string.Concat(parts[0], parts[1], "/", string.Join("/", parts.Skip(2)));

var text = (reference ?? string.Empty).Trim().Replace('$', '.').Replace("//", "/", StringComparison.Ordinal);
return text.ToUpperInvariant();
}
}
6 changes: 3 additions & 3 deletions engines/ARIEC61850.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"repository": "masarray/ARIEC61850",
"ref": "main",
"commit": "8b46cfed0614a4d0b0a5bf73fa7a6e77cf6fe817",
"sourcePullRequest": 55,
"purpose": "Immutable ARIEC61850 revision for ARSAS CI, tests, packaging, diagnostics, and release provenance."
"commit": "d041a1e05c2082f966b1e06a977b2f67261fea02",
"sourcePullRequest": 60,
"purpose": "Immutable ARIEC61850 revision for ARSAS CI, tests, packaging, diagnostics, and release provenance. Production baseline includes engine-owned DataSet semantic binding plus design/live reconciliation and exact targeted MMS probe APIs."
}
Loading
Loading