1"""Real Subset-026 ERTMS message codec - Python mirror of
2RBC_GP/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.
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).
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 "RBC_Test_Sim_Core doesn't know kind numbers"
29boundary rbc_wire.py's own module doc draws for the flat scheme.
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.
41MSG_VALIDATED_TRAIN_DATA = 129
42MSG_REQUEST_FOR_SHUNTING = 130
44MSG_POSITION_REPORT = 136
45MSG_ACKNOWLEDGEMENT = 146
46MSG_ACK_EMERGENCY_STOP = 147
48MSG_TERMINATE_SESSION = 156
49MSG_SESSION_ESTABLISHED = 159
51MSG_MOVEMENT_AUTHORITY = 3
52MSG_UNCONDITIONAL_EMERGENCY_STOP = 16
53MSG_REVOCATION_EMERGENCY_STOP = 18
54MSG_GENERAL_MESSAGE = 24
59PACKET_NATIONAL_VALUES = 3
60PACKET_LINKING_INFO = 5
61PACKET_LEVEL_2_3_MA = 15
62PACKET_GRADIENT_PROFILE = 21
63PACKET_STATIC_SPEED_PROFILE = 27
64PACKET_MA_REQUEST_PARAMS = 57
65PACKET_POSITION_REPORT_PARAMS = 58
66PACKET_TEMPORARY_SPEED_RESTRICT = 65
67PACKET_TSR_REVOCATION = 66
68PACKET_TEXT_MESSAGE = 72
70_HEADER_T2T = struct.Struct(
">BHII")
71_HEADER_C2T = struct.Struct(
">BHIBHH")
80_POSITION_REPORT = struct.Struct(
">BHHIBBIIHBB")
81_POSITION_REPORT_PACKET1 = struct.Struct(
">HHHHIHBB")
106PACKET_0_POSITION_REPORT = 0
107PACKET_1_POSITION_REPORT_2BG = 1
111 """@param pos: dict with the full Packet 0 field set - q_scale, nid_c,
112 nid_bg, d_lrbg, q_dirlrbg, q_dlrbg, l_doubtover, l_doubtunder, v_train,
113 m_mode, m_level - matching rbc_packet_read_0_position_report()'s own
114 field order exactly (ab_gp_ertms_train_packet_codec.c)."""
115 return _POSITION_REPORT.pack(pos[
"q_scale"], pos[
"nid_c"], pos[
"nid_bg"], pos[
"d_lrbg"],
116 pos[
"q_dirlrbg"], pos[
"q_dlrbg"], pos[
"l_doubtover"], pos[
"l_doubtunder"],
117 pos[
"v_train"], pos[
"m_mode"], pos[
"m_level"])
121 """Unused today (this sim only ENCODES Packet 0, as the message
122 originator - see _pack_position_report()'s own doc) - kept for
123 encode/decode symmetry with the rest of this module."""
124 (q_scale, nid_c, nid_bg, d_lrbg, q_dirlrbg, q_dlrbg, l_doubtover, l_doubtunder,
125 v_train, m_mode, m_level) = _POSITION_REPORT.unpack_from(buf, offset)
126 return {
"q_scale": q_scale,
"nid_c": nid_c,
"nid_bg": nid_bg,
"d_lrbg": d_lrbg,
127 "q_dirlrbg": q_dirlrbg,
"q_dlrbg": q_dlrbg,
"l_doubtover": l_doubtover,
128 "l_doubtunder": l_doubtunder,
"v_train": v_train,
"m_mode": m_mode,
129 "m_level": m_level}, offset + _POSITION_REPORT.size
132def _finalize(nid_message, body_after_header, nid_engine, t_train):
133 """Builds the full Train-to-Track frame and patches l_message with the
134 real total length, exactly matching
135 rbc_train_codec_encode_track_to_train()'s own "l_message computed
136 here, not caller-supplied" fix on the C side - never trust a
137 placeholder length, always the real encoded size."""
138 header = _HEADER_T2T.pack(nid_message, 0, t_train, nid_engine)
139 frame = bytearray(header + body_after_header)
140 l_message = len(frame)
141 frame[1:3] = struct.pack(
">H", l_message)
148 """Msg 155: Initiation of a Communication Session - header only."""
149 return _finalize(MSG_INIT_SESSION, b
"", nid_engine, t_train)
153 """Msg 156: Termination of a Communication Session - header only."""
154 return _finalize(MSG_TERMINATE_SESSION, b
"", nid_engine, t_train)
158 """Msg 159: Session Established."""
159 return _finalize(MSG_SESSION_ESTABLISHED, struct.pack(
">B", m_version), nid_engine, t_train)
163 """Msg 146: Acknowledgement - real Subset-026 146 acks by TIMESTAMP of
164 the acked message, not a sequence number (see message_catalog.json's
165 own _subset026Reference comment on this)."""
166 return _finalize(MSG_ACKNOWLEDGEMENT, struct.pack(
">I", t_train_ack), nid_engine, t_train)
170 """Msg 136: Train Position Report, Packet 0 - the normal active-mission
171 cadence. Real Subset-026 Msg 136 self-identifies which packet variant
172 it carries with a leading NID_PACKET byte (see
173 rbc_train_codec_decode_train_to_track()'s own Msg 136 case) - this
174 encoder writes PACKET_0_POSITION_REPORT then the Packet 0 body."""
176 return _finalize(MSG_POSITION_REPORT, body, nid_engine, t_train)
180 """Msg 136: Train Position Report, Packet 1 (2-balise-group variant) -
181 the post-EoM idle heartbeat cadence (see train_sim.py's own post-EoM
182 phase tracking). @param packet1_report: dict with nid_c, nid_bg,
183 prv_nid_c, prv_nid_bg, d_lrbg, v_train, m_mode, m_level - same field
184 set read_position_report_packet1() reads on the C side."""
185 body = struct.pack(
">B", PACKET_1_POSITION_REPORT_2BG) + _POSITION_REPORT_PACKET1.pack(
186 packet1_report[
"nid_c"], packet1_report[
"nid_bg"],
187 packet1_report[
"prv_nid_c"], packet1_report[
"prv_nid_bg"],
188 packet1_report[
"d_lrbg"], packet1_report[
"v_train"],
189 packet1_report[
"m_mode"], packet1_report[
"m_level"])
190 return _finalize(MSG_POSITION_REPORT, body, nid_engine, t_train)
194 """Msg 132: MA Request - Q_MARQSTREASON then Packet 0."""
196 return _finalize(MSG_MA_REQUEST, body, nid_engine, t_train)
199def encode_msg_129(nid_engine, position_report, l_train, v_maxtrain, m_loadinggauge,
200 m_axleload, m_airtight, t_train=0):
201 """Msg 129: Validated Train Data - Packet 0 then the flat train-data
202 fields (same field subset ab_gp_ertms_train_codec.c's own decode
205 struct.pack(
">IHBBB", l_train, v_maxtrain, m_loadinggauge, m_axleload,
206 1
if m_airtight
else 0))
207 return _finalize(MSG_VALIDATED_TRAIN_DATA, body, nid_engine, t_train)
211 """Msg 130: Request for Shunting - Packet 0."""
215def encode_msg_147(nid_engine, nid_em, q_emergency_stop, position_report, t_train=0):
216 """Msg 147: Ack Emergency Stop - NID_EM, Q_EMERGENCY_STOP then Packet 0."""
218 return _finalize(MSG_ACK_EMERGENCY_STOP, body, nid_engine, t_train)
222 """Msg 150: End of Mission - Packet 0."""
227 """Msg 154: No compatible version supported - header only."""
228 return _finalize(154, b
"", nid_engine, t_train)
232 """Msg 157: SoM Position Report - Q_STATUS then Packet 0."""
234 return _finalize(157, body, nid_engine, t_train)
238 """Msg 158: Text Message Ack - NID_TEXTMESSAGE then Packet 0."""
240 return _finalize(158, body, nid_engine, t_train)
246 """@payload could not be decoded as a real Subset-026 track-to-train
247 message - malformed length, unknown nid_message, or an unrecognized
248 NID_PACKET inside a variable-length message (Msg 3/24)."""
252 if len(buf) < _HEADER_C2T.size:
253 raise ErtmsDecodeError(f
"buffer too short for track-to-train header: {len(buf)} bytes")
254 nid_message, l_message, t_train, m_ack, nid_c, nid_bg = _HEADER_C2T.unpack_from(buf, 0)
255 if l_message > len(buf):
256 raise ErtmsDecodeError(f
"l_message={l_message} exceeds buffer length {len(buf)}")
257 header = {
"nid_message": nid_message,
"l_message": l_message,
"t_train": t_train,
258 "m_ack": m_ack,
"nid_lrbg": {
"nid_c": nid_c,
"nid_bg": nid_bg}}
259 return header, _HEADER_C2T.size
263 """Msg 3: Movement Authority. @return dict with header, ma (Packet 15,
264 always present), and optional gradient/ssp/linking (Packets 21/27/5) -
265 same has_X/X pairing ab_gp_ertms_train_interface.h's own
266 rbc_msg_3_movement_authority_t struct uses."""
268 if header[
"nid_message"] != MSG_MOVEMENT_AUTHORITY:
269 raise ErtmsDecodeError(f
"decode_msg_3 called on nid_message={header['nid_message']}")
270 l_message = header[
"l_message"]
272 packet_id, = struct.unpack_from(
">B", buf, offset)
274 if packet_id != PACKET_LEVEL_2_3_MA:
276 ma_seq, ma_length, v_loa, t_loa_s = struct.unpack_from(
">IIHI", buf, offset)
278 result = {
"header": header,
279 "ma": {
"ma_seq": ma_seq,
"ma_length": ma_length,
"v_loa": v_loa,
"t_loa_s": t_loa_s},
280 "has_gradient":
False,
"gradient":
None,
281 "has_ssp":
False,
"ssp":
None,
282 "has_linking":
False,
"linking":
None,
283 "packets_present": [
"P15"]}
285 while offset < l_message:
286 packet_id, = struct.unpack_from(
">B", buf, offset)
288 if packet_id == PACKET_GRADIENT_PROFILE:
289 count, = struct.unpack_from(
">B", buf, offset)
292 for _
in range(count):
293 d_gradient, g_a = struct.unpack_from(
">IB", buf, offset)
295 segments.append({
"d_gradient": d_gradient,
"g_a": g_a})
296 result[
"has_gradient"] =
True
297 result[
"gradient"] = {
"segment_count": count,
"segments": segments}
298 result[
"packets_present"].append(
"P21")
299 elif packet_id == PACKET_STATIC_SPEED_PROFILE:
300 count, = struct.unpack_from(
">B", buf, offset)
303 for _
in range(count):
304 d_static, v_static = struct.unpack_from(
">IB", buf, offset)
306 segments.append({
"d_static": d_static,
"v_static": v_static})
307 result[
"has_ssp"] =
True
308 result[
"ssp"] = {
"segment_count": count,
"segments": segments}
309 result[
"packets_present"].append(
"P27")
310 elif packet_id == PACKET_LINKING_INFO:
311 count, = struct.unpack_from(
">B", buf, offset)
314 for _
in range(count):
315 nid_bg, d_link, q_linkdir = struct.unpack_from(
">HIB", buf, offset)
317 links.append({
"nid_bg": nid_bg,
"d_link": d_link,
"q_linkdir": q_linkdir})
318 result[
"has_linking"] =
True
319 result[
"linking"] = {
"link_count": count,
"links": links}
320 result[
"packets_present"].append(
"P5")
322 raise ErtmsDecodeError(f
"Msg 3: unrecognized NID_PACKET {packet_id} at offset {offset - 1}")
327 """Msg 24: General Message. @return dict with header and whichever of
328 Packet 3/57/58/65/66/72 were present - same has_X/X pairing
329 ab_gp_ertms_train_interface.h's own rbc_msg_24_general_message_t
332 if header[
"nid_message"] != MSG_GENERAL_MESSAGE:
333 raise ErtmsDecodeError(f
"decode_msg_24 called on nid_message={header['nid_message']}")
334 l_message = header[
"l_message"]
336 result = {
"header": header,
337 "has_national_values":
False,
"national_values":
None,
338 "has_ma_request_params":
False,
"ma_request_params":
None,
339 "has_pr_params":
False,
"pr_params":
None,
340 "has_tsr":
False,
"tsr":
None,
341 "has_tsr_revoke":
False,
"tsr_revoke":
None,
342 "has_text_message":
False,
"text_message":
None,
343 "packets_present": []}
345 while offset < l_message:
346 packet_id, = struct.unpack_from(
">B", buf, offset)
348 if packet_id == PACKET_NATIONAL_VALUES:
349 v_nvshunt, v_nvstff, v_nvunfit, t_nvcontact, d_nvroll = struct.unpack_from(
">HHHHH", buf, offset)
351 result[
"has_national_values"] =
True
352 result[
"national_values"] = {
"v_nvshunt": v_nvshunt,
"v_nvstff": v_nvstff,
353 "v_nvunfit": v_nvunfit,
"t_nvcontact": t_nvcontact,
354 "d_nvroll": d_nvroll}
355 result[
"packets_present"].append(
"P3")
356 elif packet_id == PACKET_MA_REQUEST_PARAMS:
357 t_mar_s, d_mar_m = struct.unpack_from(
">II", buf, offset)
359 result[
"has_ma_request_params"] =
True
360 result[
"ma_request_params"] = {
"t_mar_s": t_mar_s,
"d_mar_m": d_mar_m}
361 result[
"packets_present"].append(
"P57")
362 elif packet_id == PACKET_POSITION_REPORT_PARAMS:
363 t_cycloc_s, d_cycloc_m = struct.unpack_from(
">II", buf, offset)
365 result[
"has_pr_params"] =
True
366 result[
"pr_params"] = {
"t_cycloc_s": t_cycloc_s,
"d_cycloc_m": d_cycloc_m}
367 result[
"packets_present"].append(
"P58")
368 elif packet_id == PACKET_TEMPORARY_SPEED_RESTRICT:
369 nid_tsr, d_tsr, l_tsr, v_tsr, q_front = struct.unpack_from(
">HIIHB", buf, offset)
371 result[
"has_tsr"] =
True
372 result[
"tsr"] = {
"nid_tsr": nid_tsr,
"d_tsr": d_tsr,
"l_tsr": l_tsr,
373 "v_tsr": v_tsr,
"q_front": q_front}
374 result[
"packets_present"].append(
"P65")
375 elif packet_id == PACKET_TSR_REVOCATION:
376 nid_tsr, = struct.unpack_from(
">H", buf, offset)
378 result[
"has_tsr_revoke"] =
True
379 result[
"tsr_revoke"] = {
"nid_tsr": nid_tsr}
380 result[
"packets_present"].append(
"P66")
381 elif packet_id == PACKET_TEXT_MESSAGE:
382 ack_required, duration_s, text_len = struct.unpack_from(
">BIB", buf, offset)
384 text = buf[offset:offset + text_len].
decode(
"ascii", errors=
"replace")
386 result[
"has_text_message"] =
True
387 result[
"text_message"] = {
"ack_required": bool(ack_required),
"duration_s": duration_s,
389 result[
"packets_present"].append(
"P72")
391 raise ErtmsDecodeError(f
"Msg 24: unrecognized NID_PACKET {packet_id} at offset {offset - 1}")
396 """Msg 27 SH Refused (Subset-026 5.6): header + the T_TRAIN of the refused Msg 130
397 (32-bit, same shape as Msg 8)."""
399 if header[
"nid_message"] != MSG_SH_REFUSED:
400 raise ErtmsDecodeError(f
"decode_msg_27 called on nid_message={header['nid_message']}")
401 if len(buf) < offset + 4:
403 (t_train_ref,) = struct.unpack_from(
">I", buf, offset)
404 return {
"header": header,
"t_train_ref": t_train_ref}
408 """Msg 28 SH Authorised (Subset-026 5.6): header + the T_TRAIN of the authorised Msg 130
409 (RBC_GP sends no optional packets)."""
411 if header[
"nid_message"] != MSG_SH_AUTHORISED:
412 raise ErtmsDecodeError(f
"decode_msg_28 called on nid_message={header['nid_message']}")
413 if len(buf) < offset + 4:
415 (t_train_ref,) = struct.unpack_from(
">I", buf, offset)
416 return {
"header": header,
"t_train_ref": t_train_ref}
420 MSG_MOVEMENT_AUTHORITY: decode_msg_3,
421 MSG_GENERAL_MESSAGE: decode_msg_24,
422 MSG_SH_REFUSED: decode_msg_27,
423 MSG_SH_AUTHORISED: decode_msg_28,
428 """@return (nid_message, decoded_dict) by peeking buf[0], dispatching
429 to the matching decode_msg_*() above. Raises ErtmsDecodeError for an
430 empty buffer or an nid_message this module doesn't yet decode
431 (Msg 16/18 - Emergency Stop - have real C-side encoders already but
432 no Python decoder yet, out of scope for the SoM/Running/EoM flow)."""
436 decoder = _DECODERS.get(nid_message)
438 raise ErtmsDecodeError(f
"no Python decoder registered for nid_message={nid_message}")
439 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)
_pack_position_report(pos)
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)