TrainRBCSim
ADR-029 Train (EVC) simulator — dual-homed TCP client speaking the RBC wire protocol
Loading...
Searching...
No Matches
rbc_ertms_wire.py
Go to the documentation of this file.
1"""Real Subset-026 ERTMS message codec - Python mirror of
2safeAPIRBC2oo2GP/src/application/AB/GP/ertms/train/ab_gp_ertms_train_codec.c
3(byte shape and order only). Keep this file in exact sync with that C
4file by hand; there is no shared schema between the two languages, same
5"by-hand port, not a generated binding" precedent rbc_wire.py's own
6module doc already states for the flat ADR-029 scheme.
7
8**Deliberately a DIFFERENT wire shape from rbc_wire.py, not a variant of
9it** - this is not an oversight, don't "fix" the two into consistency:
10 - rbc_wire.py: one fixed 96-byte struct, little-endian, every field at
11 a fixed offset regardless of kind - mirrors rbc_wire_types.h/
12 rbc_wire.c's own flat ADR-029 envelope.
13 - This module: real Subset-026 message-specific shapes, BIG-endian,
14 genuinely variable length (optional packets each self-identify with
15 their own leading NID_PACKET byte) - mirrors ab_gp_ertms_train_codec.c,
16 a byte-aligned TLV codec (not literally the real spec's own sub-byte
17 bit-packing - see that C file's own header doc for why: a deliberate,
18 already-made MISRA-friendly simplification this module also follows).
19
20Both scheme's bytes are wrapped identically at the TRANSPORT level by one
21new envelope kind, RBC_MSG_ERTMS_ENVELOPE (rbc_wire_types.h) - see that
22kind's own doc: byte 0 = the kind (22 - moved from 21 after a real
23collision with this project's own flat TRAIN_CONNECT=21, see
24rbc_wire_types.h's/rbc_messages.py's own comments), byte 1 = train_id, bytes 2..
25= this module's own encode()/decode() output verbatim. rbc_messages.py's
26MessageCatalog is what actually builds/parses that 2-byte prefix (its own
27"ertms" wireFormat branch) - this module only ever sees/produces the
28bytes AFTER that prefix, same "SimCore doesn't know kind numbers"
29boundary rbc_wire.py's own module doc draws for the flat scheme.
30
31Real NID_MESSAGE/NID_PACKET numbers only, never this project's own flat-
32scheme project-specific numbering (rbc_wire.py's `kind` byte) - see
33ab_gp_ertms_train_interface.h/ab_gp_ertms_train_packets.h for the C-side
34`#define`s these mirror.
35"""
36
37import struct
38
39# ---- Real NID_MESSAGE numbers (Subset-026 Ch. 8) --------------------------
40# Train -> RBC (this module's own ENCODE side - what TrainRBCSim originates)
41MSG_VALIDATED_TRAIN_DATA = 129
42MSG_REQUEST_FOR_SHUNTING = 130
43MSG_MA_REQUEST = 132
44MSG_POSITION_REPORT = 136
45MSG_ACKNOWLEDGEMENT = 146
46MSG_ACK_EMERGENCY_STOP = 147
47MSG_INIT_SESSION = 155
48MSG_TERMINATE_SESSION = 156
49MSG_SESSION_ESTABLISHED = 159
50# RBC -> Train (this module's own DECODE side - what TrainRBCSim receives)
51MSG_MOVEMENT_AUTHORITY = 3
52MSG_UNCONDITIONAL_EMERGENCY_STOP = 16
53MSG_REVOCATION_EMERGENCY_STOP = 18
54MSG_GENERAL_MESSAGE = 24
55
56# ---- Real NID_PACKET numbers (Subset-026 Ch. 7) ----------------------------
57PACKET_NATIONAL_VALUES = 3
58PACKET_LINKING_INFO = 5
59PACKET_LEVEL_2_3_MA = 15
60PACKET_GRADIENT_PROFILE = 21
61PACKET_STATIC_SPEED_PROFILE = 27
62PACKET_MA_REQUEST_PARAMS = 57
63PACKET_POSITION_REPORT_PARAMS = 58
64PACKET_TEMPORARY_SPEED_RESTRICT = 65
65PACKET_TSR_REVOCATION = 66
66PACKET_TEXT_MESSAGE = 72
67
68_HEADER_T2T = struct.Struct(">BHII") # nid_message, l_message, t_train, nid_engine (11 bytes)
69_HEADER_C2T = struct.Struct(">BHIBHH") # nid_message, l_message, t_train, m_ack, nid_c, nid_bg (12 bytes)
70# Full Packet 0 field set, in rbc_packet_0_position_report_t's own declared
71# order (ab_gp_ertms_train_packets.h) - q_scale, nid_c, nid_bg, d_lrbg,
72# q_dirlrbg, q_dlrbg, l_doubtover, l_doubtunder, v_train, m_mode, m_level
73# (23 bytes). Extended this session, on direct request, from a reduced
74# subset (nid_c/nid_bg/d_lrbg/v_train/m_mode/m_level only, 12 bytes) that
75# never modeled q_scale/q_dirlrbg/q_dlrbg/l_doubtover/l_doubtunder at all -
76# see ab_gp_ertms_train_packet_codec.c's own rbc_packet_read_0_position_
77# report() doc, which this MUST stay byte-identical to.
78_POSITION_REPORT = struct.Struct(">BHHIBBIIHBB")
79_POSITION_REPORT_PACKET1 = struct.Struct(">HHHHIHBB") # nid_c, nid_bg, prv_nid_c, prv_nid_bg,
80 # d_lrbg, v_train, m_mode, m_level (16 bytes) -
81 # Packet 1 does NOT carry q_scale/q_dirlrbg/
82 # q_dlrbg/l_doubtover/l_doubtunder (no such
83 # fields in rbc_packet_1_position_report_2bg_t).
84
85# Real Subset-026 Q_SCALE/Q_DIRLRBG/Q_DLRBG enum values (ab_gp_ertms_types.h) -
86# mirrored here as plain ints (no shared C/Python enum binding, same
87# "by-hand port" precedent this whole module already follows).
88Q_SCALE_10CM = 0
89Q_SCALE_1M = 1
90Q_SCALE_10M = 2
91Q_DIRLRBG_REVERSE = 0
92Q_DIRLRBG_NOMINAL = 1
93Q_DIRLRBG_UNKNOWN = 2
94Q_DLRBG_REVERSE = 0
95Q_DLRBG_NOMINAL = 1
96Q_DLRBG_UNKNOWN = 2
97
98# Real Subset-026 NID_PACKET values for Msg 136 specifically (Packet 0 vs
99# Packet 1 - see ab_gp_ertms_train_packets.h's own RBC_PACKET_0_POSITION_
100# REPORT/RBC_PACKET_1_POSITION_REPORT_2BG). Msg 129/132 never carry this
101# leading byte at all - only Msg 136's own decode case reads one, mirror
102# that exactly here (see rbc_train_codec_decode_train_to_track()'s own
103# RBC_MSG_TRAIN_TO_TRACK_POSITION_REPORT case).
104PACKET_0_POSITION_REPORT = 0
105PACKET_1_POSITION_REPORT_2BG = 1
106
107
109 """@param pos: dict with the full Packet 0 field set - q_scale, nid_c,
110 nid_bg, d_lrbg, q_dirlrbg, q_dlrbg, l_doubtover, l_doubtunder, v_train,
111 m_mode, m_level - matching rbc_packet_read_0_position_report()'s own
112 field order exactly (ab_gp_ertms_train_packet_codec.c)."""
113 return _POSITION_REPORT.pack(pos["q_scale"], pos["nid_c"], pos["nid_bg"], pos["d_lrbg"],
114 pos["q_dirlrbg"], pos["q_dlrbg"], pos["l_doubtover"], pos["l_doubtunder"],
115 pos["v_train"], pos["m_mode"], pos["m_level"])
116
117
118def _unpack_position_report(buf, offset):
119 """Unused today (this sim only ENCODES Packet 0, as the message
120 originator - see _pack_position_report()'s own doc) - kept for
121 encode/decode symmetry with the rest of this module."""
122 (q_scale, nid_c, nid_bg, d_lrbg, q_dirlrbg, q_dlrbg, l_doubtover, l_doubtunder,
123 v_train, m_mode, m_level) = _POSITION_REPORT.unpack_from(buf, offset)
124 return {"q_scale": q_scale, "nid_c": nid_c, "nid_bg": nid_bg, "d_lrbg": d_lrbg,
125 "q_dirlrbg": q_dirlrbg, "q_dlrbg": q_dlrbg, "l_doubtover": l_doubtover,
126 "l_doubtunder": l_doubtunder, "v_train": v_train, "m_mode": m_mode,
127 "m_level": m_level}, offset + _POSITION_REPORT.size
128
129
130def _finalize(nid_message, body_after_header, nid_engine, t_train):
131 """Builds the full Train-to-Track frame and patches l_message with the
132 real total length, exactly matching
133 rbc_train_codec_encode_track_to_train()'s own "l_message computed
134 here, not caller-supplied" fix on the C side - never trust a
135 placeholder length, always the real encoded size."""
136 header = _HEADER_T2T.pack(nid_message, 0, t_train, nid_engine)
137 frame = bytearray(header + body_after_header)
138 l_message = len(frame)
139 frame[1:3] = struct.pack(">H", l_message)
140 return bytes(frame)
141
142
143# ---- Train -> RBC encoders (what TrainRBCSim sends) ------------------------
144
145def encode_msg_155(nid_engine, t_train=0):
146 """Msg 155: Initiation of a Communication Session - header only."""
147 return _finalize(MSG_INIT_SESSION, b"", nid_engine, t_train)
148
149
150def encode_msg_156(nid_engine, t_train=0):
151 """Msg 156: Termination of a Communication Session - header only."""
152 return _finalize(MSG_TERMINATE_SESSION, b"", nid_engine, t_train)
153
154
155def encode_msg_159(nid_engine, m_version, t_train=0):
156 """Msg 159: Session Established."""
157 return _finalize(MSG_SESSION_ESTABLISHED, struct.pack(">B", m_version), nid_engine, t_train)
158
159
160def encode_msg_146(nid_engine, t_train_ack, t_train=0):
161 """Msg 146: Acknowledgement - real Subset-026 146 acks by TIMESTAMP of
162 the acked message, not a sequence number (see message_catalog.json's
163 own _subset026Reference comment on this)."""
164 return _finalize(MSG_ACKNOWLEDGEMENT, struct.pack(">I", t_train_ack), nid_engine, t_train)
165
166
167def encode_msg_136(nid_engine, position_report, t_train=0):
168 """Msg 136: Train Position Report, Packet 0 - the normal active-mission
169 cadence. Real Subset-026 Msg 136 self-identifies which packet variant
170 it carries with a leading NID_PACKET byte (see
171 rbc_train_codec_decode_train_to_track()'s own Msg 136 case) - this
172 encoder writes PACKET_0_POSITION_REPORT then the Packet 0 body."""
173 body = struct.pack(">B", PACKET_0_POSITION_REPORT) + _pack_position_report(position_report)
174 return _finalize(MSG_POSITION_REPORT, body, nid_engine, t_train)
175
176
177def encode_msg_136_packet1(nid_engine, packet1_report, t_train=0):
178 """Msg 136: Train Position Report, Packet 1 (2-balise-group variant) -
179 the post-EoM idle heartbeat cadence (see train_sim.py's own post-EoM
180 phase tracking). @param packet1_report: dict with nid_c, nid_bg,
181 prv_nid_c, prv_nid_bg, d_lrbg, v_train, m_mode, m_level - same field
182 set read_position_report_packet1() reads on the C side."""
183 body = struct.pack(">B", PACKET_1_POSITION_REPORT_2BG) + _POSITION_REPORT_PACKET1.pack(
184 packet1_report["nid_c"], packet1_report["nid_bg"],
185 packet1_report["prv_nid_c"], packet1_report["prv_nid_bg"],
186 packet1_report["d_lrbg"], packet1_report["v_train"],
187 packet1_report["m_mode"], packet1_report["m_level"])
188 return _finalize(MSG_POSITION_REPORT, body, nid_engine, t_train)
189
190
191def encode_msg_132(nid_engine, q_marqstreason, position_report, t_train=0):
192 """Msg 132: MA Request - Q_MARQSTREASON then Packet 0."""
193 body = struct.pack(">B", q_marqstreason) + _pack_position_report(position_report)
194 return _finalize(MSG_MA_REQUEST, body, nid_engine, t_train)
195
196
197def encode_msg_129(nid_engine, position_report, l_train, v_maxtrain, m_loadinggauge,
198 m_axleload, m_airtight, t_train=0):
199 """Msg 129: Validated Train Data - Packet 0 then the flat train-data
200 fields (same field subset ab_gp_ertms_train_codec.c's own decode
201 reads)."""
202 body = (_pack_position_report(position_report) +
203 struct.pack(">IHBBB", l_train, v_maxtrain, m_loadinggauge, m_axleload,
204 1 if m_airtight else 0))
205 return _finalize(MSG_VALIDATED_TRAIN_DATA, body, nid_engine, t_train)
206
207
208def encode_msg_130(nid_engine, position_report, t_train=0):
209 """Msg 130: Request for Shunting - Packet 0."""
210 return _finalize(MSG_REQUEST_FOR_SHUNTING, _pack_position_report(position_report), nid_engine, t_train)
211
212
213def encode_msg_147(nid_engine, nid_em, q_emergency_stop, position_report, t_train=0):
214 """Msg 147: Ack Emergency Stop - NID_EM, Q_EMERGENCY_STOP then Packet 0."""
215 body = struct.pack(">BB", nid_em, q_emergency_stop) + _pack_position_report(position_report)
216 return _finalize(MSG_ACK_EMERGENCY_STOP, body, nid_engine, t_train)
217
218
219def encode_msg_150(nid_engine, position_report, t_train=0):
220 """Msg 150: End of Mission - Packet 0."""
221 return _finalize(150, _pack_position_report(position_report), nid_engine, t_train)
222
223
224def encode_msg_154(nid_engine, t_train=0):
225 """Msg 154: No compatible version supported - header only."""
226 return _finalize(154, b"", nid_engine, t_train)
227
228
229def encode_msg_157(nid_engine, q_status, position_report, t_train=0):
230 """Msg 157: SoM Position Report - Q_STATUS then Packet 0."""
231 body = struct.pack(">B", q_status) + _pack_position_report(position_report)
232 return _finalize(157, body, nid_engine, t_train)
233
234
235def encode_msg_158(nid_engine, nid_textmessage, position_report, t_train=0):
236 """Msg 158: Text Message Ack - NID_TEXTMESSAGE then Packet 0."""
237 body = struct.pack(">B", nid_textmessage) + _pack_position_report(position_report)
238 return _finalize(158, body, nid_engine, t_train)
239
240
241# ---- RBC -> Train decoders (what TrainRBCSim receives) ---------------------
242
243class ErtmsDecodeError(ValueError):
244 """@payload could not be decoded as a real Subset-026 track-to-train
245 message - malformed length, unknown nid_message, or an unrecognized
246 NID_PACKET inside a variable-length message (Msg 3/24)."""
247
248
250 if len(buf) < _HEADER_C2T.size:
251 raise ErtmsDecodeError(f"buffer too short for track-to-train header: {len(buf)} bytes")
252 nid_message, l_message, t_train, m_ack, nid_c, nid_bg = _HEADER_C2T.unpack_from(buf, 0)
253 if l_message > len(buf):
254 raise ErtmsDecodeError(f"l_message={l_message} exceeds buffer length {len(buf)}")
255 header = {"nid_message": nid_message, "l_message": l_message, "t_train": t_train,
256 "m_ack": m_ack, "nid_lrbg": {"nid_c": nid_c, "nid_bg": nid_bg}}
257 return header, _HEADER_C2T.size
258
259
261 """Msg 3: Movement Authority. @return dict with header, ma (Packet 15,
262 always present), and optional gradient/ssp/linking (Packets 21/27/5) -
263 same has_X/X pairing ab_gp_ertms_train_interface.h's own
264 rbc_msg_3_movement_authority_t struct uses."""
265 header, offset = _decode_header_c2t(buf)
266 if header["nid_message"] != MSG_MOVEMENT_AUTHORITY:
267 raise ErtmsDecodeError(f"decode_msg_3 called on nid_message={header['nid_message']}")
268 l_message = header["l_message"]
269
270 packet_id, = struct.unpack_from(">B", buf, offset)
271 offset += 1
272 if packet_id != PACKET_LEVEL_2_3_MA:
273 raise ErtmsDecodeError(f"Msg 3 expected Packet 15 first, got {packet_id}")
274 ma_seq, ma_length, v_loa, t_loa_s = struct.unpack_from(">IIHI", buf, offset)
275 offset += 14
276 result = {"header": header,
277 "ma": {"ma_seq": ma_seq, "ma_length": ma_length, "v_loa": v_loa, "t_loa_s": t_loa_s},
278 "has_gradient": False, "gradient": None,
279 "has_ssp": False, "ssp": None,
280 "has_linking": False, "linking": None,
281 "packets_present": ["P15"]}
282
283 while offset < l_message:
284 packet_id, = struct.unpack_from(">B", buf, offset)
285 offset += 1
286 if packet_id == PACKET_GRADIENT_PROFILE:
287 count, = struct.unpack_from(">B", buf, offset)
288 offset += 1
289 segments = []
290 for _ in range(count):
291 d_gradient, g_a = struct.unpack_from(">IB", buf, offset)
292 offset += 5
293 segments.append({"d_gradient": d_gradient, "g_a": g_a})
294 result["has_gradient"] = True
295 result["gradient"] = {"segment_count": count, "segments": segments}
296 result["packets_present"].append("P21")
297 elif packet_id == PACKET_STATIC_SPEED_PROFILE:
298 count, = struct.unpack_from(">B", buf, offset)
299 offset += 1
300 segments = []
301 for _ in range(count):
302 d_static, v_static = struct.unpack_from(">IB", buf, offset)
303 offset += 5
304 segments.append({"d_static": d_static, "v_static": v_static})
305 result["has_ssp"] = True
306 result["ssp"] = {"segment_count": count, "segments": segments}
307 result["packets_present"].append("P27")
308 elif packet_id == PACKET_LINKING_INFO:
309 count, = struct.unpack_from(">B", buf, offset)
310 offset += 1
311 links = []
312 for _ in range(count):
313 nid_bg, d_link, q_linkdir = struct.unpack_from(">HIB", buf, offset)
314 offset += 7
315 links.append({"nid_bg": nid_bg, "d_link": d_link, "q_linkdir": q_linkdir})
316 result["has_linking"] = True
317 result["linking"] = {"link_count": count, "links": links}
318 result["packets_present"].append("P5")
319 else:
320 raise ErtmsDecodeError(f"Msg 3: unrecognized NID_PACKET {packet_id} at offset {offset - 1}")
321 return result
322
323
325 """Msg 24: General Message. @return dict with header and whichever of
326 Packet 3/57/58/65/66/72 were present - same has_X/X pairing
327 ab_gp_ertms_train_interface.h's own rbc_msg_24_general_message_t
328 struct uses."""
329 header, offset = _decode_header_c2t(buf)
330 if header["nid_message"] != MSG_GENERAL_MESSAGE:
331 raise ErtmsDecodeError(f"decode_msg_24 called on nid_message={header['nid_message']}")
332 l_message = header["l_message"]
333
334 result = {"header": header,
335 "has_national_values": False, "national_values": None,
336 "has_ma_request_params": False, "ma_request_params": None,
337 "has_pr_params": False, "pr_params": None,
338 "has_tsr": False, "tsr": None,
339 "has_tsr_revoke": False, "tsr_revoke": None,
340 "has_text_message": False, "text_message": None,
341 "packets_present": []}
342
343 while offset < l_message:
344 packet_id, = struct.unpack_from(">B", buf, offset)
345 offset += 1
346 if packet_id == PACKET_NATIONAL_VALUES:
347 v_nvshunt, v_nvstff, v_nvunfit, t_nvcontact, d_nvroll = struct.unpack_from(">HHHHH", buf, offset)
348 offset += 10
349 result["has_national_values"] = True
350 result["national_values"] = {"v_nvshunt": v_nvshunt, "v_nvstff": v_nvstff,
351 "v_nvunfit": v_nvunfit, "t_nvcontact": t_nvcontact,
352 "d_nvroll": d_nvroll}
353 result["packets_present"].append("P3")
354 elif packet_id == PACKET_MA_REQUEST_PARAMS:
355 t_mar_s, d_mar_m = struct.unpack_from(">II", buf, offset)
356 offset += 8
357 result["has_ma_request_params"] = True
358 result["ma_request_params"] = {"t_mar_s": t_mar_s, "d_mar_m": d_mar_m}
359 result["packets_present"].append("P57")
360 elif packet_id == PACKET_POSITION_REPORT_PARAMS:
361 t_cycloc_s, d_cycloc_m = struct.unpack_from(">II", buf, offset)
362 offset += 8
363 result["has_pr_params"] = True
364 result["pr_params"] = {"t_cycloc_s": t_cycloc_s, "d_cycloc_m": d_cycloc_m}
365 result["packets_present"].append("P58")
366 elif packet_id == PACKET_TEMPORARY_SPEED_RESTRICT:
367 nid_tsr, d_tsr, l_tsr, v_tsr, q_front = struct.unpack_from(">HIIHB", buf, offset)
368 offset += 13
369 result["has_tsr"] = True
370 result["tsr"] = {"nid_tsr": nid_tsr, "d_tsr": d_tsr, "l_tsr": l_tsr,
371 "v_tsr": v_tsr, "q_front": q_front}
372 result["packets_present"].append("P65")
373 elif packet_id == PACKET_TSR_REVOCATION:
374 nid_tsr, = struct.unpack_from(">H", buf, offset)
375 offset += 2
376 result["has_tsr_revoke"] = True
377 result["tsr_revoke"] = {"nid_tsr": nid_tsr}
378 result["packets_present"].append("P66")
379 elif packet_id == PACKET_TEXT_MESSAGE:
380 ack_required, duration_s, text_len = struct.unpack_from(">BIB", buf, offset)
381 offset += 6
382 text = buf[offset:offset + text_len].decode("ascii", errors="replace")
383 offset += text_len
384 result["has_text_message"] = True
385 result["text_message"] = {"ack_required": bool(ack_required), "duration_s": duration_s,
386 "text": text}
387 result["packets_present"].append("P72")
388 else:
389 raise ErtmsDecodeError(f"Msg 24: unrecognized NID_PACKET {packet_id} at offset {offset - 1}")
390 return result
391
392
393_DECODERS = {
394 MSG_MOVEMENT_AUTHORITY: decode_msg_3,
395 MSG_GENERAL_MESSAGE: decode_msg_24,
396}
397
398
399def decode(buf):
400 """@return (nid_message, decoded_dict) by peeking buf[0], dispatching
401 to the matching decode_msg_*() above. Raises ErtmsDecodeError for an
402 empty buffer or an nid_message this module doesn't yet decode
403 (Msg 16/18 - Emergency Stop - have real C-side encoders already but
404 no Python decoder yet, out of scope for the SoM/Running/EoM flow)."""
405 if len(buf) < 1:
406 raise ErtmsDecodeError("empty buffer")
407 nid_message = buf[0]
408 decoder = _DECODERS.get(nid_message)
409 if decoder is None:
410 raise ErtmsDecodeError(f"no Python decoder registered for nid_message={nid_message}")
411 return nid_message, decoder(buf)
encode_msg_159(nid_engine, m_version, t_train=0)
encode_msg_146(nid_engine, t_train_ack, t_train=0)
_finalize(nid_message, body_after_header, nid_engine, t_train)
encode_msg_157(nid_engine, q_status, position_report, t_train=0)
encode_msg_130(nid_engine, position_report, t_train=0)
encode_msg_136(nid_engine, position_report, t_train=0)
encode_msg_155(nid_engine, t_train=0)
encode_msg_150(nid_engine, position_report, t_train=0)
encode_msg_158(nid_engine, nid_textmessage, position_report, t_train=0)
encode_msg_154(nid_engine, t_train=0)
encode_msg_132(nid_engine, q_marqstreason, position_report, t_train=0)
encode_msg_136_packet1(nid_engine, packet1_report, t_train=0)
encode_msg_156(nid_engine, t_train=0)
encode_msg_129(nid_engine, position_report, l_train, v_maxtrain, m_loadinggauge, m_axleload, m_airtight, t_train=0)
encode_msg_147(nid_engine, nid_em, q_emergency_stop, position_report, t_train=0)
_unpack_position_report(buf, offset)