Technical manual — chapter list

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 (the RunLive idiom; --ticks headless 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), behind IRtuTransport.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[]); RtuProtocolRegistry keys 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) -> ShouldDrop or a SigmaMultiplier. There is no quality field on Measurement (ch. 6): sigma is the weight, so quality becomes weight here.
  • TimeDecode - Cp56Time2a, Dnp3AbsoluteTime, IsoUtcTime -> UTC DateTime (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 as MeasurementSet.
  • RtuMeasurementSetMapper.Map - the RTU analogue of Synchrophasor/PmuMeasurementSetMapper: guard-first (ForwardInferenceGuard.RejectFuture), drop-on-bad-quality, crosswalk resolve, value*scale then per-unit base, angle re-reference, emit a MeasurementSet (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.