RBC_Test_Sim_Core 0.1.0 build 51fd23e
Shared transport-layer plumbing for the ADR-029 Train/IL/CTC RBC simulators
Loading...
Searching...
No Matches
sai.py
Go to the documentation of this file.
1"""SAI - Python mirror of the Safe Application Intermediate sub-layer (Subset-098 v4.0.0 SS5.4),
2EC-counter variant. Byte-identical to ``Platform_Protocol_SafeComm`` (``src/sai/sai.c``), the same way
3``simcore.saf`` mirrors ``src/saf/saf.c``.
4
5Sits functionally *above* a connected :class:`simcore.saf.SafSession`: it wraps every user PDU in a SAI
6header (message type, 16-bit sequence number, EC counter; the Triple-Time-Stamp fields are zeroed - only
7EC is implemented, same scope limit as the C module) before handing it to ``SafSession.send_pdu``, and
8unwraps/checks it on ``recv_pdu``. This supplies the delay/re-ordering/repetition defence the Safety
9Layer's own DT PDU doesn't carry (Subset-037-2 Table 12 has no sequence field at all).
10
11Implements SS5.4.7 (sequence window) and SS5.4.9 (EC counter freshness: the EC-start exchange, the
12ratio/Ex bookkeeping, and the Delta-based 0..3 state check). Does NOT implement Triple Time Stamping
13(SS5.4.8), the long-term drift correction of SS5.4.9.5, or the separate delay-detection heartbeat of
14SS5.4.9.6 - same scope as ``sai.c``; see Platform_Protocol_SafeComm/docs/safety/SUBSET_MAPPING.md.
15"""
16from simcore import saf as _saf
17
18MSG_EC_START = 0x81
19MSG_EC_DATA = 0x86
20
21HEADER_LEN = 19 # msgtype(1) + seq(2) + TTS(12, zeroed) + EC(4)
22START_EXTRA_LEN = 6 # Version(4) + EC period(2)
23
24
25class SaiError(Exception):
26 """A SAI failure; ``status`` names the matching C ``SAFECOMM_STATUS_*`` value."""
27
28 def __init__(self, status, detail=""):
29 super().__init__(f"{status}{': ' + detail if detail else ''}")
30 self.status = status
31
32
34 """SS5.4.9.4.7: the current EC state depends only on the latest Delta."""
35 if delta <= 0:
36 return 0
37 if delta == 1:
38 return 1
39 if delta == 2:
40 return 2
41 return 3 # alarm
42
43
45 """One SAI connection, layered over an already-connected :class:`simcore.saf.SafSession`.
46
47 ``ec_period_ms`` is this side's own EC (execution cycle) period. ``seq_window`` is N of SS5.4.7 (up
48 to ``seq_window - 1`` missing messages tolerated; default 1, no gaps allowed). ``ec_alarm_state`` is
49 the Delta at/above which freshness fails (default 3, matching the spec's own worked example).
50 """
51
52 def __init__(self, saf_session, ec_period_ms, *, seq_window=1, ec_alarm_state=3):
53 if ec_period_ms <= 0:
54 raise ValueError("ec_period_ms must be positive")
55 self.saf = saf_session
56 self.ec_period_ms = ec_period_ms
57 self.seq_window = seq_window or 1
58 self.ec_alarm_state = ec_alarm_state or 3
59
60 self.seq_tx = 0
61 self.seq_rx = 0
62 self.seq_rx_valid = False
63 self.ec_tx = 0
65 self.ratio = None
66 self.expected_ec = None
67 self.last_rx_ec = 0
68 self.ec_initialized = False
69 self.ec_state = 0
70
71 def _header(self, msg_type, seq, ec):
72 return bytes([msg_type]) + seq.to_bytes(2, "big") + bytes(12) + ec.to_bytes(4, "big")
73
74 def start(self):
75 """Run the EC initialisation exchange (SS5.4.9.3): send our ExecutionCycleStart, wait for the
76 peer's, and compute the EC ratio. ``self.saf`` must already be in DATA (its own handshake done)."""
77 out = (self._header(MSG_EC_START, self.seq_tx, self.ec_tx)
78 + (1).to_bytes(4, "big") + min(self.ec_period_ms, 0xFFFF).to_bytes(2, "big"))
79 self.saf.send_pdu(out)
80 self.seq_tx = (self.seq_tx + 1) & 0xFFFF
81
82 try:
83 frame = self.saf.recv_pdu()
84 except _saf.SafError as exc:
85 raise SaiError(exc.status, str(exc)) from exc
86 if len(frame) != HEADER_LEN + START_EXTRA_LEN or frame[0] != MSG_EC_START:
87 raise SaiError("AUTH_FAILED", "EC start shape")
88
89 self.last_rx_ec = int.from_bytes(frame[15:19], "big")
90 self.peer_ec_period_ms = int.from_bytes(frame[23:25], "big")
91 if self.peer_ec_period_ms == 0:
92 raise SaiError("INVALID_PARAM", "peer EC period")
93 self.seq_rx = int.from_bytes(frame[1:3], "big")
94 self.seq_rx_valid = True
95
96 # SS5.4.9.4.3: R = receiver (our) EC period / sender (peer) EC period.
97 # SS5.4.9.4.4: update Ex once immediately, using the peer's initial EC counter.
98 self.ratio = self.ec_period_ms / self.peer_ec_period_ms
99 self.expected_ec = self.last_rx_ec + self.ratio
100 self.ec_initialized = True
101 self.ec_state = 0
102
103 def send(self, pdu):
104 frame = self._header(MSG_EC_DATA, self.seq_tx, self.ec_tx) + bytes(pdu)
105 self.saf.send_pdu(frame)
106 self.seq_tx = (self.seq_tx + 1) & 0xFFFF
107
108 def recv(self):
109 """Receive, unwrap and check one SAI-protected PDU. Raises :class:`SaiError`."""
110 try:
111 frame = self.saf.recv_pdu()
112 except _saf.SafError as exc:
113 raise SaiError(exc.status, str(exc)) from exc
114 if len(frame) < HEADER_LEN or frame[0] != MSG_EC_DATA:
115 raise SaiError("STATE", "unexpected SAI message type")
116
117 # SS5.4.7.2: sequence number check with up to (seq_window - 1) tolerated gaps.
118 rx_seq = int.from_bytes(frame[1:3], "big")
119 if self.seq_rx_valid:
120 diff = (rx_seq - self.seq_rx) & 0xFFFF
121 if diff >= self.seq_window:
122 raise SaiError("SEQ_ERROR", f"got {rx_seq}, expected {self.seq_rx}..+{self.seq_window - 1}")
123 self.seq_rx = (rx_seq + 1) & 0xFFFF
124 self.seq_rx_valid = True
125
126 # SS5.4.9.4: freshness.
127 self.last_rx_ec = int.from_bytes(frame[15:19], "big")
128 if self.ec_initialized:
129 delta = int(self.expected_ec - self.last_rx_ec)
130 self.ec_state = _ec_state_from_delta(delta)
131 if self.ec_state >= self.ec_alarm_state:
132 raise SaiError("TIMEOUT", "EC freshness alarm")
133
134 return frame[HEADER_LEN:]
135
136 def tick(self):
137 """Advance our own EC by one: call this once per ``ec_period_ms`` elapsed on the caller's clock.
138 Updates the expected-EC estimate and the freshness state (SS5.4.9.4.4-7), reporting the state
139 computed BEFORE advancing Ex for the next cycle (so the first tick after a fresh receive is 0)."""
140 if not self.ec_initialized:
141 return
142 delta = int(self.expected_ec - self.last_rx_ec)
143 self.ec_state = _ec_state_from_delta(delta)
144 self.expected_ec += self.ratio
145 self.ec_tx += 1
__init__(self, status, detail="")
Definition sai.py:28
send(self, pdu)
Definition sai.py:103
_header(self, msg_type, seq, ec)
Definition sai.py:71
__init__(self, saf_session, ec_period_ms, *, seq_window=1, ec_alarm_state=3)
Definition sai.py:52
_ec_state_from_delta(delta)
Definition sai.py:33