Skip to content
Closed
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
89 changes: 89 additions & 0 deletions MainWindow.DataSetCapabilityIndex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using AR.Iec61850.Discovery;
using ArIED61850Tester.Models;
using ArIED61850Tester.Services;

namespace ArIED61850Tester;

public partial class MainWindow
{
private readonly object _dataSetCapabilityBuildSync = new();
private readonly Dictionary<string, CancellationTokenSource> _dataSetCapabilityBuildTokens =
new(StringComparer.OrdinalIgnoreCase);

private void QueueDataSetCapabilityRefresh(Iec61850MonitorDevice device)
{
ArgumentNullException.ThrowIfNull(device);

var model = device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel;
if (model is null)
return;

var generation = device.ModelGeneration;
var source = device.SclWorkspace?.DesignModel is not null
? "OpenScl"
: "SmartDiscovery";

CancellationTokenSource cancellation;
lock (_dataSetCapabilityBuildSync)
{
if (_dataSetCapabilityBuildTokens.Remove(device.DeviceId, out var previous))
{
// The superseded worker owns disposal in its own finally block. Cancelling
// here is sufficient and avoids disposing a token source while that worker
// is still observing the token.
previous.Cancel();
}

cancellation = CancellationTokenSource.CreateLinkedTokenSource(_applicationCancellation.Token);
_dataSetCapabilityBuildTokens[device.DeviceId] = cancellation;
}

_ = BuildDataSetCapabilityIndexAsync(
device,
model,
generation,
source,
cancellation);
}

private async Task BuildDataSetCapabilityIndexAsync(
Iec61850MonitorDevice device,
LiveIedModelDiscoveryDocument model,
long generation,
string source,
CancellationTokenSource cancellation)
{
try
{
var index = await Iec61850DataSetCapabilityIndexBuilder.BuildAsync(
model,
generation,
source,
cancellation.Token);

cancellation.Token.ThrowIfCancellationRequested();

// Generation is the final authority. A late worker from an older SCL/discovery
// model is silently discarded instead of repainting capability state with stale
// DataSet/RCB information.
device.TryApplyDataSetCapabilityIndex(index);
}
catch (OperationCanceledException)
{
// Superseded model generations are expected during reconnect/reopen.
}
finally
{
lock (_dataSetCapabilityBuildSync)
{
if (_dataSetCapabilityBuildTokens.TryGetValue(device.DeviceId, out var current) &&
ReferenceEquals(current, cancellation))
{
_dataSetCapabilityBuildTokens.Remove(device.DeviceId);
}
}

cancellation.Dispose();
}
}
}
1 change: 1 addition & 0 deletions MainWindow.SclQuickActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ internal async Task OpenIedWorkspaceActionsAsync(Iec61850MonitorDevice device)
}

SelectedDevice = device;
QueueDataSetCapabilityRefresh(device);
var dialog = new SclSignalSelectionModeWindow(1, device)
{
Owner = this,
Expand Down
3 changes: 3 additions & 0 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ private void ApplySclWorkspaceToDevice(
device.RecountSelectedSignals();
device.RefreshComputed();
ScheduleGooseBindingRefreshFromWorkspace();
QueueDataSetCapabilityRefresh(device);
}

private static string BuildSclWorkspaceSummary(SclIedWorkspace workspace)
Expand Down Expand Up @@ -753,6 +754,7 @@ private async Task<bool> ConnectAndConfigureDeviceAsync(
var restoredCount = RestoreSignalSelection(device);
device.RefreshComputed();
RaiseWorkspaceCounts();
QueueDataSetCapabilityRefresh(device);
SetStatus($"{device.Name}: discovery complete, {device.SignalCount} readable signal(s), {restoredCount} saved selection(s) restored.");

// Let the card-local bar visibly settle at 100%, then release the card
Expand Down Expand Up @@ -866,6 +868,7 @@ await _runtime.ConnectUsingCachedModelAsync(
progress);

device.RecountSelectedSignals();
QueueDataSetCapabilityRefresh(device);
await WaitForDiscoveryProgressAnimationAsync(device, TimeSpan.FromMilliseconds(900));
RaiseWorkspaceCounts();
if (device.HasSclDesignModel)
Expand Down
69 changes: 69 additions & 0 deletions Models/Iec61850DataSetCapabilityModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace ArIED61850Tester.Models;

public sealed class Iec61850DataSetMemberCapability
{
public int Index { get; init; }
public string Reference { get; init; } = string.Empty;
public string FunctionalConstraint { get; init; } = string.Empty;
public string MmsReference { get; init; } = string.Empty;
}

public sealed class Iec61850ReportControlCapability
{
public string Reference { get; init; } = string.Empty;
public string DataSetReference { get; init; } = string.Empty;
public bool Buffered { get; init; }
public bool Indexed { get; init; }
public string ReportId { get; init; } = string.Empty;
public string ConfRev { get; init; } = string.Empty;
public string TriggerOptions { get; init; } = string.Empty;
public string OptionalFields { get; init; } = string.Empty;
public string BufferTimeMs { get; init; } = string.Empty;
public string IntegrityPeriodMs { get; init; } = string.Empty;
}

public sealed class Iec61850DataSetCapability
{
public string Reference { get; init; } = string.Empty;
public string Domain { get; init; } = string.Empty;
public string LogicalNode { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public bool? IsDeletable { get; init; }
public string Fingerprint { get; init; } = string.Empty;
public IReadOnlyList<Iec61850DataSetMemberCapability> Members { get; init; } =
Array.Empty<Iec61850DataSetMemberCapability>();
public IReadOnlyList<Iec61850ReportControlCapability> ReportControls { get; init; } =
Array.Empty<Iec61850ReportControlCapability>();

public int MemberCount => Members.Count;
public int StructurallyResolvedMemberCount => Members.Count(member =>
!string.IsNullOrWhiteSpace(member.Reference) &&
!string.IsNullOrWhiteSpace(member.FunctionalConstraint));
public bool HasConfiguredReportControl => ReportControls.Count > 0;
}

public sealed class Iec61850DataSetCapabilityIndex
{
public long Generation { get; init; }
public string Source { get; init; } = string.Empty;
public string IedName { get; init; } = string.Empty;
public string AccessPointName { get; init; } = string.Empty;
public string ModelFingerprint { get; init; } = string.Empty;
public string DataSetFingerprint { get; init; } = string.Empty;
public string ReportBindingFingerprint { get; init; } = string.Empty;
public IReadOnlyList<Iec61850DataSetCapability> DataSets { get; init; } =
Array.Empty<Iec61850DataSetCapability>();

public int DataSetCount => DataSets.Count;
public int MemberCount => DataSets.Sum(dataSet => dataSet.MemberCount);
public int ReportReadyDataSetCount => DataSets.Count(dataSet => dataSet.HasConfiguredReportControl);
public bool HasDataSets => DataSetCount > 0;
}

public sealed class Iec61850DataSetCapabilityParity
{
public bool DataSetsMatch { get; init; }
public bool ReportBindingsMatch { get; init; }
public bool IsEquivalent => DataSetsMatch && ReportBindingsMatch;
public string Summary { get; init; } = string.Empty;
}
43 changes: 43 additions & 0 deletions Models/MonitorModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public sealed class Iec61850MonitorDevice : ObservableObject
private string _sclSourceSha256 = string.Empty;
private string _sclIedName = string.Empty;
private string _sclAccessPointName = string.Empty;
private long _modelGeneration;
private Iec61850DataSetCapabilityIndex? _dataSetCapabilityIndex;

public string DeviceId { get; set; } = Guid.NewGuid().ToString("N");
public BulkObservableCollection<SignalDefinition> Signals { get; } = new();
Expand All @@ -54,6 +56,7 @@ public SclIedWorkspace? SclWorkspace
{
if (ReferenceEquals(_sclWorkspace, value)) return;
_sclWorkspace = value;
InvalidateDataSetCapabilityIndex();
RefreshAuthoritativeSclComparison();
RefreshComputed();
}
Expand All @@ -66,6 +69,7 @@ public LiveIedModelDiscoveryDocument? LiveDiscoveryModel
{
if (ReferenceEquals(_liveDiscoveryModel, value)) return;
_liveDiscoveryModel = value;
InvalidateDataSetCapabilityIndex();
RefreshAuthoritativeSclComparison();
RefreshComputed();
}
Expand Down Expand Up @@ -106,6 +110,45 @@ private void RefreshAuthoritativeSclComparison()
_sclComparison = comparison;
}

public long ModelGeneration => System.Threading.Interlocked.Read(ref _modelGeneration);

public Iec61850DataSetCapabilityIndex? DataSetCapabilityIndex
{
get => _dataSetCapabilityIndex;
private set
{
if (ReferenceEquals(_dataSetCapabilityIndex, value))
return;

_dataSetCapabilityIndex = value;
Raise(nameof(DataSetCapabilityIndex));
Raise(nameof(HasPreparedDataSetCapabilities));
Raise(nameof(PreparedDataSetCount));
Raise(nameof(PreparedReportReadyDataSetCount));
}
}

public bool HasPreparedDataSetCapabilities => DataSetCapabilityIndex is not null;
public int PreparedDataSetCount => DataSetCapabilityIndex?.DataSetCount ?? 0;
public int PreparedReportReadyDataSetCount => DataSetCapabilityIndex?.ReportReadyDataSetCount ?? 0;

public bool TryApplyDataSetCapabilityIndex(Iec61850DataSetCapabilityIndex index)
{
ArgumentNullException.ThrowIfNull(index);
if (index.Generation != ModelGeneration)
return false;

DataSetCapabilityIndex = index;
return true;
}

private void InvalidateDataSetCapabilityIndex()
{
System.Threading.Interlocked.Increment(ref _modelGeneration);
DataSetCapabilityIndex = null;
Raise(nameof(ModelGeneration));
}

public string SclSourcePath
{
get => _sclSourcePath;
Expand Down
5 changes: 3 additions & 2 deletions SclSignalSelectionModeWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ public SclSignalSelectionModeWindow(int iedCount, Iec61850MonitorDevice? targetD
else
{
var model = targetDevice.SclWorkspace?.DesignModel ?? targetDevice.LiveDiscoveryModel;
var dataSetCount = model?.DataSets.Count ?? 0;
var reportBackedCount =
var prepared = targetDevice.DataSetCapabilityIndex;
var dataSetCount = prepared?.DataSetCount ?? model?.DataSets.Count ?? 0;
var reportBackedCount = prepared?.ReportReadyDataSetCount ??
Iec61850StaticDataSetAuthoritySelection.BuildReportBackedDataSetReferences(targetDevice).Count;

CanUseStaticDataSet = dataSetCount > 0;
Expand Down
Loading
Loading