diff --git a/Services/Iec61850ValueFormatter.cs b/Services/Iec61850ValueFormatter.cs index 522aa0aa..d629f29a 100644 --- a/Services/Iec61850ValueFormatter.cs +++ b/Services/Iec61850ValueFormatter.cs @@ -7,6 +7,13 @@ public static class Iec61850ValueFormatter { public static string Format(object? value, string dataType, string unit) { + // Compatibility guard for ARIEC61850 builds that still expose an SPS-like + // DataObject report value as one rendered CDC structure instead of the + // projected stVal leaf. Keep this deliberately narrow: stVal must be the + // first named field and must be a Boolean. Other structures remain intact. + if (TryExtractStructuredBooleanStVal(value, out var structuredStVal)) + value = structuredStVal; + if (IsDbposDataType(dataType) && TryNormalizeDbpos(value, out var dbpos)) return FormatDbpos(dbpos); @@ -115,6 +122,19 @@ private static bool TryParseDbposText(string text, out int code) } } + private static bool TryExtractStructuredBooleanStVal(object? value, out bool status) + { + status = false; + if (value is not string text || string.IsNullOrWhiteSpace(text)) + return false; + + var match = Regex.Match( + text, + @"^\s*Structure\(\s*\d+\s*\)\s*\{\s*stVal\s*=\s*(true|false)\b", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + return match.Success && bool.TryParse(match.Groups[1].Value, out status); + } + private static bool TryParseBits(string bits, out int code) { code = bits switch diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index cb321207..ed6a1666 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "repository": "masarray/ARIEC61850", "ref": "main", - "commit": "761fa0df9ca84fbe15352d2670f03fddbe8784ba", - "sourcePullRequest": 78, - "purpose": "Pins the ARIEC61850 engine used by ARSAS. PR #76 preserves unresolved static DataSet members, PR #77 canonicalizes cross-logical-device SCL references, and PR #78 makes mandatory Signal Selection inventory protocol-member-centric: exactly one descriptor per static DataSet member while keeping resolved runtime primary leaves separate from the original FCDA/FCD identity." + "commit": "1ed53ab3252902f680d76adbadd25790bf6f8ca4", + "sourcePullRequest": 79, + "purpose": "Pins the ARIEC61850 engine used by ARSAS. PR #76 preserves unresolved static DataSet members, PR #77 canonicalizes cross-logical-device SCL references, PR #78 keeps one descriptor per static DataSet member while separating resolved runtime primary leaves from the original FCDA/FCD identity, and PR #79 projects generic Boolean status structures {stVal, q, t} to the scalar stVal leaf while preserving quality/timestamp and raw-structure diagnostics for unrecognized non-Boolean structures." } diff --git a/tests/ARSAS.Tests/Iec61850ValueFormatterTests.cs b/tests/ARSAS.Tests/Iec61850ValueFormatterTests.cs new file mode 100644 index 00000000..ddbe66b6 --- /dev/null +++ b/tests/ARSAS.Tests/Iec61850ValueFormatterTests.cs @@ -0,0 +1,36 @@ +using ArIED61850Tester.Services; + +namespace ARSAS.Tests; + +public sealed class Iec61850ValueFormatterTests +{ + [Fact] + public void Format_Extracts_Boolean_StVal_From_Legacy_Report_Structure() + { + const string value = "Structure(3) {stVal=false, q=Quality{V=1,D=0,Ov=0,F=0,Osc=0,B=0,Oot=0,Incon=0,Ina=0,src=0,test=0,opBlk=0}, t=BinaryTime(2026-08-15 16:11:57.723, Q=0x0A, ext=True)}"; + + var formatted = Iec61850ValueFormatter.Format(value, "Boolean", string.Empty); + + Assert.Equal("False", formatted); + } + + [Fact] + public void Format_Extracts_True_Boolean_StVal_From_Legacy_Report_Structure() + { + const string value = "Structure(3) {stVal=true, q=Quality{V=1,D=0}, t=BinaryTime(2026-08-15 16:11:57.723, Q=0x0A, ext=True)}"; + + var formatted = Iec61850ValueFormatter.Format(value, "Boolean", string.Empty); + + Assert.Equal("True", formatted); + } + + [Fact] + public void Format_Does_Not_Collapse_NonStVal_Structures() + { + const string value = "Structure(3) {mag=123.4, q=Quality{V=1,D=0}, t=BinaryTime(2026-08-15 16:11:57.723, Q=0x0A, ext=True)}"; + + var formatted = Iec61850ValueFormatter.Format(value, "Float", "V"); + + Assert.Equal(value, formatted); + } +} diff --git a/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs b/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs index 1b18e004..1f9c43e4 100644 --- a/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs +++ b/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs @@ -68,13 +68,14 @@ public void SignalSelectionRecovery_RunsAgainAfterLegacyConstructorDisplayPrepar } [Fact] - public void EngineLock_PinsMemberCentricStaticDataSetInventory() + public void EngineLock_PinsMergedReportProjectionEngineWithoutLosingMemberCentricInventory() { var source = File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json")); - Assert.Contains("761fa0df9ca84fbe15352d2670f03fddbe8784ba", source, StringComparison.OrdinalIgnoreCase); - Assert.Contains("\"sourcePullRequest\": 78", source, StringComparison.Ordinal); + Assert.Contains("1ed53ab3252902f680d76adbadd25790bf6f8ca4", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("\"sourcePullRequest\": 79", source, StringComparison.Ordinal); Assert.Contains("one descriptor per static DataSet member", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("generic Boolean status structures", source, StringComparison.OrdinalIgnoreCase); } [Fact]