Skip to content
Draft
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
24 changes: 18 additions & 6 deletions MainWindow.IedBrowser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -584,17 +584,19 @@ private void RefreshIedBrowserReadButtons()
}

private bool TryGetBrowserReadFailure(
Iec61850MonitorDevice device, IedBrowserDetailRow row, out string reason)
Iec61850MonitorDevice device, IedBrowserDetailRow row, out string reason,
out IedBrowserReadAbsenceKind kind)
{
if (_iedBrowserReadFailureEvidence?.TryGet(
device, row.ValueReference, row.FallbackValueReference,
row.FunctionalConstraint, out reason) == true)
row.FunctionalConstraint, out reason, out kind) == true)
return true;
if (device.InitialHydrationFailures?.TryGet(
device, row.ValueReference, row.FallbackValueReference,
row.FunctionalConstraint, out reason) == true)
row.FunctionalConstraint, out reason, out kind) == true)
return true;
reason = "";
kind = IedBrowserReadAbsenceKind.Failed;
return false;
}

Expand Down Expand Up @@ -681,10 +683,20 @@ private void RefreshIedBrowserVisibleValues()
row.ObservationSource = "Discovery snapshot";
row.ObservationTooltip = provenance;
}
else if (TryGetBrowserReadFailure(device, row, out var readFailure))
else if (TryGetBrowserReadFailure(device, row,
out var readFailure, out var absenceKind))
{
row.Value = "Unavailable";
row.ObservationSource = "MMS Read failed";
// No initial wire attempt is not an MMS rejection.
// Keep the small table calm; explain the cause in the
// existing per-row tooltip, without fake process values.
var deferred = absenceKind == IedBrowserReadAbsenceKind.Deferred;
row.Value = deferred ? "Not read" : "Unavailable";
row.ObservationSource = absenceKind switch
{
IedBrowserReadAbsenceKind.Deferred => "Initial read deferred",
IedBrowserReadAbsenceKind.Interrupted => "MMS Read interrupted",
_ => "MMS Read failed"
};
row.ObservationTooltip =
$"{row.ValueReference} [{row.FunctionalConstraint}] · {readFailure}";
}
Expand Down
66 changes: 57 additions & 9 deletions Models/IedBrowserReadFailureEvidence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@
namespace ArIED61850Tester.Models;

/// <summary>
/// Failure-only evidence for one explicit Browser MMS Read sweep.
/// Bounded absence-only evidence for one association-scoped Browser MMS Read sweep.
/// This is NOT another values cache and cannot authorize a value, FC,
/// conversion, retry or live state. Exact reference/FC, same IED object,
/// association and projection epoch are required on every UI lookup.
/// </summary>
public enum IedBrowserReadAbsenceKind
{
Failed,
Interrupted,
Deferred
}

public sealed record IedBrowserReadFailureSample(
string Reference, string FunctionalConstraint, string Reason);
string Reference, string FunctionalConstraint, string Reason,
IedBrowserReadAbsenceKind Kind = IedBrowserReadAbsenceKind.Failed);

internal sealed record IedBrowserReadAbsence(
string Reason, IedBrowserReadAbsenceKind Kind);

public sealed class IedBrowserReadFailureEvidence
{
Expand All @@ -21,7 +32,7 @@ public sealed class IedBrowserReadFailureEvidence
private readonly bool _modelBound;
private readonly object? _model;
private readonly object _gate = new();
private readonly Dictionary<string, string> _failed = new(StringComparer.Ordinal);
private readonly Dictionary<string, IedBrowserReadAbsence> _failed = new(StringComparer.Ordinal);

public IedBrowserReadFailureEvidence(Iec61850MonitorDevice owner,
bool bindToLiveModel = false)
Expand All @@ -46,6 +57,21 @@ private static string Key(string? reference, string? fc)
(fc ?? "").Trim().ToUpperInvariant();

public void RecordFailure(IedBrowserReadTarget target, string reason)
=> RecordAbsence(target, reason, IedBrowserReadAbsenceKind.Failed);

/// <summary>
/// A selected bounded initial target that never reached an MMS Read is
/// deferred, not failed. It does not authorize a value or retry.
/// </summary>
public void RecordDeferred(IedBrowserReadTarget target, string reason)
=> RecordAbsence(target, reason, IedBrowserReadAbsenceKind.Deferred);

/// <summary>A Read was started, but its terminal wire result is unknown.</summary>
public void RecordInterrupted(IedBrowserReadTarget target, string reason)
=> RecordAbsence(target, reason, IedBrowserReadAbsenceKind.Interrupted);

private void RecordAbsence(IedBrowserReadTarget target, string reason,
IedBrowserReadAbsenceKind kind)
{
ArgumentNullException.ThrowIfNull(target);
if (!IsCurrent(_owner) || string.IsNullOrWhiteSpace(reason) ||
Expand All @@ -58,8 +84,16 @@ public void RecordFailure(IedBrowserReadTarget target, string reason)
{
if (_failed.Count >= MaxFailureEntries && !_failed.ContainsKey(key))
return;
if (IsCurrent(_owner))
_failed[key] = text.Length > 320 ? text[..320] + "…" : text;
if (!IsCurrent(_owner)) return;
// A budget-deferred marker cannot erase an actual wire failure.
if (_failed.TryGetValue(key, out var previous) &&
(kind == IedBrowserReadAbsenceKind.Deferred &&
previous.Kind != IedBrowserReadAbsenceKind.Deferred ||
kind == IedBrowserReadAbsenceKind.Interrupted &&
previous.Kind == IedBrowserReadAbsenceKind.Failed))
return;
_failed[key] = new IedBrowserReadAbsence(
text.Length > 320 ? text[..320] + "…" : text, kind);
}
}

Expand Down Expand Up @@ -91,16 +125,22 @@ public IReadOnlyList<IedBrowserReadFailureSample> SnapshotCurrent(
var separator = item.Key.LastIndexOf('|');
return new IedBrowserReadFailureSample(
item.Key[..separator], item.Key[(separator + 1)..],
item.Value);
item.Value.Reason, item.Value.Kind);
})
.ToArray();
}
}

