-
Notifications
You must be signed in to change notification settings - Fork 3
Fix event-driven Close feedback for dynamic reports #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4c13ca1
0ac8ce7
f8deaf5
9fe5edc
05cece2
943e93a
291cc7f
0dc048e
7814141
5d30585
d880a4f
655900c
63404db
216eed8
5ec16a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| namespace AR.Iec61850.Mms; | ||
|
|
||
| /// <summary> | ||
| /// 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). | ||
| /// </summary> | ||
| public static class MmsReportControlFieldCodec | ||
| { | ||
| private static readonly IReadOnlyDictionary<string, int> TriggerOptionBits = | ||
| new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) | ||
| { | ||
| ["dchg"] = 0, | ||
| ["data-change"] = 0, | ||
| ["datachange"] = 0, | ||
|
Comment on lines
+12
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When dynamic monitoring writes Useful? React with 👍 / 👎. |
||
| ["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<string, int> OptionalFieldBits = | ||
| new Dictionary<string, int>(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, | ||
|
Comment on lines
+33
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new encoder silently drops unknown optional-field tokens while still writing any recognized ones, but existing discovery/simulator text uses aliases such as Useful? React with 👍 / 👎. |
||
| ["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<string, int> map, | ||
| int bitCount, | ||
| out MmsDataValue value) | ||
| { | ||
| value = MmsDataValue.BitString((byte)((8 - bitCount % 8) % 8), ReadOnlySpan<byte>.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<string> Tokenize(string? text) | ||
| => (text ?? string.Empty) | ||
| .Split(new[] { ' ', ',', ';', '|', '+', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) | ||
| .Select(token => token.Trim().Trim('[', ']', '(', ')').ToLowerInvariant()); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In dynamic mode this check runs after the temporary DataSet has already been created and
RCB.DatSethas already been written, but this failure path returns without registering a session, deleting the DataSet, or restoring the originalDatSet. Any relay whoseTrgOpsis missing or whose current value cannot be tokenized will be left with the temporary DataSet bound even though start reports failure.Useful? React with 👍 / 👎.