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.
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 "SimCore 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
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
68_HEADER_T2T = struct.Struct(
">BHII")
69_HEADER_C2T = struct.Struct(
">BHIBHH")
78_POSITION_REPORT = struct.Struct(
">BHHIBBIIHBB")
79_POSITION_REPORT_PACKET1 = struct.Struct(
">HHHHIHBB")
104PACKET_0_POSITION_REPORT = 0
105PACKET_1_POSITION_REPORT_2BG = 1
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"])
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
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)
146 """Msg 155: Initiation of a Communication Session - header only."""
147 return _finalize(MSG_INIT_SESSION, b
"", nid_engine, t_train)
151 """Msg 156: Termination of a Communication Session - header only."""
152 return _finalize(MSG_TERMINATE_SESSION, b
"", nid_engine, t_train)
156 """Msg 159: Session Established."""
157 return _finalize(MSG_SESSION_ESTABLISHED, struct.pack(
">B", m_version), nid_engine, t_train)
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)
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."""
174 return _finalize(MSG_POSITION_REPORT, body, nid_engine, t_train)
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)
192 """Msg 132: MA Request - Q_MARQSTREASON then Packet 0."""
194 return _finalize(MSG_MA_REQUEST, body, nid_engine, t_train)
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
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)
209 """Msg 130: Request for Shunting - Packet 0."""
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."""
216 return _finalize(MSG_ACK_EMERGENCY_STOP, body, nid_engine, t_train)
220 """Msg 150: End of Mission - Packet 0."""
225 """Msg 154: No compatible version supported - header only."""
226 return _finalize(154, b
"", nid_engine, t_train)
230 """Msg 157: SoM Position Report - Q_STATUS then Packet 0."""
232 return _finalize(157, body, nid_engine, t_train)
236 """Msg 158: Text Message Ack - NID_TEXTMESSAGE then Packet 0."""
238 return _finalize(158, body, nid_engine, t_train)
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)."""
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
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."""
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"]
270 packet_id, = struct.unpack_from(
">B", buf, offset)
272 if packet_id != PACKET_LEVEL_2_3_MA:
274 ma_seq, ma_length, v_loa, t_loa_s = struct.unpack_from(
">IIHI", buf, offset)
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"]}
283 while offset < l_message:
284 packet_id, = struct.unpack_from(
">B", buf, offset)
286 if packet_id == PACKET_GRADIENT_PROFILE:
287 count, = struct.unpack_from(
">B", buf, offset)
290 for _
in range(count):
291 d_gradient, g_a = struct.unpack_from(
">IB", buf, offset)
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)
301 for _
in range(count):
302 d_static, v_static = struct.unpack_from(
">IB", buf, offset)
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)
312 for _
in range(count):
313 nid_bg, d_link, q_linkdir = struct.unpack_from(
">HIB", buf, offset)
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")
320 raise ErtmsDecodeError(f
"Msg 3: unrecognized NID_PACKET {packet_id} at offset {offset - 1}")
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
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"]
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": []}
343 while offset < l_message:
344 packet_id, = struct.unpack_from(
">B", buf, offset)
346 if packet_id == PACKET_NATIONAL_VALUES:
347 v_nvshunt, v_nvstff, v_nvunfit, t_nvcontact, d_nvroll = struct.unpack_from(
">HHHHH", buf, offset)
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)
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)
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)
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)
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)
382 text = buf[offset:offset + text_len].
decode(
"ascii", errors=
"replace")
384 result[
"has_text_message"] =
True
385 result[
"text_message"] = {
"ack_required": bool(ack_required),
"duration_s": duration_s,
387 result[
"packets_present"].append(
"P72")
389 raise ErtmsDecodeError(f
"Msg 24: unrecognized NID_PACKET {packet_id} at offset {offset - 1}")
394 MSG_MOVEMENT_AUTHORITY: decode_msg_3,
395 MSG_GENERAL_MESSAGE: decode_msg_24,
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)."""
408 decoder = _DECODERS.get(nid_message)
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)
_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)