public bool TryGet(Iec61850MonitorDevice device,
string reference, string fallbackReference, string fc, out string reason)
=> TryGet(device, reference, fallbackReference, fc, out reason, out _);

public bool TryGet(Iec61850MonitorDevice device,
string reference, string fallbackReference, string fc, out string reason,
out IedBrowserReadAbsenceKind kind)
{
reason = "";
kind = IedBrowserReadAbsenceKind.Failed;
if (!IsCurrent(device) || string.IsNullOrWhiteSpace(fc))
return false;
// A fallback is permitted only when structured projection declared it
Expand All @@ -109,10 +149,18 @@ public bool TryGet(Iec61850MonitorDevice device,
{
if (!IsCurrent(device)) return false;
if (!string.IsNullOrWhiteSpace(reference) &&
_failed.TryGetValue(Key(reference, fc), out reason))
_failed.TryGetValue(Key(reference, fc), out var exact))
{
reason = exact.Reason;
kind = exact.Kind;
return true;
return !string.IsNullOrWhiteSpace(fallbackReference) &&
_failed.TryGetValue(Key(fallbackReference, fc), out reason);
}
if (string.IsNullOrWhiteSpace(fallbackReference) ||
!_failed.TryGetValue(Key(fallbackReference, fc), out var fallback))
return false;
reason = fallback.Reason;
kind = fallback.Kind;
return true;
}
}
}
82 changes: 76 additions & 6 deletions Models/IedBrowserReadPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ public static IReadOnlyList<IedBrowserReadTarget> BuildInitialPriority(
// Six values share the SAME existing 32-target/10-second envelope,
// not an extra background poll or MMS association.
const int initialAnalogSeedLimit = 6;
var analog = verifiedLiveModel.LogicalDevices
var analogSeeds = verifiedLiveModel.LogicalDevices
.SelectMany(ld => ld.LogicalNodes)
.SelectMany(ln => ln.DataObjects)
.Where(obj =>
Expand All @@ -234,8 +234,6 @@ public static IReadOnlyList<IedBrowserReadTarget> BuildInitialPriority(
+ "|MX", StringComparer.Ordinal)
.Where(group => group.Count() == 1)
.Select(group => group.First())
.Where(attr => !ledger.TryGet(attr.MmsReference, "MX", out var observed) ||
observed.IsStale || observed.Association != association)
// Fill a small coherent phasor group per owner rather than
// selecting only phase-A from dozens of unrelated MMXU nodes.
.OrderBy(attr => IedBrowserLiveValues.CanonicalReference(
Expand All @@ -244,9 +242,53 @@ public static IReadOnlyList<IedBrowserReadTarget> BuildInitialPriority(
IedBrowserLiveValues.CanonicalReference(attr.MmsReference)))
.ThenBy(attr => attr.MmsReference, StringComparer.Ordinal)
.Take(initialAnalogSeedLimit)
.ToArray();
// Selecting the canonical six owners must not depend on whether a
// Report has already supplied a magnitude. A received magnitude may
// still need its independently declared q/t and angle Read evidence.
var analog = analogSeeds
.Where(attr => !ledger.TryGet(attr.MmsReference, "MX", out var observed) ||
observed.IsStale || observed.Association != association)
.Select(attr => new IedBrowserReadTarget(attr.MmsReference, "MX",
string.IsNullOrWhiteSpace(attr.SclBType) ?
attr.MmsType : attr.SclBType))
string.IsNullOrWhiteSpace(attr.SclBType)
? attr.MmsType : attr.SclBType))
.ToArray();
// A bounded set of exact companions makes the phasor seed useful
// without presenting missing angle/quality/timestamp as "exposed"
// evidence. Unlike SCL presentation, these must be real live MMS
// attributes covered by the exact live DataSet and FC.
const int initialAnalogCompanionLimit = 9;
var companionOrder = analogSeeds
.SelectMany(seed => AnalogCompanionReferences(
IedBrowserLiveValues.CanonicalReference(seed.MmsReference)))
.Distinct(StringComparer.Ordinal)
.Select((reference, index) => (reference, index))
.ToDictionary(x => x.reference, x => x.index, StringComparer.Ordinal);
var companions = verifiedLiveModel.LogicalDevices
.SelectMany(ld => ld.LogicalNodes)
.SelectMany(ln => ln.DataObjects)
.SelectMany(obj => obj.Attributes)
.Where(attr => string.Equals(attr.FunctionalConstraint, "MX",
StringComparison.OrdinalIgnoreCase))
.Select(attr => new
{
Attr = attr,
Reference = IedBrowserLiveValues.CanonicalReference(attr.MmsReference)
})
.Where(item => companionOrder.ContainsKey(item.Reference) &&
verifiedLiveModel.DataSets.Any(set =>
schema.ContainsDataSetMember(set.Reference, item.Reference, "MX")))
.GroupBy(item => item.Reference, StringComparer.Ordinal)
// Ambiguous duplicate live FC declarations cannot authorize a read.
.Where(group => group.Count() == 1)
.Select(group => group.Single())
.Where(item => !ledger.TryGet(item.Reference, "MX", out var observed) ||
observed.IsStale || observed.Association != association)
.OrderBy(item => companionOrder[item.Reference])
.Take(initialAnalogCompanionLimit)
.Select(item => new IedBrowserReadTarget(item.Attr.MmsReference, "MX",
string.IsNullOrWhiteSpace(item.Attr.SclBType)
? item.Attr.MmsType : item.Attr.SclBType))
.ToArray();
// Critical station Mod/Beh/Health must never starve behind multiple
// optional control blocks or slow LActTm reads on a 10s envelope.
Expand All @@ -262,13 +304,41 @@ public static IReadOnlyList<IedBrowserReadTarget> BuildInitialPriority(
target.Reference.EndsWith(".GoEna", StringComparison.Ordinal))
.Take(2);
return critical.Concat(fastSg.Take(4)).Concat(firstGo).Concat(analog)
.Concat(settings).Concat(goose).Concat(candidates)
.Concat(companions).Concat(settings).Concat(goose).Concat(candidates)
.DistinctBy(target => IedBrowserLiveValues.CanonicalReference(target.Reference) +
"|" + target.FunctionalConstraint.Trim().ToUpperInvariant(),
StringComparer.Ordinal)
.Take(maxTargets).ToArray();
}

