Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
1a9806c
fix(reporting): add typed RCB scalar mutation contract (#144)
masarray Oct 7, 2026
eef0614
fix(reporting): route RCB scalar writes through semantic contract (#144)
masarray Oct 7, 2026
18f8ae2
fix(reporting): route RCB scalar writes through semantic contract (#144)
masarray Oct 7, 2026
1bd8813
fix(reporting): route RCB scalar writes through semantic contract (#144)
masarray Oct 7, 2026
7fd2fa6
test(reporting): lock RCB semantic wire types (#144)
masarray Oct 7, 2026
6f66dd9
fix(reporting): complete scalar semantic write migration (#144)
masarray Oct 7, 2026
23a4b45
fix(reporting): complete scalar semantic write migration (#144)
masarray Oct 7, 2026
488e678
fix(reporting): keep ResvTms codec semantic-neutral within INT16 (#144)
masarray Oct 7, 2026
cebd6b2
test(reporting): cover full signed ResvTms INT16 representation (#144)
masarray Oct 7, 2026
5d301be
refactor(reporting): converge SCL and discovery on configured static …
masarray Oct 7, 2026
20c7b40
test(reporting): lock source-neutral configured static entry point (#…
masarray Oct 7, 2026
c5989b0
feat(reporting): add bounded live type evidence for RCB scalar mutati…
masarray Oct 7, 2026
338cd4f
feat(reporting): preserve typed write failure evidence (#144)
masarray Oct 7, 2026
794ebea
fix(reporting): scope semantic type evidence to one association (#144)
masarray Oct 7, 2026
c386bb8
test(reporting): distinguish missing from conflicting type evidence (…
masarray Oct 7, 2026
3e94749
fix(reporting): preserve exact-case identity in type-evidence cache (…
masarray Oct 7, 2026
34964f5
feat(mms): classify write failures from structured DataAccessError ev…
masarray Oct 7, 2026
fbedf54
feat(reporting): carry structured interoperability failure evidence (…
masarray Oct 7, 2026
b201497
refactor(reporting): drive type recovery from structured failure clas…
masarray Oct 7, 2026
6b81b9c
test(mms): lock structured interoperability recovery classification (…
masarray Oct 7, 2026
4e6b521
feat(canonical): add source-neutral static report coverage resolver (…
masarray Oct 7, 2026
13b84fe
test(canonical): prove source-neutral static coverage and ordered Dat…
masarray Oct 7, 2026
ac73e5e
chore(canonical): keep static coverage resolver allocation-minimal (#…
masarray Oct 7, 2026
9cfc1ae
fix(canonical): converge SCL and live configured reference syntax in …
masarray Oct 7, 2026
c8addff
test(canonical): prove Discovery and Open-SCL builder coverage conver…
masarray Oct 7, 2026
fab541d
fix(canonical): repair configured reference normalizer syntax (#144)
masarray Oct 7, 2026
8f8b024
feat(reporting): model reduced configured-static RCB eligibility (#144)
masarray Oct 7, 2026
c04c07b
feat(reporting): gate reduced configured-static eligibility explicitl…
masarray Oct 7, 2026
beda723
fix(reporting): preserve existing reduced-confidence explicit-free se…
masarray Oct 7, 2026
ba392fe
fix(reporting): rank explicit-free static eligibility without changin…
masarray Oct 7, 2026
6bdb6b7
test(reporting): lock missing-vs-blocking static RCB evidence policy …
masarray Oct 7, 2026
e1d4d34
test(reporting): prove reduced static policy is explicit and static-f…
masarray Oct 7, 2026
70e6e9e
test(reporting): keep reduced static interoperability disabled by def…
masarray Oct 7, 2026
4cc9b49
fix(reporting): route fresh static hybrid plans through configured li…
masarray Oct 7, 2026
4a915bf
feat(diagnostics): surface typed RCB mutation failure evidence (#144)
masarray Oct 7, 2026
45e8855
test(diagnostics): lock typed RCB interoperability counters (#144)
masarray Oct 7, 2026
815d226
feat(reporting): classify static activation failures from typed evide…
masarray Oct 7, 2026
706ad53
refactor(canonical): normalize configured DataSet and RCB identity at…
masarray Oct 7, 2026
18d0ac1
test(canonical): require configured identity equality across Discover…
masarray Oct 7, 2026
302f788
fix(canonical): repair configured-reference ingress normalizer after …
masarray Oct 7, 2026
ca41ca2
feat(reporting): add JIT configured-static readback proof policy (#144)
masarray Oct 7, 2026
c2c2799
fix(reporting): fail closed when configured-static RptEna readback is…
masarray Oct 7, 2026
ad6bfc5
test(reporting): lock configured-static JIT readback proof contract (…
masarray Oct 7, 2026
97363b9
perf(reporting): add minimal JIT RCB snapshot fast path (#144)
masarray Oct 7, 2026
ac6094e
feat(reporting): retain exact JIT RCB state evidence in snapshots (#144)
masarray Oct 7, 2026
7c6f3f6
perf(reporting): prove configured static activation with one exact re…
masarray Oct 7, 2026
86915ca
perf(reporting): use minimal JIT preflight and single exact post-enab…
masarray Oct 7, 2026
f7675a6
test(reporting): lock minimal one-readback configured-static proof (#…
masarray Oct 7, 2026
12bb6bb
test(reporting): fix busy-evidence fixture construction (#144)
masarray Oct 7, 2026
327d499
ci(#144): overlay targeted hot-path slice
masarray Oct 7, 2026
048d39d
ci(#144): overlay targeted hot-path slice
masarray Oct 7, 2026
1b64dfb
ci(#144): overlay targeted hot-path slice
masarray Oct 7, 2026
b33ce7f
ci(#144): overlay targeted hot-path slice
masarray Oct 7, 2026
7ad1193
ci(#144): overlay targeted hot-path slice
masarray Oct 7, 2026
e78f007
ci(#144): overlay targeted hot-path slice
masarray Oct 7, 2026
d129faa
ci(#144): overlay targeted hot-path slice
masarray Oct 7, 2026
1722093
ci(#144): refresh targeted hot-path read-budget fixes
masarray Oct 7, 2026
bad81df
ci(#144): refresh targeted hot-path read-budget fixes
masarray Oct 7, 2026
1a033ab
ci(#144): refresh structure type-prewarm optimization
masarray Oct 7, 2026
7cef117
ci(#144): refresh structure type-prewarm optimization
masarray Oct 7, 2026
a39e932
ci(#144): overlay asynchronous armed-to-traffic proof state
masarray Oct 7, 2026
f5e8a45
ci(#144): overlay canonical-to-live RCB target resolver
masarray Oct 7, 2026
7f7328e
ci(#144): overlay canonical-to-live RCB target resolver
masarray Oct 7, 2026
4c31f11
ci(#144): refresh canonical targeted-RCB integration
masarray Oct 7, 2026
c69bebf
ci(#144): refresh canonical targeted-RCB integration
masarray Oct 7, 2026
1868ea8
ci(#144): refresh canonical targeted-RCB integration
masarray Oct 7, 2026
c0609d5
ci(#144): refresh canonical targeted-RCB integration
masarray Oct 7, 2026
cba751f
perf(reporting): integrate event-driven report wake for exact-head CI…
masarray Oct 7, 2026
471c1f1
perf(reporting): integrate event-driven report wake for exact-head CI…
masarray Oct 7, 2026
d5d82c5
perf(reporting): integrate event-driven report wake for exact-head CI…
masarray Oct 7, 2026
71a8a52
test(reporting): prove burst reports coalesce without stale wake (#144)
masarray Oct 7, 2026
9bb2f93
feat(reporting): integrate P6 source-aware acquisition preflight for …
masarray Oct 7, 2026
93e1a80
feat(reporting): integrate P6 source-aware acquisition preflight for …
masarray Oct 7, 2026
edf41a8
feat(reporting): integrate source-bound P6 preflight for exact-head C…
masarray Oct 7, 2026
959ca81
feat(reporting): integrate source-bound P6 preflight for exact-head C…
masarray Oct 7, 2026
060a217
feat(reporting): integrate source-bound P6 preflight for exact-head C…
masarray Oct 7, 2026
866b10d
feat(reporting): integrate source-bound P6 preflight for exact-head C…
masarray Oct 7, 2026
89c655e
test(reporting): prove live MMS inventory authority is stamped at dis…
masarray Oct 7, 2026
cd7002d
feat(reporting): integrate canonical structural inventory projection …
masarray Oct 7, 2026
b0860f6
feat(reporting): integrate canonical structural inventory projection …
masarray Oct 7, 2026
4d97751
feat(reporting): integrate canonical structural inventory projection …
masarray Oct 7, 2026
b8ff24b
test(reporting): use canonical SCL evidence enum name (#144)
masarray Oct 7, 2026
51fa709
test(reporting): use canonical SCL evidence enum name (#144)
masarray Oct 7, 2026
5858a86
feat(reporting): integrate P6.1 targeted live RCB reconciliation for …
masarray Oct 7, 2026
9c52925
feat(reporting): integrate P6.1 targeted live RCB reconciliation for …
masarray Oct 7, 2026
79f9de0
fix(scl): resolve same-IED ServerAt chains with fail-closed owner sem…
masarray Oct 8, 2026
8b57df8
fix(scl): preserve referenced Server model for ServerAt endpoint proj…
masarray Oct 8, 2026
f111948
fix(scl): recognize valid ServerAt in engineering profile; flag inval…
masarray Oct 8, 2026
ebffc41
test(scl): Siemens-style J Server plus F ServerAt dual endpoint regre…
masarray Oct 8, 2026
cee8021
fix(scl): resolve ServerAt owner for exact MMS domain inventory on se…
masarray Oct 8, 2026
90ec4f2
fix(scl): carry ServerAt model through SCL-assisted initial FC-read p…
masarray Oct 8, 2026
0f32a71
test(scl): ServerAt must preserve both MMS domains and SCL-assisted i…
masarray Oct 8, 2026
e45259b
test(scl): import exact domain and initial-read authority namespace
masarray Oct 8, 2026
352c81e
test(scl): retain no-server fail-closed check with ServerAt-aware dia…
masarray Oct 8, 2026
fd1e8f4
feat(discovery): close static dataset LN schemas without full name en…
masarray Oct 9, 2026
f42318c
test(discovery): gate new LN roots on exact dataset plus successful G…
masarray Oct 9, 2026
8b0c0f7
fix(discovery): restore complete policy source and deterministic memb…
masarray Oct 9, 2026
d8dc2d2
feat(identity): exact trusted-SCL IED/LD mapping on pinned smart-disc…
masarray Oct 10, 2026
d34757e
fix(scl): thread verified IED/LD identity through canonical live expo…
masarray Oct 10, 2026
00ad281
test(identity): construct canonical fixture with an observed logical …
masarray Oct 11, 2026
5559406
fix(simulation): serialize MMS InformationReport emission per associa…
masarray Oct 11, 2026
ef0fe44
ci: validate engine staging pull requests in existing Windows .NET wo…
masarray Oct 11, 2026
fa93c9f
fix(mms): quarantine contradictory RptID/DatSet reports before proces…
masarray Oct 11, 2026
d700b0d
fix(mms): fail closed instead of silently dropping overflowed report …
masarray Oct 11, 2026
9bd0190
fix(mms): bound canonical report ingress and fail closed on overflow/…
masarray Oct 12, 2026
98f2119
fix(scl): decode unambiguous BER-packed AP-title in trusted associati…
masarray Oct 12, 2026
67c6635
fix(control): identify BSC TapChg from live ValWithTrans status path
masarray Oct 12, 2026
a43b9b4
P10.9K: gate generic control semantics and reuse per-association MMS …
masarray Oct 12, 2026
55fa34c
P10.9L: add bounded read-only exact MMS ctlModel evidence probe
masarray Oct 12, 2026
5e17da5
fix: remove escaped newline from P10.9L probe declaration
masarray Oct 12, 2026
f7a4e3b
P10.9Q: explicit guarded one-shot CF ctlModel commissioning with exac…
masarray Oct 12, 2026
d1dd8fc
P10.9R: support explicit ctlModel status-only value 0 in one-shot CF …
masarray Oct 12, 2026
f3860d9
P10.9T: verify structured MMS status; allow normal SBO without option…
masarray Oct 12, 2026
9d82523
P10.9T: repair structured-status implementation syntax and keep typed…
masarray Oct 12, 2026
0ae297c
P10.9U: resolve CSWI Pos ST status via single typed MMS read when dis…
masarray Oct 12, 2026
1a7ce83
P10.9U: fix xUnit analyzer violations in truncated-NameList control t…
masarray Oct 12, 2026
ad43399
P10.9U: align structured position regression with exactly one typed S…
masarray Oct 12, 2026
38420f1
fix(control): honor exact typed live BSC over stale SPC projection
masarray Oct 13, 2026
036e2c0
test(control): cover stale SPC to exact BSC and fail-closed mismatches
masarray Oct 13, 2026
cfd2a54
fix(m217k): project typed ST enum status FCD tuples for C264 Mod/PhyH…
masarray Oct 17, 2026
54bdc4b
test(m217k): physical C264 typed Mod/Beh/PhyHealth ST FCD and strict …
masarray Oct 17, 2026
e61989f
ci(m217k): run engine Windows test gate on pinned PR169 stacked branch
masarray Oct 17, 2026
51c2eef
test(m217k): incomplete live schema must not suppress typed C264 ST F…
masarray Oct 17, 2026
b27842a
test(identity): reproduce truncated device-name suffix and unverified…
masarray Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/dotnet-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ on:
- "*.slnx"
- "Directory.Build.props"
pull_request:
branches: [ main ]
# The live ARSAS product pins PR #169 until targeted engine changes pass CI.
# Stacked engine PRs must run the exact same Windows build/test gate.
branches: [ main, feat/495-dataset-type-closure, fix/p10-9l-readonly-ctlmodel-probe ]
workflow_dispatch:

permissions:
Expand Down
61 changes: 49 additions & 12 deletions src/AR.Iec61850.Simulation/MmsServer/MmsReportingRuntime.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Globalization;
using System.Threading.Channels;
using AR.Iec61850.Asn1;
using AR.Iec61850.Mms;

Expand Down Expand Up @@ -192,6 +193,17 @@ public sealed class MmsAssociationReportingRuntime : IMmsAssociationRuntime, IDi
private readonly Dictionary<string, Timer> _integrityTimers = new(StringComparer.OrdinalIgnoreCase);
private readonly object _gate = new();
private readonly CancellationTokenSource _cts = new();
// One FIFO per MMS association. A distinct Task.Run per report previously
// allowed send delegates to acquire the socket lock in a different order
// from the SqNum assignment, manufacturing on-wire discontinuities.
private readonly Channel<(MmsRcbRuntimeState State, byte Reason)> _sendQueue =
Channel.CreateUnbounded<(MmsRcbRuntimeState, byte)>(
new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false,
AllowSynchronousContinuations = false
});
private bool _disposed;

public MmsAssociationReportingRuntime(
Expand Down Expand Up @@ -226,6 +238,12 @@ public MmsAssociationReportingRuntime(
IntgPd = (uint)Math.Max(0, rcb.IntegrityPeriodMs)
};
}

// One asynchronous worker is allocated per association, not per report.
// Capture the token here: a delayed worker must never access a CTS
// after Dispose() has disposed its source.
var senderToken = _cts.Token;
_ = Task.Run(() => DrainReportQueueAsync(senderToken));
}

public IReadOnlyCollection<MmsRcbRuntimeState> States => _states.Values;
Expand Down Expand Up @@ -414,6 +432,7 @@ public void Dispose()
return;

_disposed = true;
_sendQueue.Writer.TryComplete();
_cts.Cancel();
lock (_gate)
{
Expand Down Expand Up @@ -449,21 +468,39 @@ private void QueueReport(MmsRcbRuntimeState state, byte reason)
if (_disposed)
return;

_ = Task.Run(async () =>
// Channel FIFO is the report ordering authority. No per-report Task.Run,
// no competing asynchronous socket writers and no silent report drop.
_sendQueue.Writer.TryWrite((state, reason));
}

private async Task DrainReportQueueAsync(CancellationToken cancellationToken)
{
try
{
try
{
await SendReportAsync(state, reason, _cts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Association closing.
}
catch (Exception ex) when (ex is IOException or ObjectDisposedException or System.Net.Sockets.SocketException or InvalidOperationException)
await foreach (var request in _sendQueue.Reader.ReadAllAsync(cancellationToken)
.ConfigureAwait(false))
{
_activity?.Invoke(state.MmsReference, false, $"InformationReport send failed: {ex.Message}");
try
{
await SendReportAsync(request.State, request.Reason, cancellationToken)
.ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return;
}
catch (Exception ex) when (ex is IOException or ObjectDisposedException or
System.Net.Sockets.SocketException or InvalidOperationException)
{
_activity?.Invoke(request.State.MmsReference, false,
$"InformationReport send failed: {ex.Message}");
}
}
}, CancellationToken.None);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Association closing: pending reports are not sent on a dead socket.
}
}

private async Task SendReportAsync(MmsRcbRuntimeState state, byte reason, CancellationToken cancellationToken)
Expand Down
4 changes: 3 additions & 1 deletion src/AR.Iec61850/Control/Iec61850ControlModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ public sealed class Iec61850ControlObjectDescriptor
ControlModel is not Iec61850ControlModel.StatusOnly and not Iec61850ControlModel.Unknown &&
!string.IsNullOrWhiteSpace(ObjectReference) &&
!string.IsNullOrWhiteSpace(CtlValSpecification.MmsType) &&
(!RequiresSelect || CancelSpecification != null) &&
// Normal SBO Select -> Operate does not depend on optional Cancel.
// Enhanced SBO still requires Cancel for sequence recovery.
(ControlModel != Iec61850ControlModel.SelectBeforeOperateEnhanced || CancelSpecification != null) &&
(ControlModel != Iec61850ControlModel.SelectBeforeOperateEnhanced || SelectWithValueSpecification != null) &&
(!IsEnhanced || SupportsCommandTermination);

Expand Down
106 changes: 106 additions & 0 deletions src/AR.Iec61850/Control/Iec61850ControlSemanticResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using AR.Iec61850.Mms;

namespace AR.Iec61850.Control;

/// <summary>
/// Resolves *command* semantics from a declared CDC and corroborating live MMS
/// evidence. A primitive MMS type is an encoding, not permission to Operate.
/// This is vendor-independent and never sends an MMS request.
/// </summary>
public static class Iec61850ControlSemanticResolver
{
public static Iec61850ControlSemanticResolution Resolve(
Iec61850ControlObjectDescriptor descriptor,
string? declaredCdc,
string? declaredStatusReference = null)
{
ArgumentNullException.ThrowIfNull(descriptor);
var declared = (declaredCdc ?? string.Empty).Trim().ToUpperInvariant();
var inferred = (descriptor.Cdc ?? string.Empty).Trim().ToUpperInvariant();
var status = descriptor.StatusReference ?? string.Empty;
var mmsType = (descriptor.CtlValSpecification.MmsType ?? string.Empty).Trim().ToLowerInvariant();
var size = descriptor.CtlValSpecification.Size;
// A saved SCL/DataSet SPC can describe a process projection, not the
// command payload. Only an EXACT same-object ST step status plus a
// typed two-bit MMS ctlVal establishes live directional BSC authority.
// A bit-string alone (or a similarly named object) proves nothing.
var exactLiveBsc =
inferred == "BSC" &&
mmsType == "bit-string" && size == 2 &&
status.Equals(descriptor.ObjectReference + ".valWTr.posVal",
StringComparison.OrdinalIgnoreCase);
var supersededAdvisorySpc = declared == "SPC" && exactLiveBsc;
var cdc = supersededAdvisorySpc
? "BSC"
: string.IsNullOrWhiteSpace(declared) ? inferred : declared;

Iec61850ControlSemanticResolution Deny(string reason)
=> new(false, cdc, reason);

if (!descriptor.IsOperationallyReady)
return Deny("The live control descriptor or ctlModel is not operationally ready.");
if (string.IsNullOrWhiteSpace(status))
return Deny("No live status reference corroborates the control semantics.");

// A stale persisted feedback reference must not silently override a
// different live BSC status object when commands are involved.
if (cdc == "BSC" && !string.IsNullOrWhiteSpace(declaredStatusReference) &&
!string.Equals(declaredStatusReference.Trim(), status, StringComparison.OrdinalIgnoreCase))
return Deny("Declared BSC feedback reference disagrees with the live control descriptor.");

if (cdc != inferred &&
!(inferred == "INC/ISC" && cdc is "INC" or "ISC") &&
!(cdc == "DPC" && inferred == "SPC" && IsStandardPosition(descriptor.ObjectReference)))
return Deny($"Declared CDC '{cdc}' conflicts with live CDC evidence '{inferred}'.");

var statusIsStep = status.EndsWith(".valWTr.posVal", StringComparison.OrdinalIgnoreCase);
var statusIsState = status.EndsWith(".stVal", StringComparison.OrdinalIgnoreCase);
var supported = cdc switch
{
"BSC" => statusIsStep &&
(mmsType is "integer" or "unsigned" ||
(mmsType == "bit-string" && size == 2)),
"DPC" => statusIsState &&
((mmsType == "bit-string" && size == 2) ||
(mmsType == "boolean" && IsStandardPosition(descriptor.ObjectReference))),
"SPC" => statusIsState && mmsType == "boolean",
"INC" or "ISC" => declared == cdc && statusIsState &&
mmsType is "integer" or "unsigned" or "bcd",
"APC" => declared == "APC" &&
(mmsType == "floating-point" || IsAnalogStructure(descriptor.CtlValSpecification)),
_ => false // Other CDCs require an independently audited typed codec.
};

if (!supported)
return Deny($"CDC '{cdc}' cannot be safely bound to live ctlVal '{descriptor.CtlValSpecification.Signature}' and status '{status}'.");

var provenance = supersededAdvisorySpc
? " Saved/DataSet CDC=SPC was advisory and superseded by exact live BSC control evidence."
: string.Empty;
return new(true, cdc,
$"Verified CDC={cdc}; live ctlModel={descriptor.ControlModel}; ctlVal={descriptor.CtlValSpecification.Signature}; status={status}.{provenance}");
}

internal static bool IsStandardPosition(string reference)
{
var suffix = reference[(reference.LastIndexOf('/') + 1)..];
var dot = suffix.IndexOf('.');
if (dot <= 0 || !suffix[(dot + 1)..].Equals("Pos", StringComparison.OrdinalIgnoreCase))
return false;
var ln = suffix[..dot].ToUpperInvariant();
return ln.StartsWith("CSWI", StringComparison.Ordinal) ||
ln.StartsWith("XCBR", StringComparison.Ordinal) ||
ln.StartsWith("XSWI", StringComparison.Ordinal);
}

private static bool IsAnalogStructure(MmsTypeSpecificationNode ctlVal)
{
if (!ctlVal.MmsType.Equals("structure", StringComparison.OrdinalIgnoreCase))
return false;
var numeric = ctlVal.Children.Where(child =>
child.MmsType is "integer" or "unsigned" or "floating-point").ToArray();
return numeric.Length == 1 && ctlVal.Children.Count == 1;
}
}

public sealed record Iec61850ControlSemanticResolution(bool IsCommandSafe, string Cdc, string Reason);
Loading
Loading