20. The RTU/SCADA interposer
The live-telemetry ingest path: raw SCADA/RTU protocol frames corrected, decoded, mapped onto the network
model, and emitted as guarded measurement windows and a GDA-shaped archive. It completes the object-model
layer the framing codecs of ch. 18.8 deferred. Everything here is governed by
UnderstandingRestrictions.md: live telemetry is present-time, so
the 169 h horizon guard (ch. 8) stands between it and the estimator, and the GDA public-data-only boundary
(ch. 9) stands between it and the evidence lake. Neither is ever bypassed.
The conceptual overview is docs/rtu-interposer.md; this chapter is the code map.
20.1 The verb and the loop
- CLI
interpose(GridSim.Cli/Program.cs,RunInterpose) - the first socket-binding verb. Selects a protocol (--protocol,--list-protocols), opens a transport, drives a timeout-bounded read loop (theRunLiveidiom;--ticksheadless cap; shutdown flush), and fans each window to the sinks. - Transports (
GridSim.Cli/InterposeTransport.cs):TcpDelimitedFrameTransport(start-byte + length framing, IEC-104),TcpMbapFrameTransport(Modbus MBAP), behindIRtuTransport.TryReadFrame+TransportFactory.Open. Reassembly and start-delimiter resync live here.
20.2 Protocol abstraction (Interpose/)
IRtuProtocol(name, display, transport kind, default port,Decode(frame, QualityFold) -> RtuPoint[]);RtuProtocolRegistrykeys stacks by lowercase name.RtuPoint= protocol-native address + value +RtuQuality+ optional device time;RtuPointType= Analog/Counter/BinaryStatus/Setpoint.- Active:
Protocols/Iec104Protocol,Protocols/ModbusProtocol. Scaffolded (framing done, object model pending): DNP3 (Dnp3Frame), GOOSE (Iec61850GooseFrame), C37.118 (Synchrophasor/C37118Decoder).
20.3 Object-model decoders (IO/Telemetry/)
Iec104Asdu- the IEC 60870-5-101/104 ASDU: DUI (type id, VSQ/SQ, COT, common address) + information objects for measured (9/11/13), counter (15), single/double-point (1/3) and their CP56Time2a-timed siblings (30/34/35/36/37). Shared by 104 (TCP) and 101 (serial, when wired).ModbusFrame- MBAP header + FC 3/4 read-registers response; registers keyed by response position; request/exception frames decode to no registers (distinguished by the MBAP length relation).
20.4 The correction stage
FrameCorrection- per-protocol framing/integrity gate (ForProtocol), accept/reject tallies; byte-stream resync is owned by the transport.QualityFold- protocol quality descriptor ->RtuQuality(FromIec104/FromDnp3/FromGoose) ->ShouldDropor aSigmaMultiplier. There is no quality field onMeasurement(ch. 6): sigma is the weight, so quality becomes weight here.TimeDecode-Cp56Time2a,Dnp3AbsoluteTime,IsoUtcTime-> UTCDateTime(Kind=Utc) for the guard.
20.5 Crosswalk and mapping
RtuPointMap(gridsim-rtu-pointmap/1) - user-authored address ->RtuPointBinding(kind, bus or from/to/circuit/end,baseKv/baseAmps,scale,sigma,datumAngleDeg). Same serializer contract asMeasurementSet.RtuMeasurementSetMapper.Map- the RTU analogue ofSynchrophasor/PmuMeasurementSetMapper: guard-first (ForwardInferenceGuard.RejectFuture), drop-on-bad-quality, crosswalk resolve,value*scalethen per-unit base, angle re-reference, emit aMeasurementSet(source"rtu").InterposePipeline- frame ->FrameCorrection-> decode -> window accumulation ->FlushWindow(latest device stamp, or receive clock as fallback).
20.6 The horizon policy
Live windows carry a present-time stamp, so the mapper/emitter is called with utcNow = DateTime.MaxValue
(the replay-feeder escape, ch. 12) - the window file writes now, and RealtimeEstimatorEngine (ch. 18.1)
re-checks it against the real clock and skips it (rejected-fresh) until it has aged past now - 169 h.
No live value is ever estimated; it becomes estimable only by ageing. The guard is enforced at write
(deferred) and at consume (real), never circumvented.
20.7 The quarantine sink (GridSim.Gda/Interpose/)
QuarantineLiveStore writes GDA-shaped Parquet (Parquet/<source>/year=/week=, columns
timestamp_utc/source/source_class/unit-named value cols/sigma/provenance, via
ParquetSerializer + [JsonPropertyName] snake_case) so GDA tooling reads it, but firewalled from the
public lake: source_class="telemetry", a _QUARANTINE.json evidential:false sidecar, and a path guard
that throws if the target is at/below a DataSchema.json (the lake master contract, ch. 9). Never merged
into GDA unified tables.
20.8 Tests
Iec104AsduTests, ModbusFrameTests (object models), RtuMeasurementSetMapperTests (crosswalk, quality
-> sigma, angle datum, guard reject vs MaxValue defer), InterposePipelineTests (bytes -> set, no
socket), QuarantineLiveStoreTests (snake_case columns, sidecar, path-guard firewall).
See the 1.1.0 entry in CHANGELOG.md for the full itemised list.