Skip to content
Merged
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
20 changes: 20 additions & 0 deletions Services/Iec61850ValueFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions engines/ARIEC61850.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
36 changes: 36 additions & 0 deletions tests/ARSAS.Tests/Iec61850ValueFormatterTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading