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..92ff742 --- /dev/null +++ b/.github/workflows/apply-close-edge-report-fix.yml @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..3825b18 --- /dev/null +++ b/.github/workflows/export-report-source.yml @@ -0,0 +1,47 @@ +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 audit branch + uses: actions/checkout@v4 + with: + ref: audit/close-edge-report-projection + 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 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. 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. 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. 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 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()); +} 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); + } +}