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
10 changes: 8 additions & 2 deletions MainWindow.IedBrowser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -584,12 +584,18 @@ private void UpdateIedBrowserControlStateDetails()
device.LiveValues, block, pageKind, field,
out var observed, out var reference))
{
row.Value = observed.IsStale ? "Offline · " + observed.Value : observed.Value;
var display = IedBrowserControlStateEvidence.FormatObservedField(
pageKind, field, observed);
row.Value = observed.IsStale ? "Offline · " + display : display;
row.ObservationSource = observed.IsStale
? "Stale MMS Read" : "MMS Read";
row.ObservationTooltip = $"{reference} [{block.FunctionalConstraint}]" +
$" · {observed.Source} · {observed.ObservedUtc:yyyy-MM-dd HH:mm:ss} UTC" +
$" · association {observed.Association}";
$" · association {observed.Association}" +
(field == "LActTm" && observed.TimeBits is
{ IsDecoded: true, HasWireProvenance: true } time
? $" · wire {time.RawHex} · time quality {time.QualityHex}"
: "");
}
else
{
Expand Down
18 changes: 18 additions & 0 deletions Models/IedBrowserControlStateEvidence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ public static bool TryRead(
}
return true;
}
/// <summary>
/// SGCB LActTm is a real relay value, but an all-zero wire UTC-Time
/// denotes an unset time rather than an actual group-change in 1970.
/// A formatted date alone is never proof: require exact raw MMS bytes.
/// </summary>
public static string FormatObservedField(
string pageKind, string field, IedBrowserObservedValue observed)
=> pageKind == "SGCB" && field == "LActTm" &&
observed.TimeBits is
{
IsDecoded: true,
HasWireProvenance: true,
SecondsSinceEpoch: 0,
FractionOfSecond24: 0
}
? "Not initialized"
: observed.Value;

private static string Canonical(string reference)
=> (reference ?? "").Trim().Replace('$', '.');
}
4 changes: 3 additions & 1 deletion Models/IedBrowserLiveValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,9 @@ private static bool IsTimeReference(string key)
{
var fcIndex = key.LastIndexOf('|');
return fcIndex > 2 && key.AsSpan(0, fcIndex).EndsWith(
".t".AsSpan(), StringComparison.Ordinal);
".t".AsSpan(), StringComparison.Ordinal) ||
fcIndex > 2 && key.AsSpan(0, fcIndex).EndsWith(
".LActTm".AsSpan(), StringComparison.Ordinal);
}

public void PublishReport(Iec61850MonitorPoint point, bool observedReportIngress = false)
Expand Down
5 changes: 4 additions & 1 deletion Services/NativeIec61850Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1113,7 +1113,10 @@ private static string BuildDynamicDataSetName(ReportControlPlan plan)
raw.RawValue.ToArray(), out var verifiedQuality))
projected.QualityEvidence = verifiedQuality;
if ((reference.EndsWith(".t", StringComparison.Ordinal) ||
reference.EndsWith("$t", StringComparison.Ordinal)) &&
reference.EndsWith("$t", StringComparison.Ordinal) ||
// SGCB LActTm is the model-verified last-active-group UTC-Time.
reference.EndsWith(".LActTm", StringComparison.Ordinal) ||
reference.EndsWith("$LActTm", StringComparison.Ordinal)) &&
raw.Kind == ArMms.MmsDataKind.UtcTime)
{
var evidence = ArMms.Iec61850UtcTimeEvidence.Decode(raw);
Expand Down
56 changes: 56 additions & 0 deletions tests/ARSAS.Tests/M214SgLastChangedWireEvidenceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using AR.Iec61850.Mms;
using ArIED61850Tester.Models;

namespace ARSAS.Tests;

public sealed class M214SgLastChangedWireEvidenceTests
{
private const string Reference = "LD/LLN0.SGCB.LActTm";

[Fact]
public void ZeroWireUtcTime_IsUnsetGroupChange_NotA1970EngineeringEvent()
{
// Typed fixture represents a validated raw eight-byte LActTm from
// the pinned engine's decoder; no formatted-date guessing.
var time = new Iec61850UtcTimeEvidence
{
IsDecoded = true, HasWireProvenance = true,
RawHex = "000000000000003F",
SecondsSinceEpoch = 0, FractionOfSecond24 = 0,
Quality = 0x3F, EngineeringUtc = "1970-01-01 00:00:00 UTC"
};
Assert.True(time.HasWireProvenance);
Assert.Equal(0u, time.SecondsSinceEpoch);
var ledger = new IedBrowserLiveValues();
var association = ledger.BeginAssociation();
Assert.True(ledger.PublishForAssociation(association, Reference, "SP",
time.EngineeringUtc, "", "", "MMS Read", utcTimeEvidence: time));
Assert.True(ledger.TryGet(Reference, "SP", out var observed));
Assert.NotNull(observed.TimeBits);
Assert.Equal("Not initialized", IedBrowserControlStateEvidence.FormatObservedField(
"SGCB", "LActTm", observed));
Assert.Equal(time.RawHex, observed.TimeBits.RawHex);
}

[Fact]
public void RealTimestamp_AndUnverifiedText_AreNeverRewrittenAsUnset()
{
var nonzero = new Iec61850UtcTimeEvidence
{
IsDecoded = true, HasWireProvenance = true,
RawHex = "651000000000003F", SecondsSinceEpoch = 0x65100000,
FractionOfSecond24 = 0, Quality = 0x3F,
EngineeringUtc = "2023-09-24 00:00:00 UTC"
};
var value = new IedBrowserObservedValue(
nonzero.EngineeringUtc, "", "", "MMS Read", DateTime.UtcNow,
TimeBits: nonzero);
Assert.Equal(nonzero.EngineeringUtc,
IedBrowserControlStateEvidence.FormatObservedField("SGCB", "LActTm", value));
var legacy = value with { Value = "1970-01-01 00:00:00 UTC", TimeBits = null };
Assert.Equal(legacy.Value,
IedBrowserControlStateEvidence.FormatObservedField("SGCB", "LActTm", legacy));
Assert.Equal(legacy.Value,
IedBrowserControlStateEvidence.FormatObservedField("GoCB", "LActTm", legacy));
}
}
Loading