From 94562ca4bf6cc8b83d129a7e28f10ba573e9f02a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:58:33 +0700 Subject: [PATCH 1/6] fix(browser): preserve SGCB LActTm raw UTC-Time evidence and mark unset zero time --- Services/NativeIec61850Client.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Services/NativeIec61850Client.cs b/Services/NativeIec61850Client.cs index 8a18c6c42..727c9324f 100644 --- a/Services/NativeIec61850Client.cs +++ b/Services/NativeIec61850Client.cs @@ -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); From 95e0f9814c23c7d5727b4fdf8e876914af72a36b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:58:36 +0700 Subject: [PATCH 2/6] fix(browser): preserve SGCB LActTm raw UTC-Time evidence and mark unset zero time --- Models/IedBrowserLiveValues.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Models/IedBrowserLiveValues.cs b/Models/IedBrowserLiveValues.cs index 5527871ee..6bc971fe3 100644 --- a/Models/IedBrowserLiveValues.cs +++ b/Models/IedBrowserLiveValues.cs @@ -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) From 65e197b55df94d47327126d524d7971c5314241d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:58:39 +0700 Subject: [PATCH 3/6] fix(browser): preserve SGCB LActTm raw UTC-Time evidence and mark unset zero time --- Models/IedBrowserControlStateEvidence.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Models/IedBrowserControlStateEvidence.cs b/Models/IedBrowserControlStateEvidence.cs index aa4c07b76..c50073eea 100644 --- a/Models/IedBrowserControlStateEvidence.cs +++ b/Models/IedBrowserControlStateEvidence.cs @@ -84,6 +84,24 @@ public static bool TryRead( } return true; } + /// + /// 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. + /// + 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('$', '.'); } From 1991577516bcb3dd07817d8859d383cdadb98759 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:58:42 +0700 Subject: [PATCH 4/6] fix(browser): preserve SGCB LActTm raw UTC-Time evidence and mark unset zero time --- MainWindow.IedBrowser.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/MainWindow.IedBrowser.cs b/MainWindow.IedBrowser.cs index dc5c2f385..511bfcb54 100644 --- a/MainWindow.IedBrowser.cs +++ b/MainWindow.IedBrowser.cs @@ -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 { From a90aaee3aef020351fc4d62efc3531aa5bd6c7b7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:58:45 +0700 Subject: [PATCH 5/6] test(browser): preserve and present SGCB zero UTC-Time without fabricating 1970 change --- .../M214SgLastChangedWireEvidenceTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/ARSAS.Tests/M214SgLastChangedWireEvidenceTests.cs diff --git a/tests/ARSAS.Tests/M214SgLastChangedWireEvidenceTests.cs b/tests/ARSAS.Tests/M214SgLastChangedWireEvidenceTests.cs new file mode 100644 index 000000000..81b1d8eb4 --- /dev/null +++ b/tests/ARSAS.Tests/M214SgLastChangedWireEvidenceTests.cs @@ -0,0 +1,44 @@ +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() + { + var time = Iec61850UtcTimeEvidence.Decode(MmsDataValue.UtcTime( + Iec61850UtcTime.FromBytes(new byte[8] { 0, 0, 0, 0, 0, 0, 0, 0x3F }))); + 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 = Iec61850UtcTimeEvidence.Decode(MmsDataValue.UtcTime( + Iec61850UtcTime.FromBytes(new byte[8] { 0x65, 0x10, 0x00, 0x00, 0, 0, 0, 0x3F }))); + 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)); + } +} From 5ef905ecefbe1a3bccbec28dfc89f71f9f374a15 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Thu, 15 Oct 2026 16:59:16 +0700 Subject: [PATCH 6/6] test(browser): use typed captured-byte fixture instead of synthetic UTC value without wire proof --- .../M214SgLastChangedWireEvidenceTests.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/ARSAS.Tests/M214SgLastChangedWireEvidenceTests.cs b/tests/ARSAS.Tests/M214SgLastChangedWireEvidenceTests.cs index 81b1d8eb4..7d0fafd60 100644 --- a/tests/ARSAS.Tests/M214SgLastChangedWireEvidenceTests.cs +++ b/tests/ARSAS.Tests/M214SgLastChangedWireEvidenceTests.cs @@ -10,8 +10,15 @@ public sealed class M214SgLastChangedWireEvidenceTests [Fact] public void ZeroWireUtcTime_IsUnsetGroupChange_NotA1970EngineeringEvent() { - var time = Iec61850UtcTimeEvidence.Decode(MmsDataValue.UtcTime( - Iec61850UtcTime.FromBytes(new byte[8] { 0, 0, 0, 0, 0, 0, 0, 0x3F }))); + // 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(); @@ -28,8 +35,13 @@ public void ZeroWireUtcTime_IsUnsetGroupChange_NotA1970EngineeringEvent() [Fact] public void RealTimestamp_AndUnverifiedText_AreNeverRewrittenAsUnset() { - var nonzero = Iec61850UtcTimeEvidence.Decode(MmsDataValue.UtcTime( - Iec61850UtcTime.FromBytes(new byte[8] { 0x65, 0x10, 0x00, 0x00, 0, 0, 0, 0x3F }))); + 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);