92 def __init__(self, catalog_path, ertms_codec=None, ertms_encoders=None, ertms_flatteners=None):
93 """@param catalog_path: path to this sim's own message_catalog.json.
94 Callers should pass an absolute path built from their own
95 __file__ (e.g. os.path.join(os.path.dirname(os.path.abspath(
96 __file__)), "message_catalog.json")) rather than a bare relative
97 name, since a relative path here would otherwise depend on the
98 process's own current working directory at startup, not on
99 where the catalog file actually lives on disk.
100 @param ertms_codec, ertms_encoders, ertms_flatteners: only needed
101 if this catalog declares any "wireFormat": "ertms" message - see
102 this module's own header doc for what each one is. Omitted
103 (None/{}) for a catalog with no ertms messages (raises at load
104 time below if that combination is wrong either way)."""
106 ertms_encoders = ertms_encoders
or {}
107 ertms_flatteners = ertms_flatteners
or {}
108 with open(path,
"r", encoding=
"utf-8")
as f:
112 has_ertms_messages =
False
114 for name, spec
in raw.get(
"messages", {}).items():
115 if name.startswith(
"P")
and (name[1:].isdigit()
or name
in (
116 "P0",
"P1",
"P2",
"P11",
"P12",
"P15",
"P65",
"P66",
"P68",
"P72",
"P131",
"P136"
118 packets_catalog[name] = spec
121 wire_format = spec.get(
"wireFormat")
122 if wire_format
is None:
123 if "kind" in spec
and not name.startswith(
"M"):
126 wire_format =
"ertms"
128 if wire_format
not in (
"flat",
"ertms"):
129 raise ValueError(f
"{path}: {name!r} has wireFormat {wire_format!r} - must be \"flat\" or \"ertms\"")
131 direction = spec.get(
"direction")
133 digits = re.findall(
r'\d+', name)
136 direction =
"send" if num >= 120
else "receive"
137 elif name
in (
"TRAIN_CONNECT",
"TRAIN_DISCONNECT"):
142 if direction
not in (
"send",
"receive"):
143 raise ValueError(f
"{path}: {name!r} has direction {direction!r} - must be \"send\" or \"receive\"")
145 if wire_format ==
"ertms":
146 nid_message = spec.get(
"nidMessage")
147 if nid_message
is None:
148 digits = re.findall(
r'\d+', name)
150 nid_message = int(digits[0])
153 if not isinstance(nid_message, int)
or isinstance(nid_message, bool)
or not (0 <= nid_message <= 255):
154 raise ValueError(f
"{path}: {name!r} (wireFormat=ertms) nidMessage={nid_message!r} "
155 "is not a plausible wire byte (0-255) - required, not optional, for ertms messages")
157 raw_fields = spec.get(
"fields", {})
158 if isinstance(raw_fields, list):
159 all_fields = {f: {}
for f
in raw_fields}
160 elif isinstance(raw_fields, dict):
161 all_fields = raw_fields
165 has_ertms_messages =
True
168 "nid_message": nid_message,
169 "direction": direction,
171 "fields": all_fields,
172 "wire_format":
"ertms",
174 messages[name] = msg_entry
175 if name.startswith(
"M")
and name[1:].isdigit():
176 messages[name[1:]] = msg_entry
178 messages[f
"M{name}"] = msg_entry
181 packets = spec.get(
"packets", {})
184 f
"{path}: {name!r} must define at least one packet in 'packets'"
187 if not isinstance(kind, int)
or isinstance(kind, bool)
or not (0 <= kind <= 255):
188 raise ValueError(f
"{path}: {name!r} kind={kind!r} is not a plausible wire kind byte (0-255)")
189 nid_message = spec.get(
"nidMessage", 0)
190 if not isinstance(nid_message, int)
or isinstance(nid_message, bool)
or not (0 <= nid_message <= 255):
191 raise ValueError(f
"{path}: {name!r} nidMessage={nid_message!r} is not a plausible wire byte (0-255)")
194 for pkt_name, pkt_spec
in packets.items():
195 if isinstance(pkt_spec, dict)
and "fields" in pkt_spec:
196 all_fields.update(pkt_spec[
"fields"])
200 "nid_message": nid_message,
201 "direction": direction,
203 "fields": all_fields,
204 "wire_format":
"flat",
206 messages[name] = msg_entry
207 if name ==
"TRAIN_CONNECT":
208 messages[
"P0"] = msg_entry
210 messages[
"TRAIN_CONNECT"] = msg_entry
212 ertms_wrapper_kind = raw.get(
"_ertmsWrapperKind")
213 if has_ertms_messages
and ertms_wrapper_kind
is None:
222 ertms_wrapper_kind = 22
223 if ertms_wrapper_kind
is not None:
224 if not isinstance(ertms_wrapper_kind, int)
or isinstance(ertms_wrapper_kind, bool) \
225 or not (0 <= ertms_wrapper_kind <= 255):
226 raise ValueError(f
"{path}: _ertmsWrapperKind={ertms_wrapper_kind!r} is not a plausible wire byte (0-255)")
227 if has_ertms_messages
and ertms_codec
is None:
228 raise ValueError(f
"{path}: has wireFormat=ertms message(s) but no ertms_codec was injected "
229 "(MessageCatalog(catalog_path, ertms_codec=..., ...) - see this module's own doc)")
238 self.
_name_by_kind = {spec[
"kind"]: name
for name, spec
in messages.items()
if spec[
"wire_format"] ==
"flat"}
255 if (spec[
"wire_format"] ==
"ertms")
and name.startswith(
"M")}
316 """@return the raw wire datagram for @message/@train_id/@fields (a
317 dict of field name -> int, may omit fields to leave them 0).
318 Raises UnknownMessageError/WrongDirectionError/UnknownFieldError/
319 InvalidFieldValueError on a bad request - a typo'd name, a
320 message this sim does not originate, or an out-of-catalog value
321 (e.g. msg_type=99, not 24 or 15) must fail loudly here, not be
322 silently sent onto the wire as garbage.
324 For a "wireFormat": "ertms" message, delegates to this catalog's
325 own injected ertms_encoders[message] adapter (byte length is
326 genuinely message-specific, not this method's own concern the
327 way the flat scheme's l_message computation below is) and wraps
328 the result with the shared 2-byte _ertmsWrapperKind+train_id
329 prefix - see this module's own header doc."""
332 if self.
_messages[message][
"direction"] !=
"send":
333 raise WrongDirectionError(f
"{message} is direction=receive in {self._path} - this sim does not send it")
334 if self.
_messages[message][
"wire_format"] ==
"ertms":
337 raise ValueError(f
"no ertms_encoders[{message!r}] was injected for ERTMS send message")
338 codec_bytes = encoder(train_id, fields, fields.get(
"t_train", 0))
342 unknown = set(fields) - set(field_specs)
345 f
"{message} does not have field(s) {sorted(unknown)} - allowed: {sorted(field_specs)}"
347 for field, value
in fields.items():
360 if "t_train_ack" in field_specs:
362 elif "nid_lrbg" in field_specs:
368 extra_content = 4 + 40
369 elif (
"start_signal" in field_specs)
or (
"end_signal" in field_specs):
375 extra_content = 4 + 40 + 16
378 l_message = 12 + 24 + extra_content
382 nid_message=self.
_messages[message][
"nid_message"],