diff --git a/Services/IedBrowserReportWireEvidence.cs b/Services/IedBrowserReportWireEvidence.cs index 10eb1aeaa..9b46d1701 100644 --- a/Services/IedBrowserReportWireEvidence.cs +++ b/Services/IedBrowserReportWireEvidence.cs @@ -21,6 +21,10 @@ public static IReadOnlyList Extract(MmsReportFra { var reference = IedBrowserLiveValues.CanonicalReference(reported.MemberReference); var fc = reported.Member?.FunctionalConstraint ?? ""; + // DataRef is optional. If present it must agree with the verified + // DataSet member AND explicit FC, or the raw q/t is quarantined. + if (!DataReferenceAgreesWithMember(reported.DataReference, reference, fc)) + continue; var payload = reported.Value; if (reported.FailureCode.HasValue || payload is null || string.IsNullOrWhiteSpace(reference) || string.IsNullOrWhiteSpace(fc)) @@ -57,6 +61,27 @@ public static IReadOnlyList Extract(MmsReportFra return result; } + + private static bool DataReferenceAgreesWithMember( + string? dataReference, string memberReference, string memberFc) + { + if (string.IsNullOrWhiteSpace(dataReference)) + return true; + var wire = dataReference.Trim(); + if (!string.Equals(IedBrowserLiveValues.CanonicalReference(wire), + memberReference, StringComparison.Ordinal)) + return false; + // The canonical ledger deliberately drops the MMS $FC$ segment. + // Recheck this separately to reject cross-FC alias collisions. + var fcStart = wire.IndexOf('$'); + if (fcStart < 0) + return true; + var fcEnd = wire.IndexOf('$', fcStart + 1); + return fcEnd > fcStart + 1 && + string.Equals(wire[(fcStart + 1)..fcEnd], memberFc, + StringComparison.OrdinalIgnoreCase); + } + private static MmsQualityEvidence? DecodeQuality(MmsDataValue raw) => raw.Kind == MmsDataKind.BitString && MmsQualityDecoder.TryDecode(raw.RawValue.ToArray(), out var decoded) diff --git a/tests/ARSAS.Tests/M214ReportQualityDataRefIdentityTests.cs b/tests/ARSAS.Tests/M214ReportQualityDataRefIdentityTests.cs new file mode 100644 index 000000000..60e5d102c --- /dev/null +++ b/tests/ARSAS.Tests/M214ReportQualityDataRefIdentityTests.cs @@ -0,0 +1,79 @@ +using AR.Iec61850.Mms; +using ArIED61850Tester.Services; + +namespace ARSAS.Tests; + +public sealed class M214ReportQualityDataRefIdentityTests +{ + private const string QualityReference = "LD/CSWI1.Pos.q"; + + private static MmsReportFrame Frame( + string? dataReference, + string memberReference = QualityReference) + => new() + { + Values = + [ + new MmsReportValue + { + Index = 0, + Member = new MmsDataSetDirectoryMember + { + UserReference = memberReference, + FunctionalConstraint = "ST" + }, + DataReference = dataReference ?? "", + // IEC 61850 Quality: Overflow, OutOfRange, OldData and + // Inaccurate set, while Source remains process. + Value = MmsDataValue.BitString(3, new byte[] { 0x31, 0x40 }) + } + ] + }; + + [Theory] + [InlineData("LD/CSWI1.Pos.q")] + [InlineData("LD/CSWI1$ST$Pos$q")] + [InlineData(null)] + public void ExactOrAbsentOptionalDataRef_PreservesActualRawQuality(string? dataReference) + { + var raw = Assert.Single(IedBrowserReportWireEvidence.Extract(Frame(dataReference))); + Assert.Equal(QualityReference, raw.CarrierReference); + Assert.Equal("ST", raw.FunctionalConstraint); + var quality = Assert.IsType(raw.QualityBits); + Assert.Equal("true", quality.FormatField("Overflow")); + Assert.Equal("true", quality.FormatField("OutOfRange")); + Assert.Equal("true", quality.FormatField("OldData")); + Assert.Equal("true", quality.FormatField("Inaccurate")); + Assert.Equal("process", quality.FormatField("Source")); + } + + [Theory] + [InlineData("LD/CSWI1.Mod.q")] + [InlineData("OTHER/CSWI1.Pos.q")] + [InlineData("LD/CSWI2.Pos.q")] + [InlineData("LD/CSWI1$MX$Pos$q")] + public void ConflictingOnWireDataRef_CannotAttributeBitsToAnotherObject( + string contradictoryReference) + { + Assert.Empty(IedBrowserReportWireEvidence.Extract( + Frame(contradictoryReference))); + } + + [Fact] + public void MissingMemberIdentity_CannotBeRepairedFromOnWireText() + { + var frame = new MmsReportFrame + { + Values = + [ + new MmsReportValue + { + Index = 0, + DataReference = "LD/CSWI1$ST$Pos$q", + Value = MmsDataValue.BitString(3, new byte[] { 0x31, 0x40 }) + } + ] + }; + Assert.Empty(IedBrowserReportWireEvidence.Extract(frame)); + } +}