RBC_Test_Sim_Core
0.1.0 build 51fd23e
Shared transport-layer plumbing for the ADR-029 Train/IL/CTC RBC simulators
Toggle main menu visibility
Loading...
Searching...
No Matches
saf_train_link.py
Go to the documentation of this file.
1
"""SafTrainLink - Subset-037 SaF protection of a train sim's ERTMS traffic (the train = SaF initiator).
2
3
Python side of RBC_GP's train-link SaF termination (``ab_gp_saf_train.c``; root
4
``docs/architecture/SAFE_COMM_INTEGRATION.dox`` plan steps 2/3). Wraps either transport a train sim uses
5
(``DualHomedLink`` or ``SafeCommLink``) and keeps their ``ensure_connected()`` / ``send_all()`` / ``poll_recv()``
6
surface, so ``train_sim`` only swaps the object:
7
8
* An outgoing ``RBC_MSG_ERTMS_ENVELOPE`` (kind 22: kind, train_id, codec bytes, zero padding) becomes one Sa-DATA
9
frame of that train's SaF connection, carried in an ``RBC_MSG_SAF_ENVELOPE`` (kind 28: kind, train_id, 2-byte
10
big-endian frame length, frame, zero padding). The PDU is exactly the codec bytes (``l_message`` long).
11
* The first ERTMS send for a train starts its handshake (AU1); PDUs queue until AR arrives, then go out in order.
12
* An incoming kind-28 frame is verified; a Sa-DATA PDU is handed to the caller as an ordinary kind-22 frame, so the
13
catalog decoder and everything above it are unchanged.
14
* Anything else (the flat P0/M136/TRAIN_CONNECT scheme) passes through untouched - the RBC side protects only the
15
ERTMS envelope too.
16
* A handshake past ``t_connect_ms`` or a DATA connection silent past ``t_supervision_ms`` is dropped; the next ERTMS
17
send starts a new handshake (this is also how a site failover is recovered: the newly ONLINE site has no session).
18
* A Sa-DISCONNECT from the RBC (it answers a Sa-DATA frame for a connection it no longer holds - e.g. its processes
19
restarted) drops the session at once: the PDUs sent in the last ``resend_window_ms`` were swallowed by the dead
20
session, so they are queued again and a new handshake starts immediately (a real train's application would redo
21
whatever the lost messages carried, e.g. Start of Mission).
22
23
Keys: ``keys`` maps a train's ETCS identity (its nid_engine) to its 24-byte KMAC - the same test-only key file the
24
RBC reads through ``RTE_SAFECOMM_TRAIN_KEYS_FILE``.
25
"""
26
import
collections
27
import
time
28
29
from
.
import
saf
30
31
ERTMS_KIND = 22
32
SAF_KIND = 28
33
_SAF_HEADER = 4
34
ETY_ENGINE = 2
# Subset-037-2 Table 8
35
36
37
def
load_keys_file
(path):
38
"""Parse ``<etcs_id> <48 hex>`` lines (``#`` comments, blank lines ignored) into {etcs_id: kmac}."""
39
keys = {}
40
with
open(path, encoding=
"ascii"
)
as
fh:
41
for
lineno, raw
in
enumerate(fh, 1):
42
line = raw.split(
"#"
, 1)[0].strip()
43
if
not
line:
44
continue
45
parts = line.split()
46
if
len(parts) != 2
or
len(parts[1]) != 2 * saf.KEY_LEN:
47
raise
ValueError(f
"{path}:{lineno}: expected '<etcs_id> <48 hex chars>'"
)
48
keys[int(parts[0], 0) & 0xFFFFFF] = bytes.fromhex(parts[1])
49
return
keys
50
51
52
def
encode_envelope
(train_id, frame, wire_size):
53
if
not
frame
or
len(frame) > wire_size - _SAF_HEADER:
54
raise
ValueError(f
"SaF frame of {len(frame)} bytes does not fit a {wire_size}-byte envelope"
)
55
env = bytes([SAF_KIND, train_id & 0xFF]) + len(frame).to_bytes(2,
"big"
) + bytes(frame)
56
return
env + bytes(wire_size - len(env))
57
58
59
def
decode_envelope
(env):
60
"""(train_id, frame) of a kind-28 envelope, or None if malformed."""
61
if
len(env) < _SAF_HEADER
or
env[0] != SAF_KIND:
62
return
None
63
length = int.from_bytes(env[2:4],
"big"
)
64
if
length == 0
or
_SAF_HEADER + length > len(env):
65
return
None
66
return
env[1], bytes(env[_SAF_HEADER:_SAF_HEADER + length])
67
68
69
class
SafTrainLink
:
70
def
__init__
(self, inner, log, rbc_etcs_id, keys, wire_size, *, t_connect_ms=10000, t_supervision_ms=20000,
71
clock=None, rng=None, resend_window_ms=3000):
72
self.
_inner
= inner
73
self.
_log
= log
74
self.
_rbc_id
= rbc_etcs_id & 0xFFFFFF
75
self.
_keys
= dict(keys)
76
self.
_wire_size
= wire_size
77
self.
_t_connect_ms
= t_connect_ms
78
self.
_t_supervision_ms
= t_supervision_ms
79
self.
_clock
= clock
80
self.
_rng
= rng
81
self.
_sessions
= {}
# train_id -> SafSession
82
self.
_queues
= {}
# train_id -> [pdu, ...] waiting for the handshake
83
self.
_recent
= {}
# train_id -> deque of (sent_ms, pdu) sent in DATA, for a resend after a Sa-DISCONNECT
84
self.
_resend_window_ms
= resend_window_ms
85
86
def
__getattr__
(self, name):
87
# sockets / disconnect / connected_sites and anything else belong to the wrapped transport
88
return
getattr(self.
_inner
, name)
89
90
# -- transport surface -------------------------------------------------------------------------------------
91
def
ensure_connected
(self):
92
self.
_inner
.
ensure_connected
()
93
self.
_supervise
()
94
95
def
send_all
(self, payload):
96
if
len(payload) < 5
or
payload[0] != ERTMS_KIND:
97
self.
_inner
.
send_all
(payload)
98
return
99
tid = payload[1]
100
codec = bytes(payload[2:])
101
l_message = int.from_bytes(codec[1:3],
"big"
)
102
if
l_message == 0
or
l_message > len(codec):
103
self.
_log
.warning(
"[SAF] [Train %d] ERTMS frame with bad l_message=%d not sent"
, tid, l_message)
104
return
105
pdu = codec[:l_message]
106
sess = self.
_sessions
.get(tid)
107
if
sess
is
not
None
and
sess.state == saf.DATA:
108
self.
_send_pdu
(tid, sess, pdu)
109
return
110
self.
_queues
.setdefault(tid, []).append(pdu)
111
if
sess
is
None
or
sess.state == saf.IDLE:
112
self.
_start_handshake
(tid)
113
114
def
poll_recv
(self, timeout=0.0):
115
for
site, data
in
self.
_inner
.
poll_recv
(timeout):
116
if
not
data
or
data[0] != SAF_KIND:
117
yield
site, data
118
continue
119
decoded =
decode_envelope
(data)
120
if
decoded
is
None
:
121
self.
_log
.warning(
"[SAF] malformed SaF envelope from %s dropped"
, site)
122
continue
123
tid, frame = decoded
124
pdu = self.
_on_frame
(tid, frame)
125
if
pdu
is
not
None
:
126
env = bytes([ERTMS_KIND, tid]) + pdu
127
yield
site, env + bytes(max(0, self.
_wire_size
- len(env)))
128
129
# -- internals ----------------------------------------------------------------------------------------------
130
def
_new_session
(self, tid):
131
kmac = self.
_keys
.get(tid)
132
if
kmac
is
None
:
133
return
None
134
kwargs = {
"ety"
: ETY_ENGINE,
"t_connect_ms"
: self.
_t_connect_ms
,
"t_supervision_ms"
: self.
_t_supervision_ms
}
135
if
self.
_clock
is
not
None
:
136
kwargs[
"clock"
] = self.
_clock
137
if
self.
_rng
is
not
None
:
138
kwargs[
"rng"
] = self.
_rng
139
return
saf.SafSession
(
"initiator"
, tid, self.
_rbc_id
, kmac, self.
_raw_send_unused
, self.
_raw_recv_unused
,
140
**kwargs)
141
142
@staticmethod
143
def
_raw_send_unused
(_frame):
144
raise
RuntimeError(
"frame-driven session: frames are sent by SafTrainLink"
)
145
146
@staticmethod
147
def
_raw_recv_unused
():
148
raise
TimeoutError
149
150
def
_send_frame
(self, tid, frame):
151
self.
_inner
.
send_all
(
encode_envelope
(tid, frame, self.
_wire_size
))
152
153
def
_now_ms
(self):
154
return
self.
_clock
()
if
self.
_clock
is
not
None
else
int(time.monotonic() * 1000)
155
156
def
_send_pdu
(self, tid, sess, pdu):
157
self.
_send_frame
(tid, sess.build_frame(pdu))
158
sess.seq_tx = (sess.seq_tx + 1) & 0xFFFFFFFF
159
recent = self.
_recent
.setdefault(tid, collections.deque())
160
now = self.
_now_ms
()
161
recent.append((now, pdu))
162
while
recent
and
now - recent[0][0] > self.
_resend_window_ms
:
163
recent.popleft()
164
165
def
_start_handshake
(self, tid):
166
sess = self.
_new_session
(tid)
167
if
sess
is
None
:
168
self.
_log
.error(
"[SAF] [Train %d] no KMAC for ETCS id %d - ERTMS traffic not sent"
, tid, tid)
169
self.
_queues
.pop(tid,
None
)
170
return
171
self.
_sessions
[tid] = sess
172
self.
_send_frame
(tid, sess.initiator_start())
173
self.
_log
.info(
"[SAF] [Train %d] AU1 sent (rbc_etcs_id=%d)"
, tid, self.
_rbc_id
)
174
175
def
_on_frame
(self, tid, frame):
176
sess = self.
_sessions
.get(tid)
177
if
sess
is
None
:
178
self.
_log
.warning(
"[SAF] [Train %d] SaF frame with no session dropped"
, tid)
179
return
None
180
try
:
181
if
sess.state
in
(saf.WAIT_AU2, saf.WAIT_AR):
182
reply = sess.initiator_on_frame(frame)
183
if
reply
is
not
None
:
184
self.
_send_frame
(tid, reply)
185
if
sess.state == saf.DATA:
186
self.
_log
.info(
"[SAF] [Train %d] safe connection established"
, tid)
187
for
pdu
in
self.
_queues
.pop(tid, []):
188
self.
_send_pdu
(tid, sess, pdu)
189
return
None
190
return
sess.accept_frame(frame)
191
except
saf.SafError
as
err:
192
if
err.status ==
"PEER_DISCONNECT"
:
193
now = self.
_now_ms
()
194
lost = [pdu
for
sent, pdu
in
self.
_recent
.pop(tid, ())
if
now - sent <= self.
_resend_window_ms
]
195
self.
_queues
[tid] = lost + self.
_queues
.get(tid, [])
196
self.
_log
.info(
"[SAF] [Train %d] released by the RBC - reconnecting, %d recent message(s) queued "
197
"for resend"
, tid, len(lost))
198
else
:
199
self.
_log
.warning(
"[SAF] [Train %d] connection dropped: %s"
, tid, err)
200
if
self.
_queues
.get(tid):
201
self.
_start_handshake
(tid)
202
return
None
203
204
def
_supervise
(self):
205
for
tid, sess
in
list(self.
_sessions
.items()):
206
if
sess.handshake_expired():
207
self.
_log
.warning(
"[SAF] [Train %d] handshake timed out - retrying"
, tid)
208
sess.state = saf.IDLE
209
if
self.
_queues
.get(tid):
210
self.
_start_handshake
(tid)
211
elif
sess.state == saf.DATA:
212
try
:
213
sess.supervise()
214
except
saf.SafError
:
215
self.
_log
.warning(
"[SAF] [Train %d] supervision expired - link lost, will re-handshake"
, tid)
simcore.saf.SafError
Definition
saf.py:237
simcore.saf.SafSession
Definition
saf.py:248
simcore.saf_train_link.SafTrainLink
Definition
saf_train_link.py:69
simcore.saf_train_link.SafTrainLink.__init__
__init__(self, inner, log, rbc_etcs_id, keys, wire_size, *, t_connect_ms=10000, t_supervision_ms=20000, clock=None, rng=None, resend_window_ms=3000)
Definition
saf_train_link.py:71
simcore.saf_train_link.SafTrainLink._on_frame
_on_frame(self, tid, frame)
Definition
saf_train_link.py:175
simcore.saf_train_link.SafTrainLink._t_supervision_ms
_t_supervision_ms
Definition
saf_train_link.py:78
simcore.saf_train_link.SafTrainLink._log
_log
Definition
saf_train_link.py:73
simcore.saf_train_link.SafTrainLink._start_handshake
_start_handshake(self, tid)
Definition
saf_train_link.py:165
simcore.saf_train_link.SafTrainLink._supervise
_supervise(self)
Definition
saf_train_link.py:204
simcore.saf_train_link.SafTrainLink._new_session
_new_session(self, tid)
Definition
saf_train_link.py:130
simcore.saf_train_link.SafTrainLink._clock
_clock
Definition
saf_train_link.py:79
simcore.saf_train_link.SafTrainLink._t_connect_ms
_t_connect_ms
Definition
saf_train_link.py:77
simcore.saf_train_link.SafTrainLink.poll_recv
poll_recv(self, timeout=0.0)
Definition
saf_train_link.py:114
simcore.saf_train_link.SafTrainLink._send_frame
_send_frame(self, tid, frame)
Definition
saf_train_link.py:150
simcore.saf_train_link.SafTrainLink._resend_window_ms
_resend_window_ms
Definition
saf_train_link.py:84
simcore.saf_train_link.SafTrainLink._now_ms
_now_ms(self)
Definition
saf_train_link.py:153
simcore.saf_train_link.SafTrainLink._rbc_id
int _rbc_id
Definition
saf_train_link.py:74
simcore.saf_train_link.SafTrainLink._send_pdu
_send_pdu(self, tid, sess, pdu)
Definition
saf_train_link.py:156
simcore.saf_train_link.SafTrainLink._recent
dict _recent
Definition
saf_train_link.py:83
simcore.saf_train_link.SafTrainLink._sessions
dict _sessions
Definition
saf_train_link.py:81
simcore.saf_train_link.SafTrainLink.ensure_connected
ensure_connected(self)
Definition
saf_train_link.py:91
simcore.saf_train_link.SafTrainLink._keys
_keys
Definition
saf_train_link.py:75
simcore.saf_train_link.SafTrainLink._rng
_rng
Definition
saf_train_link.py:80
simcore.saf_train_link.SafTrainLink.send_all
send_all(self, payload)
Definition
saf_train_link.py:95
simcore.saf_train_link.SafTrainLink._inner
_inner
Definition
saf_train_link.py:72
simcore.saf_train_link.SafTrainLink._raw_recv_unused
_raw_recv_unused
Definition
saf_train_link.py:139
simcore.saf_train_link.SafTrainLink._queues
dict _queues
Definition
saf_train_link.py:82
simcore.saf_train_link.SafTrainLink.__getattr__
__getattr__(self, name)
Definition
saf_train_link.py:86
simcore.saf_train_link.SafTrainLink._wire_size
_wire_size
Definition
saf_train_link.py:76
simcore.saf_train_link.SafTrainLink._raw_send_unused
_raw_send_unused
Definition
saf_train_link.py:139
simcore.saf_train_link.load_keys_file
load_keys_file(path)
Definition
saf_train_link.py:37
simcore.saf_train_link.encode_envelope
encode_envelope(train_id, frame, wire_size)
Definition
saf_train_link.py:52
simcore.saf_train_link.decode_envelope
decode_envelope(env)
Definition
saf_train_link.py:59
src
simcore
saf_train_link.py
Generated by
1.18.0