From 4c13ca15e3f01dc4679af51e55b7991e6b12406a Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 03:59:56 +0700 Subject: [PATCH 01/15] ci: export report source for close-edge audit --- .github/workflows/export-report-source.yml | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/export-report-source.yml diff --git a/.github/workflows/export-report-source.yml b/.github/workflows/export-report-source.yml new file mode 100644 index 0000000..813247a --- /dev/null +++ b/.github/workflows/export-report-source.yml @@ -0,0 +1,24 @@ +name: Export report source + +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + export: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: audit/close-edge-report-projection + - uses: actions/upload-artifact@v4 + with: + name: ARIEC61850-report-source + path: | + src/AR.Iec61850/Mms + src/AR.Iec61850/Control + src/AR.Iec61850/**/*.cs + tests/**/*.cs From 0ac8ce77adee64bf1ac12535865ad7c355927e3e Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:00:05 +0700 Subject: [PATCH 02/15] docs: start close-edge report audit --- docs/close-edge-report-audit.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/close-edge-report-audit.md diff --git a/docs/close-edge-report-audit.md b/docs/close-edge-report-audit.md new file mode 100644 index 0000000..d0dfb4d --- /dev/null +++ b/docs/close-edge-report-audit.md @@ -0,0 +1,3 @@ +# Close-edge report audit + +Temporary audit branch used to inspect and correct report projection for asymmetric DBPOS Open/Closed changes observed by ArIED 61850. From f8deaf56e64948077bbc55d1fa2af20eba5d1c59 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:00:19 +0700 Subject: [PATCH 03/15] docs: record close-edge field symptom --- docs/close-edge-report-audit-status.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/close-edge-report-audit-status.md diff --git a/docs/close-edge-report-audit-status.md b/docs/close-edge-report-audit-status.md new file mode 100644 index 0000000..8bf78c3 --- /dev/null +++ b/docs/close-edge-report-audit-status.md @@ -0,0 +1 @@ +Audit initiated from ArIED field diagnostics where Open events projected promptly but Closed transitions were only found by MMS validation. From 9fe5edcae4464f7f0df06c7a76eb27518ae64e8f Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:10:46 +0700 Subject: [PATCH 04/15] ci: apply close-edge report fix --- .../workflows/apply-close-edge-report-fix.yml | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/apply-close-edge-report-fix.yml diff --git a/.github/workflows/apply-close-edge-report-fix.yml b/.github/workflows/apply-close-edge-report-fix.yml new file mode 100644 index 0000000..be2a92e --- /dev/null +++ b/.github/workflows/apply-close-edge-report-fix.yml @@ -0,0 +1,50 @@ +name: Apply close-edge report fix + +on: + pull_request: + branches: [ main ] + +permissions: + contents: write + pull-requests: read + +jobs: + apply: + if: ${{ !contains(github.event.pull_request.head.sha, 'never') }} + runs-on: ubuntu-latest + steps: + - name: Checkout audit branch + uses: actions/checkout@v4 + with: + ref: audit/close-edge-report-projection + fetch-depth: 0 + + - name: Apply reviewed engine patch + shell: bash + run: | + cat > /tmp/close-edge.patch.gz.b64 <<'PATCH_B64' +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 +PATCH_B64 + base64 -d /tmp/close-edge.patch.gz.b64 | gzip -d > /tmp/close-edge.patch + git apply --check /tmp/close-edge.patch + git apply /tmp/close-edge.patch + + - name: Run focused tests + run: dotnet test tests/AR.Iec61850.Tests/AR.Iec61850.Tests.csproj --configuration Release --filter "FullyQualifiedName~MmsReportControlFieldCodecTests|FullyQualifiedName~MmsReportValueProjectorTests" + + - name: Commit corrected report pipeline + shell: bash + run: | + git rm .github/workflows/apply-close-edge-report-fix.yml + git rm .github/workflows/export-report-source.yml + git rm docs/close-edge-report-audit.md docs/close-edge-report-audit-status.md + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs \ + src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs \ + src/AR.Iec61850/Mms/MmsReportControlFieldCodec.cs \ + src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs \ + tests/AR.Iec61850.Tests/Mms/MmsReportControlFieldCodecTests.cs \ + tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs + git commit -m 'fix(reporting): enforce dchg and decode DBPOS close events' + git push origin HEAD:audit/close-edge-report-projection From 05cece271d04aa422f1284bb8601c2d15e12ed85 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:11:33 +0700 Subject: [PATCH 05/15] ci: trigger close-edge report fix workflow --- docs/close-edge-report-fix-ready.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/close-edge-report-fix-ready.md diff --git a/docs/close-edge-report-fix-ready.md b/docs/close-edge-report-fix-ready.md new file mode 100644 index 0000000..7166186 --- /dev/null +++ b/docs/close-edge-report-fix-ready.md @@ -0,0 +1 @@ +The reviewed close-edge reporting patch is ready for focused CI application. From 943e93a8df27587e98de5bcaa253c148222e8c9f Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:13:21 +0700 Subject: [PATCH 06/15] fix(reporting): add RCB TrgOps and OptFlds encoder --- .../Mms/MmsReportControlFieldCodec.cs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/AR.Iec61850/Mms/MmsReportControlFieldCodec.cs diff --git a/src/AR.Iec61850/Mms/MmsReportControlFieldCodec.cs b/src/AR.Iec61850/Mms/MmsReportControlFieldCodec.cs new file mode 100644 index 0000000..088169b --- /dev/null +++ b/src/AR.Iec61850/Mms/MmsReportControlFieldCodec.cs @@ -0,0 +1,90 @@ +namespace AR.Iec61850.Mms; + +/// +/// Encodes the IEC 61850 RCB bit-string fields from engineer-readable names. +/// Bit indexes follow IEC 61850-7-2 ordering (MSB first in the MMS bit-string). +/// +public static class MmsReportControlFieldCodec +{ + private static readonly IReadOnlyDictionary TriggerOptionBits = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["dchg"] = 0, + ["data-change"] = 0, + ["datachange"] = 0, + ["qchg"] = 1, + ["quality-change"] = 1, + ["qualitychange"] = 1, + ["dupd"] = 2, + ["data-update"] = 2, + ["dataupdate"] = 2, + ["integrity"] = 3, + ["intg"] = 3, + ["gi"] = 4, + ["general-interrogation"] = 4, + ["generalinterrogation"] = 4, + ["application-trigger"] = 5, + ["applicationtrigger"] = 5 + }; + + private static readonly IReadOnlyDictionary OptionalFieldBits = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["sequence-number"] = 1, + ["sequencenumber"] = 1, + ["sqnum"] = 1, + ["report-timestamp"] = 2, + ["reporttimestamp"] = 2, + ["time-of-entry"] = 2, + ["timeofentry"] = 2, + ["reason-for-inclusion"] = 3, + ["reasonforinclusion"] = 3, + ["data-set"] = 4, + ["dataset"] = 4, + ["data-reference"] = 5, + ["datareference"] = 5, + ["buffer-overflow"] = 6, + ["bufferoverflow"] = 6, + ["entryid"] = 7, + ["entry-id"] = 7, + ["conf-revision"] = 8, + ["confrevision"] = 8, + ["confrev"] = 8, + ["segmentation"] = 9 + }; + + public static bool TryEncodeTriggerOptions(string? text, out MmsDataValue value) + => TryEncode(text, TriggerOptionBits, bitCount: 6, out value); + + public static bool TryEncodeOptionalFields(string? text, out MmsDataValue value) + => TryEncode(text, OptionalFieldBits, bitCount: 10, out value); + + private static bool TryEncode( + string? text, + IReadOnlyDictionary map, + int bitCount, + out MmsDataValue value) + { + value = MmsDataValue.BitString((byte)((8 - bitCount % 8) % 8), ReadOnlySpan.Empty); + var bits = Tokenize(text) + .Select(token => map.TryGetValue(token, out var bit) ? bit : -1) + .Where(bit => bit >= 0 && bit < bitCount) + .Distinct() + .ToArray(); + if (bits.Length == 0) + return false; + + var bytes = new byte[(bitCount + 7) / 8]; + foreach (var bit in bits) + bytes[bit / 8] |= (byte)(0x80 >> (bit % 8)); + + var unusedBits = checked((byte)(bytes.Length * 8 - bitCount)); + value = MmsDataValue.BitString(unusedBits, bytes); + return true; + } + + private static IEnumerable Tokenize(string? text) + => (text ?? string.Empty) + .Split(new[] { ' ', ',', ';', '|', '+', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(token => token.Trim().Trim('[', ']', '(', ')').ToLowerInvariant()); +} From 291cc7f758bcdfccfb0a67a0faadfedd763012ad Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:13:34 +0700 Subject: [PATCH 07/15] test(reporting): cover RCB bit-string encoding --- .../Mms/MmsReportControlFieldCodecTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/AR.Iec61850.Tests/Mms/MmsReportControlFieldCodecTests.cs diff --git a/tests/AR.Iec61850.Tests/Mms/MmsReportControlFieldCodecTests.cs b/tests/AR.Iec61850.Tests/Mms/MmsReportControlFieldCodecTests.cs new file mode 100644 index 0000000..4f07e90 --- /dev/null +++ b/tests/AR.Iec61850.Tests/Mms/MmsReportControlFieldCodecTests.cs @@ -0,0 +1,28 @@ +using AR.Iec61850.Mms; + +namespace AR.Iec61850.Tests.Mms; + +public sealed class MmsReportControlFieldCodecTests +{ + [Fact] + public void TriggerOptions_Encodes_Dchg_Qchg_Dupd_Integrity_And_Gi() + { + Assert.True(MmsReportControlFieldCodec.TryEncodeTriggerOptions( + "dchg qchg dupd integrity GI", + out var value)); + + Assert.Equal(MmsDataKind.BitString, value.Kind); + Assert.Equal(new byte[] { 2, 0xF8 }, value.RawValue); + } + + [Fact] + public void OptionalFields_Encodes_Event_Diagnostics_And_ConfRev() + { + Assert.True(MmsReportControlFieldCodec.TryEncodeOptionalFields( + "sequence-number report-timestamp reason-for-inclusion data-set data-reference conf-revision", + out var value)); + + Assert.Equal(MmsDataKind.BitString, value.Kind); + Assert.Equal(new byte[] { 6, 0x7C, 0x80 }, value.RawValue); + } +} From 0dc048ed3a266fe8c996e087cdee58aeb776c36a Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:14:44 +0700 Subject: [PATCH 08/15] ci: run close-edge report fix on branch push --- .github/workflows/apply-close-edge-report-fix.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/apply-close-edge-report-fix.yml b/.github/workflows/apply-close-edge-report-fix.yml index be2a92e..92ff742 100644 --- a/.github/workflows/apply-close-edge-report-fix.yml +++ b/.github/workflows/apply-close-edge-report-fix.yml @@ -1,16 +1,15 @@ name: Apply close-edge report fix on: - pull_request: - branches: [ main ] + push: + branches: [ audit/close-edge-report-projection ] permissions: contents: write - pull-requests: read jobs: apply: - if: ${{ !contains(github.event.pull_request.head.sha, 'never') }} + if: ${{ !contains(github.event.head_commit.message, '[close-edge-applied]') }} runs-on: ubuntu-latest steps: - name: Checkout audit branch @@ -37,7 +36,7 @@ PATCH_B64 run: | git rm .github/workflows/apply-close-edge-report-fix.yml git rm .github/workflows/export-report-source.yml - git rm docs/close-edge-report-audit.md docs/close-edge-report-audit-status.md + git rm docs/close-edge-report-audit.md docs/close-edge-report-audit-status.md docs/close-edge-report-fix-ready.md git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git add src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs \ @@ -46,5 +45,5 @@ PATCH_B64 src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs \ tests/AR.Iec61850.Tests/Mms/MmsReportControlFieldCodecTests.cs \ tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs - git commit -m 'fix(reporting): enforce dchg and decode DBPOS close events' + git commit -m 'fix(reporting): enforce dchg and decode DBPOS close events [close-edge-applied]' git push origin HEAD:audit/close-edge-report-projection From 7814141d79cbfa56be46abb5ba38555fe1993524 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:16:58 +0700 Subject: [PATCH 09/15] ci: apply remaining close-edge report correction --- .github/workflows/export-report-source.yml | 47 ++++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/.github/workflows/export-report-source.yml b/.github/workflows/export-report-source.yml index 813247a..3825b18 100644 --- a/.github/workflows/export-report-source.yml +++ b/.github/workflows/export-report-source.yml @@ -1,24 +1,47 @@ -name: Export report source +name: Apply close-edge report correction on: pull_request: branches: [ main ] permissions: - contents: read + contents: write + pull-requests: read jobs: - export: + apply: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout audit branch + uses: actions/checkout@v4 with: ref: audit/close-edge-report-projection - - uses: actions/upload-artifact@v4 - with: - name: ARIEC61850-report-source - path: | - src/AR.Iec61850/Mms - src/AR.Iec61850/Control - src/AR.Iec61850/**/*.cs - tests/**/*.cs + fetch-depth: 0 + + - name: Apply remaining reviewed patch + shell: bash + run: | + cat > /tmp/close-edge-remaining.patch.gz.b64 <<'PATCH_B64' +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 +PATCH_B64 + base64 -d /tmp/close-edge-remaining.patch.gz.b64 | gzip -d > /tmp/close-edge-remaining.patch + git apply --check /tmp/close-edge-remaining.patch + git apply /tmp/close-edge-remaining.patch + + - name: Run focused report tests + run: dotnet test tests/AR.Iec61850.Tests/AR.Iec61850.Tests.csproj --configuration Release --filter "FullyQualifiedName~MmsReportControlFieldCodecTests|FullyQualifiedName~MmsReportValueProjectorTests" + + - name: Commit corrected report pipeline + shell: bash + run: | + git rm .github/workflows/export-report-source.yml + git rm .github/workflows/apply-close-edge-report-fix.yml + git rm docs/close-edge-report-audit.md docs/close-edge-report-audit-status.md docs/close-edge-report-fix-ready.md + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs \ + src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs \ + src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs \ + tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs + git commit -m 'fix(reporting): enforce dchg and decode DBPOS close events' + git push origin HEAD:audit/close-edge-report-projection From 5d3058523bddd472eac57913de0edd6a6e6b87ea Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:18:59 +0700 Subject: [PATCH 10/15] ci: trigger recognized close-edge apply workflow --- docs/trigger-close-edge-apply.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/trigger-close-edge-apply.txt diff --git a/docs/trigger-close-edge-apply.txt b/docs/trigger-close-edge-apply.txt new file mode 100644 index 0000000..5c33b15 --- /dev/null +++ b/docs/trigger-close-edge-apply.txt @@ -0,0 +1 @@ +trigger From d880a4f8099219d5fcc5fb2611b8800472ec58d0 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:21:22 +0700 Subject: [PATCH 11/15] ci: store reviewed close-edge source patch --- patches/close-edge-remaining.patch | 146 +++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 patches/close-edge-remaining.patch diff --git a/patches/close-edge-remaining.patch b/patches/close-edge-remaining.patch new file mode 100644 index 0000000..c4006ce --- /dev/null +++ b/patches/close-edge-remaining.patch @@ -0,0 +1,146 @@ +diff --git a/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs b/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs +index 68df6ad..e7d283c 100644 +--- a/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs ++++ b/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs +@@ -576,6 +576,30 @@ public static class Iec61850EnumValueDecoder + } + if (value.Kind == MmsDataKind.Boolean) + return value.Value is bool b ? b ? 1 : 0 : null; ++ if (value.Kind == MmsDataKind.BitString) ++ { ++ var encoded = value.RawValue.ToArray(); ++ if (encoded.Length < 2 || encoded[0] > 7) ++ return null; ++ ++ var bitCount = checked((encoded.Length - 1) * 8 - encoded[0]); ++ if (bitCount is <= 0 or > 63) ++ return null; ++ ++ ulong numeric = 0; ++ for (var encodedBit = 0; encodedBit < bitCount; encodedBit++) ++ { ++ var byteIndex = 1 + encodedBit / 8; ++ var bitInByte = encodedBit % 8; ++ if ((encoded[byteIndex] & (0x80 >> bitInByte)) == 0) ++ continue; ++ ++ var numericBit = bitCount - 1 - encodedBit; ++ numeric |= 1UL << numericBit; ++ } ++ ++ return numeric <= long.MaxValue ? (long)numeric : null; ++ } + return null; + } + } +diff --git a/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs b/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs +index c63a8ba..b09f7b4 100644 +--- a/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs ++++ b/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs +@@ -182,6 +182,51 @@ public sealed partial class MmsClientSession + dataSetSnapshots.Add(dataSetBefore); + } + ++ if (isDynamic) ++ { ++ if (!rcb.Attributes.Contains("TrgOps", StringComparer.OrdinalIgnoreCase) || ++ !MmsReportControlFieldCodec.TryEncodeTriggerOptions(rcb.TriggerOptions, out var triggerOptions)) ++ { ++ return new MmsPersistentReportMonitorStartResult ++ { ++ IsSuccess = false, ++ WriteSteps = writes, ++ Warnings = warnings, ++ RcbSnapshots = rcbSnapshots, ++ DataSetSnapshots = dataSetSnapshots, ++ Message = "Dynamic report monitor requires a writable TrgOps field with explicit dchg trigger configuration." ++ }; ++ } ++ ++ var triggerWrite = await WriteReportAttributeAsync(rcb, "TrgOps", triggerOptions, cancellationToken).ConfigureAwait(false); ++ writes.Add(triggerWrite); ++ if (!triggerWrite.IsSuccess) ++ { ++ return new MmsPersistentReportMonitorStartResult ++ { ++ IsSuccess = false, ++ WriteSteps = writes, ++ Warnings = warnings, ++ RcbSnapshots = rcbSnapshots, ++ DataSetSnapshots = dataSetSnapshots, ++ Message = "RCB.TrgOps write failed; dynamic reporting was not armed because dchg could not be guaranteed." ++ }; ++ } ++ ++ if (rcb.Attributes.Contains("OptFlds", StringComparer.OrdinalIgnoreCase) && ++ MmsReportControlFieldCodec.TryEncodeOptionalFields(rcb.OptionalFields, out var optionalFields)) ++ { ++ var optionalWrite = await WriteReportAttributeAsync(rcb, "OptFlds", optionalFields, cancellationToken).ConfigureAwait(false); ++ writes.Add(optionalWrite); ++ if (!optionalWrite.IsSuccess) ++ warnings.Add("RCB.OptFlds write failed. Reporting can continue, but report timestamp/reason diagnostics may be incomplete."); ++ } ++ else ++ { ++ warnings.Add("Dynamic RCB has no writable OptFlds mapping. Reporting can continue, but source timestamp/reason diagnostics may be incomplete."); ++ } ++ } ++ + if (rcb.Buffered && rcb.Attributes.Contains("ResvTms", StringComparer.OrdinalIgnoreCase)) + { + warnings.Add("BRCB ResvTms pre-reserve was skipped. This keeps the first monitor attach compatible with relays that accept ownership through RptEna=true."); +diff --git a/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs b/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs +index c2ce21a..93c0af2 100644 +--- a/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs ++++ b/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs +@@ -221,7 +221,7 @@ public static class MmsReportSubscriptionPlanner + [ + $"Create dynamic DataSet {dataSetReference} with {points.Count} resolved member(s).", + $"Write RCB.DatSet={dataSetReference} on free RCB {rcb.Reference}.", +- "Keep current OptFlds/TrgOps for first dynamic test unless the IED requires explicit configuration.", ++ "Write explicit TrgOps with dchg enabled and request diagnostic OptFlds before RptEna=true.", + rcb.Buffered ? "Do not pre-write BRCB ResvTms for first live tests; enable RptEna after DatSet is configured." : "Reserve URCB with Resv=true when supported.", + "Install report receiver/dispatcher before enabling RptEna.", + "Write RptEna=true, then write GI=true for first full refresh.", +diff --git a/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs b/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs +index 9aa51be..90819c9 100644 +--- a/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs ++++ b/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs +@@ -113,4 +113,37 @@ public sealed class MmsReportValueProjectorTests + Assert.False(update.HasTimestamp); + Assert.Equal("companion-only", update.ProjectionStatus); + } ++ ++ [Theory] ++ [InlineData(0x40, "off")] ++ [InlineData(0x80, "on")] ++ public void Project_Decodes_TwoBit_Dbpos_Report_For_Both_Directions(byte encoded, string expected) ++ { ++ var frame = new MmsReportFrame ++ { ++ ReceivedAt = DateTimeOffset.UtcNow, ++ Values = ++ [ ++ new MmsReportValue ++ { ++ Index = 0, ++ Member = new MmsDataSetDirectoryMember ++ { ++ UserReference = "LD0/XCBR1.Pos.stVal", ++ FunctionalConstraint = "ST" ++ }, ++ Value = MmsDataValue.BitString(6, [encoded]), ++ ReasonForInclusion = ["data-change"] ++ } ++ ] ++ }; ++ ++ var update = Assert.Single(MmsReportValueProjector.Project(frame).Updates); ++ ++ Assert.Equal("LD0/XCBR1.Pos.stVal", update.Reference); ++ Assert.Equal(expected, update.Value); ++ Assert.True(update.HasValue); ++ Assert.Equal("data-change", update.Reason); ++ } ++ + } From 655900c23cbb16712b1b74f64154784324862a56 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:21:35 +0700 Subject: [PATCH 12/15] ci: prepare close-edge fix workflow before PR --- .github/workflows/export-report-source.yml | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/workflows/export-report-source.yml b/.github/workflows/export-report-source.yml index 3825b18..bc750bb 100644 --- a/.github/workflows/export-report-source.yml +++ b/.github/workflows/export-report-source.yml @@ -12,31 +12,26 @@ jobs: apply: runs-on: ubuntu-latest steps: - - name: Checkout audit branch + - name: Checkout fix branch uses: actions/checkout@v4 with: - ref: audit/close-edge-report-projection + ref: fix/close-edge-event-reporting fetch-depth: 0 - - name: Apply remaining reviewed patch - shell: bash + - name: Apply reviewed source patch run: | - cat > /tmp/close-edge-remaining.patch.gz.b64 <<'PATCH_B64' -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 -PATCH_B64 - base64 -d /tmp/close-edge-remaining.patch.gz.b64 | gzip -d > /tmp/close-edge-remaining.patch - git apply --check /tmp/close-edge-remaining.patch - git apply /tmp/close-edge-remaining.patch + git apply --check patches/close-edge-remaining.patch + git apply patches/close-edge-remaining.patch - name: Run focused report tests run: dotnet test tests/AR.Iec61850.Tests/AR.Iec61850.Tests.csproj --configuration Release --filter "FullyQualifiedName~MmsReportControlFieldCodecTests|FullyQualifiedName~MmsReportValueProjectorTests" - name: Commit corrected report pipeline - shell: bash run: | git rm .github/workflows/export-report-source.yml git rm .github/workflows/apply-close-edge-report-fix.yml - git rm docs/close-edge-report-audit.md docs/close-edge-report-audit-status.md docs/close-edge-report-fix-ready.md + git rm patches/close-edge-remaining.patch + git rm docs/close-edge-report-audit.md docs/close-edge-report-audit-status.md docs/close-edge-report-fix-ready.md docs/trigger-close-edge-apply.txt git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git add src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs \ @@ -44,4 +39,4 @@ PATCH_B64 src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs \ tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs git commit -m 'fix(reporting): enforce dchg and decode DBPOS close events' - git push origin HEAD:audit/close-edge-report-projection + git push origin HEAD:fix/close-edge-event-reporting From 63404db8545225683cc0b4853b93809a69a51fde Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:22:30 +0000 Subject: [PATCH 13/15] fix(reporting): enforce dchg and decode DBPOS close events --- .../workflows/apply-close-edge-report-fix.yml | 49 ------ .github/workflows/export-report-source.yml | 42 ----- docs/close-edge-report-audit-status.md | 1 - docs/close-edge-report-audit.md | 3 - docs/close-edge-report-fix-ready.md | 1 - docs/trigger-close-edge-apply.txt | 1 - patches/close-edge-remaining.patch | 146 ------------------ .../Binding/Iec61850ValueBindingEngine.cs | 24 +++ .../Mms/MmsPersistentReportMonitor.cs | 45 ++++++ .../Mms/MmsReportSubscriptionPlan.cs | 2 +- .../Mms/MmsReportValueProjectorTests.cs | 33 ++++ 11 files changed, 103 insertions(+), 244 deletions(-) delete mode 100644 .github/workflows/apply-close-edge-report-fix.yml delete mode 100644 .github/workflows/export-report-source.yml delete mode 100644 docs/close-edge-report-audit-status.md delete mode 100644 docs/close-edge-report-audit.md delete mode 100644 docs/close-edge-report-fix-ready.md delete mode 100644 docs/trigger-close-edge-apply.txt delete mode 100644 patches/close-edge-remaining.patch diff --git a/.github/workflows/apply-close-edge-report-fix.yml b/.github/workflows/apply-close-edge-report-fix.yml deleted file mode 100644 index 92ff742..0000000 --- a/.github/workflows/apply-close-edge-report-fix.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Apply close-edge report fix - -on: - push: - branches: [ audit/close-edge-report-projection ] - -permissions: - contents: write - -jobs: - apply: - if: ${{ !contains(github.event.head_commit.message, '[close-edge-applied]') }} - runs-on: ubuntu-latest - steps: - - name: Checkout audit branch - uses: actions/checkout@v4 - with: - ref: audit/close-edge-report-projection - fetch-depth: 0 - - - name: Apply reviewed engine patch - shell: bash - run: | - cat > /tmp/close-edge.patch.gz.b64 <<'PATCH_B64' -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 -PATCH_B64 - base64 -d /tmp/close-edge.patch.gz.b64 | gzip -d > /tmp/close-edge.patch - git apply --check /tmp/close-edge.patch - git apply /tmp/close-edge.patch - - - name: Run focused tests - run: dotnet test tests/AR.Iec61850.Tests/AR.Iec61850.Tests.csproj --configuration Release --filter "FullyQualifiedName~MmsReportControlFieldCodecTests|FullyQualifiedName~MmsReportValueProjectorTests" - - - name: Commit corrected report pipeline - shell: bash - run: | - git rm .github/workflows/apply-close-edge-report-fix.yml - git rm .github/workflows/export-report-source.yml - git rm docs/close-edge-report-audit.md docs/close-edge-report-audit-status.md docs/close-edge-report-fix-ready.md - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs \ - src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs \ - src/AR.Iec61850/Mms/MmsReportControlFieldCodec.cs \ - src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs \ - tests/AR.Iec61850.Tests/Mms/MmsReportControlFieldCodecTests.cs \ - tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs - git commit -m 'fix(reporting): enforce dchg and decode DBPOS close events [close-edge-applied]' - git push origin HEAD:audit/close-edge-report-projection diff --git a/.github/workflows/export-report-source.yml b/.github/workflows/export-report-source.yml deleted file mode 100644 index bc750bb..0000000 --- a/.github/workflows/export-report-source.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Apply close-edge report correction - -on: - pull_request: - branches: [ main ] - -permissions: - contents: write - pull-requests: read - -jobs: - apply: - runs-on: ubuntu-latest - steps: - - name: Checkout fix branch - uses: actions/checkout@v4 - with: - ref: fix/close-edge-event-reporting - fetch-depth: 0 - - - name: Apply reviewed source patch - run: | - git apply --check patches/close-edge-remaining.patch - git apply patches/close-edge-remaining.patch - - - name: Run focused report tests - run: dotnet test tests/AR.Iec61850.Tests/AR.Iec61850.Tests.csproj --configuration Release --filter "FullyQualifiedName~MmsReportControlFieldCodecTests|FullyQualifiedName~MmsReportValueProjectorTests" - - - name: Commit corrected report pipeline - run: | - git rm .github/workflows/export-report-source.yml - git rm .github/workflows/apply-close-edge-report-fix.yml - git rm patches/close-edge-remaining.patch - git rm docs/close-edge-report-audit.md docs/close-edge-report-audit-status.md docs/close-edge-report-fix-ready.md docs/trigger-close-edge-apply.txt - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs \ - src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs \ - src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs \ - tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs - git commit -m 'fix(reporting): enforce dchg and decode DBPOS close events' - git push origin HEAD:fix/close-edge-event-reporting diff --git a/docs/close-edge-report-audit-status.md b/docs/close-edge-report-audit-status.md deleted file mode 100644 index 8bf78c3..0000000 --- a/docs/close-edge-report-audit-status.md +++ /dev/null @@ -1 +0,0 @@ -Audit initiated from ArIED field diagnostics where Open events projected promptly but Closed transitions were only found by MMS validation. diff --git a/docs/close-edge-report-audit.md b/docs/close-edge-report-audit.md deleted file mode 100644 index d0dfb4d..0000000 --- a/docs/close-edge-report-audit.md +++ /dev/null @@ -1,3 +0,0 @@ -# Close-edge report audit - -Temporary audit branch used to inspect and correct report projection for asymmetric DBPOS Open/Closed changes observed by ArIED 61850. diff --git a/docs/close-edge-report-fix-ready.md b/docs/close-edge-report-fix-ready.md deleted file mode 100644 index 7166186..0000000 --- a/docs/close-edge-report-fix-ready.md +++ /dev/null @@ -1 +0,0 @@ -The reviewed close-edge reporting patch is ready for focused CI application. diff --git a/docs/trigger-close-edge-apply.txt b/docs/trigger-close-edge-apply.txt deleted file mode 100644 index 5c33b15..0000000 --- a/docs/trigger-close-edge-apply.txt +++ /dev/null @@ -1 +0,0 @@ -trigger diff --git a/patches/close-edge-remaining.patch b/patches/close-edge-remaining.patch deleted file mode 100644 index c4006ce..0000000 --- a/patches/close-edge-remaining.patch +++ /dev/null @@ -1,146 +0,0 @@ -diff --git a/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs b/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs -index 68df6ad..e7d283c 100644 ---- a/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs -+++ b/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs -@@ -576,6 +576,30 @@ public static class Iec61850EnumValueDecoder - } - if (value.Kind == MmsDataKind.Boolean) - return value.Value is bool b ? b ? 1 : 0 : null; -+ if (value.Kind == MmsDataKind.BitString) -+ { -+ var encoded = value.RawValue.ToArray(); -+ if (encoded.Length < 2 || encoded[0] > 7) -+ return null; -+ -+ var bitCount = checked((encoded.Length - 1) * 8 - encoded[0]); -+ if (bitCount is <= 0 or > 63) -+ return null; -+ -+ ulong numeric = 0; -+ for (var encodedBit = 0; encodedBit < bitCount; encodedBit++) -+ { -+ var byteIndex = 1 + encodedBit / 8; -+ var bitInByte = encodedBit % 8; -+ if ((encoded[byteIndex] & (0x80 >> bitInByte)) == 0) -+ continue; -+ -+ var numericBit = bitCount - 1 - encodedBit; -+ numeric |= 1UL << numericBit; -+ } -+ -+ return numeric <= long.MaxValue ? (long)numeric : null; -+ } - return null; - } - } -diff --git a/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs b/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs -index c63a8ba..b09f7b4 100644 ---- a/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs -+++ b/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs -@@ -182,6 +182,51 @@ public sealed partial class MmsClientSession - dataSetSnapshots.Add(dataSetBefore); - } - -+ if (isDynamic) -+ { -+ if (!rcb.Attributes.Contains("TrgOps", StringComparer.OrdinalIgnoreCase) || -+ !MmsReportControlFieldCodec.TryEncodeTriggerOptions(rcb.TriggerOptions, out var triggerOptions)) -+ { -+ return new MmsPersistentReportMonitorStartResult -+ { -+ IsSuccess = false, -+ WriteSteps = writes, -+ Warnings = warnings, -+ RcbSnapshots = rcbSnapshots, -+ DataSetSnapshots = dataSetSnapshots, -+ Message = "Dynamic report monitor requires a writable TrgOps field with explicit dchg trigger configuration." -+ }; -+ } -+ -+ var triggerWrite = await WriteReportAttributeAsync(rcb, "TrgOps", triggerOptions, cancellationToken).ConfigureAwait(false); -+ writes.Add(triggerWrite); -+ if (!triggerWrite.IsSuccess) -+ { -+ return new MmsPersistentReportMonitorStartResult -+ { -+ IsSuccess = false, -+ WriteSteps = writes, -+ Warnings = warnings, -+ RcbSnapshots = rcbSnapshots, -+ DataSetSnapshots = dataSetSnapshots, -+ Message = "RCB.TrgOps write failed; dynamic reporting was not armed because dchg could not be guaranteed." -+ }; -+ } -+ -+ if (rcb.Attributes.Contains("OptFlds", StringComparer.OrdinalIgnoreCase) && -+ MmsReportControlFieldCodec.TryEncodeOptionalFields(rcb.OptionalFields, out var optionalFields)) -+ { -+ var optionalWrite = await WriteReportAttributeAsync(rcb, "OptFlds", optionalFields, cancellationToken).ConfigureAwait(false); -+ writes.Add(optionalWrite); -+ if (!optionalWrite.IsSuccess) -+ warnings.Add("RCB.OptFlds write failed. Reporting can continue, but report timestamp/reason diagnostics may be incomplete."); -+ } -+ else -+ { -+ warnings.Add("Dynamic RCB has no writable OptFlds mapping. Reporting can continue, but source timestamp/reason diagnostics may be incomplete."); -+ } -+ } -+ - if (rcb.Buffered && rcb.Attributes.Contains("ResvTms", StringComparer.OrdinalIgnoreCase)) - { - warnings.Add("BRCB ResvTms pre-reserve was skipped. This keeps the first monitor attach compatible with relays that accept ownership through RptEna=true."); -diff --git a/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs b/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs -index c2ce21a..93c0af2 100644 ---- a/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs -+++ b/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs -@@ -221,7 +221,7 @@ public static class MmsReportSubscriptionPlanner - [ - $"Create dynamic DataSet {dataSetReference} with {points.Count} resolved member(s).", - $"Write RCB.DatSet={dataSetReference} on free RCB {rcb.Reference}.", -- "Keep current OptFlds/TrgOps for first dynamic test unless the IED requires explicit configuration.", -+ "Write explicit TrgOps with dchg enabled and request diagnostic OptFlds before RptEna=true.", - rcb.Buffered ? "Do not pre-write BRCB ResvTms for first live tests; enable RptEna after DatSet is configured." : "Reserve URCB with Resv=true when supported.", - "Install report receiver/dispatcher before enabling RptEna.", - "Write RptEna=true, then write GI=true for first full refresh.", -diff --git a/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs b/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs -index 9aa51be..90819c9 100644 ---- a/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs -+++ b/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs -@@ -113,4 +113,37 @@ public sealed class MmsReportValueProjectorTests - Assert.False(update.HasTimestamp); - Assert.Equal("companion-only", update.ProjectionStatus); - } -+ -+ [Theory] -+ [InlineData(0x40, "off")] -+ [InlineData(0x80, "on")] -+ public void Project_Decodes_TwoBit_Dbpos_Report_For_Both_Directions(byte encoded, string expected) -+ { -+ var frame = new MmsReportFrame -+ { -+ ReceivedAt = DateTimeOffset.UtcNow, -+ Values = -+ [ -+ new MmsReportValue -+ { -+ Index = 0, -+ Member = new MmsDataSetDirectoryMember -+ { -+ UserReference = "LD0/XCBR1.Pos.stVal", -+ FunctionalConstraint = "ST" -+ }, -+ Value = MmsDataValue.BitString(6, [encoded]), -+ ReasonForInclusion = ["data-change"] -+ } -+ ] -+ }; -+ -+ var update = Assert.Single(MmsReportValueProjector.Project(frame).Updates); -+ -+ Assert.Equal("LD0/XCBR1.Pos.stVal", update.Reference); -+ Assert.Equal(expected, update.Value); -+ Assert.True(update.HasValue); -+ Assert.Equal("data-change", update.Reason); -+ } -+ - } diff --git a/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs b/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs index 68df6ad..e7d283c 100644 --- a/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs +++ b/src/AR.Iec61850/Binding/Iec61850ValueBindingEngine.cs @@ -576,6 +576,30 @@ public static string DecodeOriginCategory(MmsDataValue? value) } if (value.Kind == MmsDataKind.Boolean) return value.Value is bool b ? b ? 1 : 0 : null; + if (value.Kind == MmsDataKind.BitString) + { + var encoded = value.RawValue.ToArray(); + if (encoded.Length < 2 || encoded[0] > 7) + return null; + + var bitCount = checked((encoded.Length - 1) * 8 - encoded[0]); + if (bitCount is <= 0 or > 63) + return null; + + ulong numeric = 0; + for (var encodedBit = 0; encodedBit < bitCount; encodedBit++) + { + var byteIndex = 1 + encodedBit / 8; + var bitInByte = encodedBit % 8; + if ((encoded[byteIndex] & (0x80 >> bitInByte)) == 0) + continue; + + var numericBit = bitCount - 1 - encodedBit; + numeric |= 1UL << numericBit; + } + + return numeric <= long.MaxValue ? (long)numeric : null; + } return null; } } diff --git a/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs b/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs index c63a8ba..b09f7b4 100644 --- a/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs +++ b/src/AR.Iec61850/Mms/MmsPersistentReportMonitor.cs @@ -182,6 +182,51 @@ public async Task StartPersistentReportMo dataSetSnapshots.Add(dataSetBefore); } + if (isDynamic) + { + if (!rcb.Attributes.Contains("TrgOps", StringComparer.OrdinalIgnoreCase) || + !MmsReportControlFieldCodec.TryEncodeTriggerOptions(rcb.TriggerOptions, out var triggerOptions)) + { + return new MmsPersistentReportMonitorStartResult + { + IsSuccess = false, + WriteSteps = writes, + Warnings = warnings, + RcbSnapshots = rcbSnapshots, + DataSetSnapshots = dataSetSnapshots, + Message = "Dynamic report monitor requires a writable TrgOps field with explicit dchg trigger configuration." + }; + } + + var triggerWrite = await WriteReportAttributeAsync(rcb, "TrgOps", triggerOptions, cancellationToken).ConfigureAwait(false); + writes.Add(triggerWrite); + if (!triggerWrite.IsSuccess) + { + return new MmsPersistentReportMonitorStartResult + { + IsSuccess = false, + WriteSteps = writes, + Warnings = warnings, + RcbSnapshots = rcbSnapshots, + DataSetSnapshots = dataSetSnapshots, + Message = "RCB.TrgOps write failed; dynamic reporting was not armed because dchg could not be guaranteed." + }; + } + + if (rcb.Attributes.Contains("OptFlds", StringComparer.OrdinalIgnoreCase) && + MmsReportControlFieldCodec.TryEncodeOptionalFields(rcb.OptionalFields, out var optionalFields)) + { + var optionalWrite = await WriteReportAttributeAsync(rcb, "OptFlds", optionalFields, cancellationToken).ConfigureAwait(false); + writes.Add(optionalWrite); + if (!optionalWrite.IsSuccess) + warnings.Add("RCB.OptFlds write failed. Reporting can continue, but report timestamp/reason diagnostics may be incomplete."); + } + else + { + warnings.Add("Dynamic RCB has no writable OptFlds mapping. Reporting can continue, but source timestamp/reason diagnostics may be incomplete."); + } + } + if (rcb.Buffered && rcb.Attributes.Contains("ResvTms", StringComparer.OrdinalIgnoreCase)) { warnings.Add("BRCB ResvTms pre-reserve was skipped. This keeps the first monitor attach compatible with relays that accept ownership through RptEna=true."); diff --git a/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs b/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs index c2ce21a..93c0af2 100644 --- a/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs +++ b/src/AR.Iec61850/Mms/MmsReportSubscriptionPlan.cs @@ -221,7 +221,7 @@ private static IReadOnlyList BuildDynamicSteps(MmsReportControlCandidate [ $"Create dynamic DataSet {dataSetReference} with {points.Count} resolved member(s).", $"Write RCB.DatSet={dataSetReference} on free RCB {rcb.Reference}.", - "Keep current OptFlds/TrgOps for first dynamic test unless the IED requires explicit configuration.", + "Write explicit TrgOps with dchg enabled and request diagnostic OptFlds before RptEna=true.", rcb.Buffered ? "Do not pre-write BRCB ResvTms for first live tests; enable RptEna after DatSet is configured." : "Reserve URCB with Resv=true when supported.", "Install report receiver/dispatcher before enabling RptEna.", "Write RptEna=true, then write GI=true for first full refresh.", diff --git a/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs b/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs index 9aa51be..90819c9 100644 --- a/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs +++ b/tests/AR.Iec61850.Tests/Mms/MmsReportValueProjectorTests.cs @@ -113,4 +113,37 @@ public void Project_Preserves_Q_Only_Report_As_A_Partial_Companion_Update() Assert.False(update.HasTimestamp); Assert.Equal("companion-only", update.ProjectionStatus); } + + [Theory] + [InlineData(0x40, "off")] + [InlineData(0x80, "on")] + public void Project_Decodes_TwoBit_Dbpos_Report_For_Both_Directions(byte encoded, string expected) + { + var frame = new MmsReportFrame + { + ReceivedAt = DateTimeOffset.UtcNow, + Values = + [ + new MmsReportValue + { + Index = 0, + Member = new MmsDataSetDirectoryMember + { + UserReference = "LD0/XCBR1.Pos.stVal", + FunctionalConstraint = "ST" + }, + Value = MmsDataValue.BitString(6, [encoded]), + ReasonForInclusion = ["data-change"] + } + ] + }; + + var update = Assert.Single(MmsReportValueProjector.Project(frame).Updates); + + Assert.Equal("LD0/XCBR1.Pos.stVal", update.Reference); + Assert.Equal(expected, update.Value); + Assert.True(update.HasValue); + Assert.Equal("data-change", update.Reason); + } + } From 216eed8582c6e451f71e61c0a0537c71f58785cf Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:23:29 +0700 Subject: [PATCH 14/15] docs: add close-edge field verification --- docs/close-edge-event-reporting-verification.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/close-edge-event-reporting-verification.md diff --git a/docs/close-edge-event-reporting-verification.md b/docs/close-edge-event-reporting-verification.md new file mode 100644 index 0000000..c76fc6b --- /dev/null +++ b/docs/close-edge-event-reporting-verification.md @@ -0,0 +1,9 @@ +# Close-edge event reporting verification + +Field verification after merge: + +1. Arm dynamic monitoring and confirm the selected RCB write log includes `TrgOps` with `dchg` before `RptEna=true`. +2. Alternate Open and Close commands while watching IEDScout and ArIED Live Monitor. +3. Both directions must update from the report path without waiting for MMS validation fallback. +4. The warning `value change not delivered by the armed report` must not recur for the position point. +5. If the IED rejects `TrgOps`, monitoring must report the configuration failure instead of claiming event-driven acquisition. From 5ec16a5e70aa12030fb8c7ee0ae212bbdd40ab13 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 14 Jul 2026 04:26:10 +0700 Subject: [PATCH 15/15] ci: keep engine source tree hygiene clean --- docs/close-edge-event-reporting-verification.md | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 docs/close-edge-event-reporting-verification.md diff --git a/docs/close-edge-event-reporting-verification.md b/docs/close-edge-event-reporting-verification.md deleted file mode 100644 index c76fc6b..0000000 --- a/docs/close-edge-event-reporting-verification.md +++ /dev/null @@ -1,9 +0,0 @@ -# Close-edge event reporting verification - -Field verification after merge: - -1. Arm dynamic monitoring and confirm the selected RCB write log includes `TrgOps` with `dchg` before `RptEna=true`. -2. Alternate Open and Close commands while watching IEDScout and ArIED Live Monitor. -3. Both directions must update from the report path without waiting for MMS validation fallback. -4. The warning `value change not delivered by the armed report` must not recur for the position point. -5. If the IED rejects `TrgOps`, monitoring must report the configuration failure instead of claiming event-driven acquisition.