-
Notifications
You must be signed in to change notification settings - Fork 5
Restore all mandatory IED DataSet signals in selection wizard #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c02158b
ff3705c
94aad92
ca4eae7
e2e0877
474a5f0
65510f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| using ArIED61850Tester.Models; | ||
| using ArIED61850Tester.Services; | ||
|
|
||
| namespace ArIED61850Tester; | ||
|
|
||
| public partial class MainWindow | ||
| { | ||
| internal void RegisterRecoveredDataSetSignals( | ||
| Iec61850MonitorDevice device, | ||
| Iec61850DataSetSignalInventoryMergeResult merge) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(device); | ||
| ArgumentNullException.ThrowIfNull(merge); | ||
|
|
||
| foreach (var signal in merge.AddedSignals) | ||
| { | ||
| // The wizard can recover a row after the normal discovery collection has | ||
| // already been registered with MainWindow. Bring that row under the same | ||
| // application lifecycle without changing its user-selection state. | ||
| signal.PropertyChanged -= Signal_PropertyChanged; | ||
| signal.PropertyChanged += Signal_PropertyChanged; | ||
| _signalOwners[signal] = device; | ||
| } | ||
|
|
||
| if (merge.AddedCount == 0 && merge.EnrichedExistingCount == 0) | ||
| return; | ||
|
|
||
| device.RecountSelectedSignals(); | ||
| device.RefreshComputed(); | ||
| RaiseWorkspaceCounts(); | ||
|
|
||
| if (merge.AddedCount > 0) | ||
| { | ||
| AddLog( | ||
| "INFO", | ||
| device.Name, | ||
| $"ARIEC DataSet authority restored {merge.AddedCount} mandatory primary signal(s) to the selection inventory; user selection was not changed."); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| using AR.Iec61850.Discovery; | ||
| using ArIED61850Tester.Models; | ||
|
|
||
| namespace ArIED61850Tester.Services; | ||
|
|
||
| public sealed record Iec61850DataSetSignalInventoryMergeResult( | ||
| IReadOnlyList<SignalDefinition> AddedSignals, | ||
| int EnrichedExistingCount, | ||
| int MandatoryCatalogCount) | ||
| { | ||
| public int AddedCount => AddedSignals.Count; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Application-side projection of ARIEC-owned DataSet signal authority. | ||
| /// | ||
| /// ARIEC decides which IEC 61850 signals are mandatory primary DataSet members. | ||
| /// ARSAS only guarantees that those already-classified engine signals are present in | ||
| /// the user-visible signal inventory. No IEC reference guessing, fuzzy matching, or | ||
| /// DataSet semantic inference is performed here. | ||
| /// </summary> | ||
| public static class Iec61850DataSetSignalInventoryService | ||
| { | ||
| public static Iec61850DataSetSignalInventoryMergeResult EnsureMandatorySignals( | ||
| Iec61850MonitorDevice device) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(device); | ||
|
|
||
| if (device.LiveDiscoveryModel is null) | ||
| return new Iec61850DataSetSignalInventoryMergeResult(Array.Empty<SignalDefinition>(), 0, 0); | ||
|
|
||
| var catalog = Iec61850SignalCatalogBuilder.Build(device.LiveDiscoveryModel); | ||
| var mandatory = catalog.GetMandatoryPrimarySignals(); | ||
| if (mandatory.Count == 0) | ||
| return new Iec61850DataSetSignalInventoryMergeResult(Array.Empty<SignalDefinition>(), 0, 0); | ||
|
|
||
| // Keep application matching literal. The engine owns IEC 61850 reference | ||
| // canonicalization; ARSAS only compares the reference forms that the engine has | ||
| // already exposed on the descriptor. | ||
| var existing = device.Signals | ||
| .Where(signal => !string.IsNullOrWhiteSpace(signal.ObjectReference)) | ||
| .GroupBy(signal => LiteralReference(signal.ObjectReference), StringComparer.OrdinalIgnoreCase) | ||
| .ToDictionary(group => group.Key, group => group.First(), StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| var added = new List<SignalDefinition>(); | ||
| var enriched = 0; | ||
|
|
||
| foreach (var descriptor in mandatory) | ||
| { | ||
| var engineReferences = EngineReferenceCandidates(descriptor).ToArray(); | ||
| var current = engineReferences | ||
| .Select(reference => existing.TryGetValue(reference, out var signal) ? signal : null) | ||
| .FirstOrDefault(signal => signal is not null); | ||
|
|
||
| if (current is not null) | ||
| { | ||
| if (ApplyEngineDataSetAuthority(current, descriptor)) | ||
| enriched++; | ||
| continue; | ||
| } | ||
|
|
||
| var reference = FirstNonEmpty( | ||
| descriptor.PrimaryValueReference, | ||
| descriptor.DesignReference, | ||
| descriptor.ObservedReference); | ||
| if (string.IsNullOrWhiteSpace(reference)) | ||
| continue; | ||
|
|
||
| var signal = CreateSignal(descriptor, reference); | ||
| device.Signals.Add(signal); | ||
| foreach (var key in EngineReferenceCandidates(descriptor).Append(LiteralReference(reference))) | ||
| existing.TryAdd(key, signal); | ||
| added.Add(signal); | ||
| } | ||
|
|
||
| return new Iec61850DataSetSignalInventoryMergeResult(added, enriched, mandatory.Count); | ||
| } | ||
|
|
||
| private static SignalDefinition CreateSignal( | ||
| Iec61850SignalDescriptor descriptor, | ||
| string reference) | ||
| { | ||
| var primaryMembership = FirstMembership(descriptor); | ||
| var report = descriptor.ReportMemberships.FirstOrDefault(); | ||
| var dataType = FirstNonEmpty(descriptor.MmsType, descriptor.SclBType, "Unknown"); | ||
| return new SignalDefinition | ||
| { | ||
| Name = FirstNonEmpty(descriptor.DataObject, descriptor.DataAttributePath, reference), | ||
| ObjectReference = reference, | ||
| FunctionalConstraint = descriptor.FunctionalConstraint, | ||
| DataType = dataType, | ||
| Category = "DataSet", | ||
| Confidence = "High", | ||
| DataSetReference = primaryMembership?.DataSetReference ?? string.Empty, | ||
| ReportControlReference = report?.ReportControlReference ?? string.Empty, | ||
| QualityReference = descriptor.QualityReference, | ||
| TimestampReference = descriptor.TimestampReference, | ||
| Source = "ARIEC61850 signal catalog • mandatory static DataSet member", | ||
| IsSelected = false, | ||
| IsReportCapable = true, | ||
| ReportCoverage = report is null | ||
| ? "Static DataSet member • MMS polling fallback" | ||
| : "Static report/DataSet • polling fallback", | ||
| ReportCoverageReason = BuildCoverageReason(descriptor), | ||
| ProbeStatus = "Not probed", | ||
| Value = "-", | ||
| Quality = "Unknown", | ||
| DeviceTimestamp = "-" | ||
| }; | ||
| } | ||
|
|
||
| private static bool ApplyEngineDataSetAuthority( | ||
| SignalDefinition signal, | ||
| Iec61850SignalDescriptor descriptor) | ||
| { | ||
| var changed = false; | ||
| var membership = FirstMembership(descriptor); | ||
| var report = descriptor.ReportMemberships.FirstOrDefault(); | ||
|
|
||
| if (membership is not null && | ||
| !string.Equals(signal.DataSetReference, membership.DataSetReference, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| signal.DataSetReference = membership.DataSetReference; | ||
| changed = true; | ||
| } | ||
|
|
||
| if (report is not null && string.IsNullOrWhiteSpace(signal.ReportControlReference)) | ||
| { | ||
| signal.ReportControlReference = report.ReportControlReference; | ||
| changed = true; | ||
| } | ||
|
|
||
| if (!signal.IsReportCapable) | ||
| { | ||
| signal.IsReportCapable = true; | ||
| changed = true; | ||
| } | ||
|
|
||
| if ((string.IsNullOrWhiteSpace(signal.ReportCoverage) || | ||
| signal.ReportCoverage.Equals("Polling fallback", StringComparison.OrdinalIgnoreCase)) && | ||
| (membership is not null || report is not null)) | ||
| { | ||
| signal.ReportCoverage = report is null | ||
| ? "Static DataSet member • MMS polling fallback" | ||
| : "Static report/DataSet • polling fallback"; | ||
| changed = true; | ||
| } | ||
|
|
||
| var reason = BuildCoverageReason(descriptor); | ||
| if (!string.Equals(signal.ReportCoverageReason, reason, StringComparison.Ordinal)) | ||
| { | ||
| signal.ReportCoverageReason = reason; | ||
| changed = true; | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(signal.QualityReference) && !string.IsNullOrWhiteSpace(descriptor.QualityReference)) | ||
| { | ||
| signal.QualityReference = descriptor.QualityReference; | ||
| changed = true; | ||
| } | ||
|
|
||
| if (string.IsNullOrWhiteSpace(signal.TimestampReference) && !string.IsNullOrWhiteSpace(descriptor.TimestampReference)) | ||
| { | ||
| signal.TimestampReference = descriptor.TimestampReference; | ||
| changed = true; | ||
| } | ||
|
|
||
| return changed; | ||
| } | ||
|
|
||
| private static IEnumerable<string> EngineReferenceCandidates(Iec61850SignalDescriptor descriptor) | ||
| { | ||
| var values = new[] | ||
| { | ||
| descriptor.PrimaryValueReference, | ||
| descriptor.DesignReference, | ||
| descriptor.ObservedReference, | ||
| descriptor.PrimaryValueMmsReference, | ||
| descriptor.CanonicalMmsReference, | ||
| descriptor.EffectiveMmsReference, | ||
| descriptor.ObservedMmsReference | ||
| }; | ||
|
|
||
| return values | ||
| .Where(value => !string.IsNullOrWhiteSpace(value)) | ||
| .Select(LiteralReference) | ||
| .Distinct(StringComparer.OrdinalIgnoreCase); | ||
| } | ||
|
|
||
| private static Iec61850SignalDataSetMembership? FirstMembership(Iec61850SignalDescriptor descriptor) | ||
| => descriptor.DataSetMemberships | ||
| .OrderBy(membership => membership.DataSetReference, StringComparer.OrdinalIgnoreCase) | ||
| .ThenBy(membership => membership.MemberIndex) | ||
| .FirstOrDefault(); | ||
|
|
||
| private static string BuildCoverageReason(Iec61850SignalDescriptor descriptor) | ||
| { | ||
| var memberships = descriptor.DataSetMemberships | ||
| .OrderBy(membership => membership.DataSetReference, StringComparer.OrdinalIgnoreCase) | ||
| .ThenBy(membership => membership.MemberIndex) | ||
| .Select(membership => $"{membership.DataSetReference}[{membership.MemberIndex}]") | ||
| .Distinct(StringComparer.OrdinalIgnoreCase) | ||
| .ToArray(); | ||
| var membershipText = memberships.Length == 0 | ||
| ? "static DataSet membership" | ||
| : string.Join(", ", memberships); | ||
|
|
||
| return $"ARIEC61850 mandatory primary DataSet signal: {membershipText}. " + | ||
| "Inventory presence is engine-authoritative; user selection remains independent."; | ||
| } | ||
|
|
||
| private static string LiteralReference(string? reference) | ||
| => (reference ?? string.Empty).Trim(); | ||
|
|
||
| private static string FirstNonEmpty(params string?[] values) | ||
| => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| using System.Windows; | ||
| using ArIED61850Tester.Models; | ||
| using ArIED61850Tester.Services; | ||
|
|
||
| namespace ArIED61850Tester; | ||
|
|
||
| public partial class SignalSelectionWizardWindow | ||
| { | ||
| protected override void OnInitialized(EventArgs e) | ||
| { | ||
| var merge = Iec61850DataSetSignalInventoryService.EnsureMandatorySignals(_device); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Running recovery only from Useful? React with 👍 / 👎. |
||
| foreach (var signal in merge.AddedSignals) | ||
| { | ||
| signal.DisplayReference = Iec61850MonitorPoint.StripIedNamePrefix(signal.ObjectReference, _device.Name); | ||
| signal.PropertyChanged += Signal_PropertyChanged; | ||
| } | ||
|
|
||
| // Window initialization can run inside InitializeComponent(), before the caller's | ||
| // object initializer assigns Owner. Register recovered rows through the actual | ||
| // application MainWindow so they receive the same owner/property-change lifecycle | ||
| // as rows produced by the normal discovery pipeline. | ||
| if (Application.Current?.MainWindow is MainWindow mainWindow) | ||
| mainWindow.RegisterRecoveredDataSetSignals(_device, merge); | ||
| else | ||
| { | ||
| _device.RecountSelectedSignals(); | ||
| _device.RefreshComputed(); | ||
| } | ||
|
|
||
| base.OnInitialized(e); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a signal belongs to multiple DataSets, this unconditionally replaces an existing
DataSetReferencewith the alphabetically first membership, whileApplyEngineDataSetAuthoritypreserves any nonemptyReportControlReference. A previously valid DataSet/RCB pair can therefore become an RCB for one DataSet paired with another DataSet, and the report planner will build a static plan from that mismatched pair. Select a membership associated with the chosen RCB, or retain an existing authoritative pair when both references are already populated.Useful? React with 👍 / 👎.