private static IEnumerable<string> AnalogCompanionReferences(string magnitude)
{
// Only the exact IEC phasor/measurement owner of a selected live
// magnitude can authorize companions. Similar suffixes, other LNs,
// missing FC and SCL-only leaves are not candidates.
const string cValMagnitude = ".cVal.mag.f";
const string simpleMagnitude = ".mag.f";
string owner;
string angle;
if (magnitude.EndsWith(cValMagnitude, StringComparison.Ordinal))
{
owner = magnitude[..^cValMagnitude.Length];
angle = owner + ".cVal.ang.f";
}
else if (magnitude.EndsWith(simpleMagnitude, StringComparison.Ordinal))
{
owner = magnitude[..^simpleMagnitude.Length];
angle = owner + ".ang.f";
}
else
yield break;
// Observe raw IEC quality/time before optional angle, never infer
// these from a magnitude or another phase's Report.
yield return owner + ".q";
yield return owner + ".t";
yield return angle;
}

private static int AnalogSeedRank(string reference)
{
// Deterministic priority for canonical IEC three-phase magnitude.
Expand Down
6 changes: 4 additions & 2 deletions Models/IedBrowserStructuredDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@
if (!scl.Owner.Attributes.Any(a =>
string.Equals(a.FunctionalConstraint, memberFc,
StringComparison.OrdinalIgnoreCase) &&
IedBrowserLiveValues.CanonicalReference(a.MmsReference)
.StartsWith(memberReference + ".", StringComparison.Ordinal)))
(string.Equals(IedBrowserLiveValues.CanonicalReference(a.MmsReference),
memberReference, StringComparison.Ordinal) ||
IedBrowserLiveValues.CanonicalReference(a.MmsReference)
.StartsWith(memberReference + ".", StringComparison.Ordinal))))
return live.Owner;
sclOnly = true;
return scl.Owner;
Expand Down Expand Up @@ -359,7 +361,7 @@
var hasDirect = matching.attr is not null;
var attr = hasDirect ? matching.attr : groupArray[0].attr;
var descendants = groupArray.Where(x => x.parts.Length > pathIndex + 1).ToArray();
var fullPrefix = IedBrowserLiveValues.CanonicalReference(attr.ObjectReference) +

Check warning on line 364 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 364 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 364 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 364 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 364 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.

Check warning on line 364 in Models/IedBrowserStructuredDetails.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Dereference of a possibly null reference.
"." + string.Join(".", groupArray[0].parts.Take(pathIndex + 1));
var polar = allowVector
? IedBrowserEngineeringComposite.BuildPolar(
Expand Down
Loading
Loading