-
Notifications
You must be signed in to change notification settings - Fork 3
G2.7 bound native Dynamic RCB groups to proven G2.3 envelope #110
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
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,65 @@ | ||
| using AR.Iec61850.Acse; | ||
| using AR.Iec61850.Discovery; | ||
|
|
||
| namespace AR.Iec61850.Mms; | ||
|
|
||
| /// <summary> | ||
| /// Field-stability wrapper for the P1.7 native per-IED capability runtime. | ||
| /// | ||
| /// A native dchg + cleanup witness proves that dynamic reporting may be generalized to | ||
| /// fresh exact-resolved members, but it does not prove that a larger NamedVariableList than | ||
| /// the G2.3 accepted envelope is safe for the IED association. Runtime therefore keeps the | ||
| /// general member-capability semantics while capping each individual Dynamic DataSet to the | ||
| /// physically proven G2.3 member-count envelope. | ||
| /// | ||
| /// This planner does not alter identity/witness authorization, RCB availability, deterministic | ||
| /// AR_HYB identity, ProductionEligible state, or polling fallback. Those remain owned by the | ||
| /// existing stable P1.7 planner and its downstream hybrid planner. | ||
| /// </summary> | ||
| public static class MmsGuardedDynamicReportNativeFieldCapabilityEnvelopeBoundRuntimePlanner | ||
| { | ||
| public static MmsCapabilityAwareHybridReportAcquisitionPlan Build( | ||
| Iec61850SignalCatalogDocument catalog, | ||
| IEnumerable<Iec61850SignalDescriptor> requestedSignals, | ||
| MmsReportInventory inventory, | ||
| MmsRcbAvailabilityResult availability, | ||
| MmsIedModelDirectory liveDirectory, | ||
| AcseMmsNegotiatedCapabilities? negotiatedCapabilities, | ||
| MmsHybridReportAcquisitionOptions? options, | ||
| MmsDynamicReportGuardedRuntimePlanningContext sourceContext, | ||
| MmsDynamicReportNativeFieldCapabilityEvidence evidence) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(sourceContext); | ||
| options ??= new MmsHybridReportAcquisitionOptions(); | ||
|
|
||
| var provenEnvelopeMembers = sourceContext.Profile.ProvenSafeMemberCount; | ||
| var boundedMembers = provenEnvelopeMembers > 1 | ||
| ? Math.Min(options.MaxDynamicMembersPerReport, provenEnvelopeMembers) | ||
| : options.MaxDynamicMembersPerReport; | ||
|
Comment on lines
+36
to
+38
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 an application supplies a deserialized or manually constructed Useful? React with 👍 / 👎. |
||
|
|
||
| var boundedOptions = new MmsHybridReportAcquisitionOptions | ||
| { | ||
| MaxStaticReportPlans = options.MaxStaticReportPlans, | ||
| MaxDynamicReportPlans = options.MaxDynamicReportPlans, | ||
| MaxDynamicMembersPerReport = boundedMembers, | ||
| RequireExactAvailabilityEvidence = options.RequireExactAvailabilityEvidence, | ||
| AllowCallerOwnedReports = options.AllowCallerOwnedReports, | ||
| AllowStaticBrcb = options.AllowStaticBrcb, | ||
| AllowStaticUrcb = options.AllowStaticUrcb, | ||
| AllowDynamicBrcb = options.AllowDynamicBrcb, | ||
| AllowDynamicUrcb = options.AllowDynamicUrcb, | ||
| AllowPollingFallback = options.AllowPollingFallback | ||
| }; | ||
|
|
||
| return MmsGuardedDynamicReportNativeFieldCapabilityStableRuntimePlanner.Build( | ||
| catalog, | ||
| requestedSignals, | ||
| inventory, | ||
| availability, | ||
| liveDirectory, | ||
| negotiatedCapabilities, | ||
| boundedOptions, | ||
| sourceContext, | ||
| evidence); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,252 @@ | ||
| using AR.Iec61850.Acse; | ||
| using AR.Iec61850.Discovery; | ||
| using AR.Iec61850.Mms; | ||
|
|
||
| namespace AR.Iec61850.Tests.Mms; | ||
|
|
||
| public sealed class MmsG27NativeEnvelopeBoundRuntimeTests | ||
| { | ||
| [Fact] | ||
| public void NativeRuntime_GenericLimitAbovePhysicalEnvelope_UsesMoreBoundedGroupsInsteadOfOversizedDataSet() | ||
| { | ||
| var signals = Enumerable.Range(1, 5).Select(Signal).ToArray(); | ||
| var profile = NativeProfile(); | ||
| var context = new MmsDynamicReportGuardedRuntimePlanningContext | ||
| { | ||
| Profile = profile, | ||
| CurrentIdentity = Identity() | ||
| }; | ||
|
|
||
| var plan = MmsGuardedDynamicReportNativeFieldCapabilityEnvelopeBoundRuntimePlanner.Build( | ||
| new Iec61850SignalCatalogDocument | ||
| { | ||
| IedName = "G27_BOUND_IED", | ||
| Source = "P1.7 envelope-bound runtime fixture", | ||
| Signals = signals | ||
| }, | ||
| signals, | ||
| DynamicInventory(1, 2, 3), | ||
| DynamicAvailability(1, 2, 3), | ||
| Directory(1, 2, 3, 4, 5), | ||
| new AcseMmsNegotiatedCapabilities | ||
| { | ||
| IsDecoded = true, | ||
| SupportsWrite = true, | ||
| SupportsDefineNamedVariableList = true, | ||
| SupportsDeleteNamedVariableList = true | ||
| }, | ||
| new MmsHybridReportAcquisitionOptions | ||
| { | ||
| MaxDynamicReportPlans = 8, | ||
| MaxDynamicMembersPerReport = 64, | ||
| AllowStaticBrcb = false, | ||
| AllowStaticUrcb = false, | ||
| AllowDynamicBrcb = false, | ||
| AllowDynamicUrcb = true, | ||
| AllowCallerOwnedReports = false, | ||
| AllowPollingFallback = true, | ||
| RequireExactAvailabilityEvidence = true | ||
| }, | ||
| context, | ||
| NativeEvidence()); | ||
|
|
||
| Assert.False(plan.AutomaticDynamicActivationQuarantined); | ||
| Assert.Equal(5, plan.AcquisitionPlan.DynamicUrcbSignalCount); | ||
| Assert.Equal(0, plan.AcquisitionPlan.PollingFallbackSignalCount); | ||
|
|
||
| var groups = plan.AcquisitionPlan.Segments | ||
| .Where(segment => segment.Kind == MmsHybridAcquisitionKind.DynamicUrcb) | ||
| .ToArray(); | ||
|
|
||
| Assert.Equal(3, groups.Length); | ||
| Assert.Equal(new[] { 2, 2, 1 }, groups.Select(group => group.Signals.Count).ToArray()); | ||
| Assert.All(groups, group => Assert.True(group.Signals.Count <= profile.ProvenSafeMemberCount)); | ||
| Assert.All(groups, group => Assert.Contains("AR_HYB_", group.DataSetReference, StringComparison.OrdinalIgnoreCase)); | ||
| } | ||
|
|
||
| private static MmsDynamicReportQualificationProfile NativeProfile() | ||
| { | ||
| var members = new[] { Member(1), Member(2) }; | ||
| var assessment = MmsDynamicDataSetQualificationLadder.Assess( | ||
| [ | ||
| new MmsDynamicDataSetQualificationAttemptEvidence | ||
| { | ||
| AttemptId = "g27-bound-envelope", | ||
| ObservedAtUtc = Time(1), | ||
| DataSetReference = ProvenDataSetReference, | ||
| MemberReferences = members, | ||
| DefineRequestByteCount = 220, | ||
| NegotiatedMaxMmsPduSize = 65000, | ||
| RequestWithinKnownNegotiatedPdu = true, | ||
| IsSuccess = true, | ||
| FailureStage = MmsDynamicDataSetQualificationFailureStage.None, | ||
| DynamicMutationAttempted = true, | ||
| AssociationSurvived = true, | ||
| CleanupSucceeded = true | ||
| } | ||
| ]); | ||
| var envelope = MmsDynamicDataSetQualificationLadder.AcceptExactEnvelope(assessment, "g27-bound-envelope"); | ||
| var profile = MmsDynamicReportQualificationProfilePolicy.CreateEnvelopeQualifiedProfile( | ||
| Identity(), envelope, assessment, null, "g27-bound-envelope-evidence", Time(2)); | ||
|
|
||
| var activated = MmsDynamicReportQualificationProfilePolicy.RecordRcbActivationProof( | ||
| profile, | ||
| Identity(), | ||
| new MmsDynamicRcbActivationProof | ||
| { | ||
| EvidenceId = ActivationEvidenceId, | ||
| ObservedAtUtc = Time(3), | ||
| RcbReference = DynamicRcb(1), | ||
| DataSetReference = ProvenDataSetReference, | ||
| MemberReferences = members, | ||
| FreshRcbAvailabilityVerified = true, | ||
| DataSetReadbackVerified = true, | ||
| RcbDataSetBindingAccepted = true, | ||
| RptEnaAccepted = true, | ||
| AssociationHealthyAfterActivation = true | ||
| }); | ||
|
|
||
| return MmsDynamicReportQualificationProfilePolicy.RecordInformationReportProof( | ||
| activated, | ||
| Identity(), | ||
| new MmsDynamicInformationReportProof | ||
| { | ||
| EvidenceId = ReportEvidenceId, | ||
| ObservedAtUtc = Time(4), | ||
| RcbReference = DynamicRcb(1), | ||
| DataSetReference = ProvenDataSetReference, | ||
| MemberReferences = members, | ||
| Kind = MmsDynamicInformationReportKind.DataChange, | ||
| ActualInformationReportReceived = true, | ||
| ReportIdentityVerified = true, | ||
| ExactMemberMappingVerified = true, | ||
| AssociationHealthyAfterReport = true, | ||
| ReportAuthoritativePointCount = 1 | ||
| }); | ||
| } | ||
|
|
||
| private static MmsDynamicReportNativeFieldCapabilityEvidence NativeEvidence() | ||
| => new() | ||
| { | ||
| EvidenceId = "g27-bound-native-witness", | ||
| ObservedAtUtc = Time(5), | ||
| StableIdentityKey = Identity().StableIdentityKey, | ||
| ModelFingerprint = Identity().ModelFingerprint, | ||
| ProfileRevision = Identity().ProfileRevision, | ||
| RcbReference = DynamicRcb(1), | ||
| DataSetReference = ProvenDataSetReference, | ||
| RcbActivationEvidenceId = ActivationEvidenceId, | ||
| InformationReportEvidenceId = ReportEvidenceId, | ||
| IncludedMemberReferences = [Member(1)], | ||
| ActualInformationReportReceived = true, | ||
| DataChangeReasonVerified = true, | ||
| GeneralInterrogationDisabled = true, | ||
| ExactMemberMappingVerified = true, | ||
| AssociationHealthyAfterReport = true, | ||
| MonitorCleanupSucceeded = true, | ||
| ProofFieldRestoreSucceeded = true, | ||
| FreshCleanupClosureSucceeded = true | ||
| }; | ||
|
|
||
| private static MmsDynamicReportIedIdentity Identity() | ||
| => new() | ||
| { | ||
| StableIdentityKey = "ied:g27:bound", | ||
| ModelFingerprint = "sha256:g27-bound-model", | ||
| Manufacturer = "Example", | ||
| Model = "BoundIED", | ||
| FirmwareRevision = "1.0.0", | ||
| ProfileRevision = "cfg-bound" | ||
| }; | ||
|
|
||
| private static MmsReportInventory DynamicInventory(params int[] indexes) | ||
| { | ||
| var inventory = new MmsReportInventory(); | ||
| foreach (var index in indexes) | ||
| { | ||
| inventory.ReportControls.Add(new MmsReportControlCandidate | ||
| { | ||
| Domain = "LD0", | ||
| LogicalNode = "LLN0", | ||
| FunctionalConstraint = "RP", | ||
| Name = $"Unbuffer{index:00}", | ||
| Reference = DynamicRcb(index), | ||
| Buffered = false, | ||
| DataSetReference = string.Empty, | ||
| DataSetProbeState = MmsRcbDataSetProbeState.ReadSucceeded, | ||
| EnabledState = "false", | ||
| ReservationState = "false", | ||
| TriggerOptions = "dchg", | ||
| ReportId = $"Unbuffer{index:00}", | ||
| ConfRev = "1" | ||
| }); | ||
| } | ||
| return inventory; | ||
| } | ||
|
|
||
| private static MmsRcbAvailabilityResult DynamicAvailability(params int[] indexes) | ||
| => new() | ||
| { | ||
| CheckedAtUtc = Time(10), | ||
| ReportControls = indexes.Select(index => new MmsRcbAvailabilitySnapshot | ||
| { | ||
| Reference = DynamicRcb(index), | ||
| Domain = "LD0", | ||
| LogicalNode = "LLN0", | ||
| Name = $"Unbuffer{index:00}", | ||
| Mode = "URCB", | ||
| Buffered = false, | ||
| DataSetReference = string.Empty, | ||
| DataSetProbeState = MmsRcbDataSetProbeState.ReadSucceeded, | ||
| ReportId = $"Unbuffer{index:00}", | ||
| ConfRev = "1", | ||
| EnabledState = "false", | ||
| ReservationState = "false", | ||
| TriggerOptions = "dchg", | ||
| DataSetDirectoryRead = false, | ||
| DataSetDirectorySuccess = false, | ||
| DataSetMemberCount = 0, | ||
| DataSetMembers = Array.Empty<MmsDataSetDirectoryMember>(), | ||
| Availability = MmsRcbOperationalAvailability.NoDataSet, | ||
| Confidence = MmsRcbAvailabilityConfidence.Exact, | ||
| Reason = "P1.7 exact empty URCB fixture", | ||
| Attributes = ["DatSet", "RptEna", "TrgOps", "OptFlds", "Resv"] | ||
| }).ToArray() | ||
| }; | ||
|
|
||
| private static MmsIedModelDirectory Directory(params int[] indexes) | ||
| => new(indexes.Select(index => new MmsFcResolvedPoint | ||
| { | ||
| Domain = "LD0", | ||
| LogicalNode = "GGIO1", | ||
| FunctionalConstraint = "ST", | ||
| DataObjectPath = $"Ind{index}.stVal", | ||
| MmsItemName = $"GGIO1$ST$Ind{index}$stVal", | ||
| Source = "P1.7 synthetic live directory", | ||
| Confidence = 100 | ||
| })); | ||
|
|
||
| private static Iec61850SignalDescriptor Signal(int index) | ||
| => new() | ||
| { | ||
| DesignReference = $"LD0/GGIO1.Ind{index}.stVal", | ||
| ObservedReference = $"LD0/GGIO1.Ind{index}.stVal", | ||
| CanonicalMmsReference = Member(index), | ||
| EffectiveMmsReference = Member(index), | ||
| PrimaryValueReference = $"LD0/GGIO1.Ind{index}.stVal", | ||
| PrimaryValueMmsReference = Member(index), | ||
| FunctionalConstraint = "ST", | ||
| SemanticRole = Iec61850DataAttributeSemanticRole.PrimaryValue, | ||
| IsOperationalCandidate = true, | ||
| ResolutionStatus = Iec61850SignalCatalogResolutionStatus.DesignAttribute, | ||
| LiveStatus = Iec61850DesignLiveStatus.Exact | ||
| }; | ||
|
|
||
| private static string DynamicRcb(int index) => $"LD0/LLN0.RP.Unbuffer{index:00}"; | ||
| private static string Member(int index) => $"LD0/GGIO1$ST$Ind{index}$stVal"; | ||
| private static DateTimeOffset Time(int minutes) => DateTimeOffset.Parse("2026-09-03T00:00:00Z").AddMinutes(minutes); | ||
|
|
||
| private const string ProvenDataSetReference = "LD0/LLN0.AR_G27_BOUND"; | ||
| private const string ActivationEvidenceId = "g27-bound-activation"; | ||
| private const string ReportEvidenceId = "g27-bound-dchg-report"; | ||
| } |
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.
This adds a public protocol runtime and unit-tested G2.7 behavior without updating either the engine maturity matrix or changelog, leaving the documented reporting capability and its evidence boundary stale. Record the implemented/unit-tested scope and what remains unvalidated as required for meaningful completed patches.
AGENTS.md reference: AGENTS.md:L119-L123
Useful? React with 👍 / 👎.