120 config = sim_config.load()
121 site_tag = sim_config.require(config,
"siteTag")
133 route_length_m = int(config.get(
"routeLengthM", 500))
134 control_host = sim_config.require(config,
"controlServer",
"bindHost")
135 control_port = sim_config.require(config,
"controlServer",
"port")
140 logging.basicConfig(level=logging.INFO, format=
"[%(levelname)s] %(message)s", stream=sys.stdout)
141 log = logging.getLogger(
"il")
144 "WEST": (sim_config.require(config,
"rbcWest",
"host"), sim_config.require(config,
"rbcWest",
"port")),
145 "EAST": (sim_config.require(config,
"rbcEast",
"host"), sim_config.require(config,
"rbcEast",
"port")),
147 link = DualHomedLink(log, targets, ENVELOPE_SIZE)
164 route_table = site_data.routes()
165 except Exception
as exc:
166 log.warning(
"[GENERAL] [%s] [internal] [CONFIG] [could not load route table from RailML - "
167 "Phase D route-status broadcast disabled] [%s]", f
"il-{site_tag.lower()}", exc)
169 route_lock_state = {(r[
"start_signal"], r[
"end_signal"]):
False for r
in route_table}
171 def _mark_route(start_signal, end_signal, locked):
172 key = (int(start_signal), int(end_signal))
173 if key
in route_lock_state:
174 route_lock_state[key] = locked
181 il_status = {
"value": il_wire.IL_STATUS_UP}
182 _IL_STATUS_BY_NAME = {
183 "down": il_wire.IL_STATUS_DOWN,
184 "restarting": il_wire.IL_STATUS_RESTARTING,
185 "up": il_wire.IL_STATUS_UP,
188 def _send_il_status():
189 link.send_all(il_wire.encode_il_status(il_status[
"value"]))
191 def handle_set_il_status(args):
192 name = (args[0]
if args
else "up").strip().lower()
193 if name
not in _IL_STATUS_BY_NAME:
194 return "ERR unknown status (down|restarting|up)\n"
195 il_status[
"value"] = _IL_STATUS_BY_NAME[name]
197 log.info(
"[IO] [%s] [c-west,c-east] [IL_STATUS] [IL status set] [il_status=%d]",
198 f
"il-{site_tag.lower()}", il_status[
"value"])
201 def handle_add_route(args):
203 length_m = int(args[0])
if args
else route_length_m
206 "route #%d added (+%dm, no route identity) for train_id=%d [commanded] - RBC will reject this: "
207 "the plaintext ADD_ROUTE command has no route identity fields; use sendMessage/the Robot "
208 "'Set IL Route' keyword for a real grant",
209 routes_sent, length_m, train_id,
211 link.send_all(CATALOG.build_envelope(
"ROUTE_ADD", train_id, {
"route_len": length_m}))
214 def handle_release_route(args):
215 start_signal, end_signal = int(args[0]), int(args[1])
217 "route release requested (start_signal=%d end_signal=%d) for train_id=%d [commanded]",
218 start_signal, end_signal, train_id,
220 link.send_all(CATALOG.build_envelope(
221 "ROUTE_RELEASE", train_id, {
"start_signal": start_signal,
"end_signal": end_signal}
223 _mark_route(start_signal, end_signal,
False)
226 def handle_ping(_args):
229 def handle_send_message(request):
231 nid_engine = request[
"nidEngine"]
232 message = request[
"message"]
233 fields = dict(request.get(
"fields", {}))
237 if message ==
"IL_ROUTE_CMD":
242 "route_type": int(fields.get(
"route_type", il_wire.ROUTE_TYPE_NO_ROUTE)),
243 "route_status": int(fields.get(
"route_status", il_wire.ROUTE_STATUS_NO_STATUS)),
244 "degraded_status": int(fields.get(
"degraded_status", il_wire.DEGRADED_STATUS_NO_STATUS)),
245 "release_route_request": int(fields.get(
"release_route_request", 0)),
246 "first_route_in_path": int(fields.get(
"first_route_in_path", 0)),
247 "route_id": int(fields.get(
"route_id", -1)),
248 "start_signal": int(fields.get(
"start_signal", -1)),
249 "end_signal": int(fields.get(
"end_signal", -1)),
251 frame = il_wire.encode_route_cmd(**sent)
253 if sent[
"start_signal"] != -1
and sent[
"end_signal"] != -1:
254 locked = (sent[
"route_type"] == il_wire.ROUTE_TYPE_LOCKED
255 and not sent[
"release_route_request"])
256 _mark_route(sent[
"start_signal"], sent[
"end_signal"], locked)
257 fields_str =
" ".join(f
"{k}={v}" for k, v
in sorted(sent.items()))
258 log.info(
"[IO] [%s] [c-west,c-east] [IL_ROUTE_CMD] [IL route command sent] "
259 "[%s type=%s status=%s]",
260 f
"il-{site_tag.lower()}", fields_str,
261 _CMD_ROUTE_TYPE_NAME.get(sent[
"route_type"], sent[
"route_type"]),
262 _CMD_ROUTE_STATUS_NAME.get(sent[
"route_status"], sent[
"route_status"]))
263 return {
"status":
"OK"}
264 if message ==
"IL_STATUS":
265 il_status[
"value"] = int(fields.get(
"il_status", il_wire.IL_STATUS_UP))
267 log.info(
"[IO] [%s] [c-west,c-east] [IL_STATUS] [IL status sent] [il_status=%d]",
268 f
"il-{site_tag.lower()}", il_status[
"value"])
269 return {
"status":
"OK"}
271 if message ==
"ROUTE_ADD" and "route_len" not in fields:
272 fields[
"route_len"] = route_length_m
273 link.send_all(CATALOG.build_envelope(message, nid_engine, fields))
274 if message ==
"ROUTE_ADD":
276 if "start_signal" in fields
and "end_signal" in fields:
277 _mark_route(fields[
"start_signal"], fields[
"end_signal"],
True)
278 elif message ==
"ROUTE_RELEASE" and "start_signal" in fields
and "end_signal" in fields:
279 _mark_route(fields[
"start_signal"], fields[
"end_signal"],
False)
280 fields_str =
" ".join(f
"{k}={v}" for k, v
in sorted(fields.items()))
281 log.info(
"[IO] [%s] [c-west,c-east] [%s] [IL message sent] [nid_engine=%d %s]",
282 f
"il-{site_tag.lower()}", message, nid_engine, fields_str)
283 return {
"status":
"OK"}
285 def handle_get_message(request):
286 nid_engine = request[
"nidEngine"]
287 message = request[
"message"]
288 packets = last_received.get(nid_engine, {}).get(message)
290 return {
"status":
"ERR",
"reason": f
"no {message} received yet for nidEngine {nid_engine}"}
291 return {
"status":
"OK",
"message": message,
"packets": {message: packets}}
293 def handle_clear_messages(_request):
294 last_received.clear()
295 return {
"status":
"OK"}
297 control = ControlServer(
300 {
"ADD_ROUTE": handle_add_route,
"RELEASE_ROUTE": handle_release_route,
"PING": handle_ping,
301 "SET_IL_STATUS": handle_set_il_status},
303 "sendMessage": handle_send_message,
304 "getMessage": handle_get_message,
305 "clearMessages": handle_clear_messages,
307 bind_host=control_host,
312 def _stop(signum, _frame):
314 log.info(
"[GENERAL] [%s] [internal] [SIGNAL] [IL sim shutting down] [signal=%d]",
315 f
"il-{site_tag.lower()}", signum)
318 signal.signal(signal.SIGTERM, _stop)
319 signal.signal(signal.SIGINT, _stop)
321 log.info(
"[GENERAL] [%s] [internal] [INIT] [IL sim starting] [port=%d]",
322 f
"il-{site_tag.lower()}", control_port)
328 status = StatusReporter(log, link, f
"SIM-IL/{site_tag}", f
"il-{site_tag.lower()}")
329 status.emit({
"trains": 1})
332 next_route_status_report = 0.0
333 next_il_status_report = 0.0
336 link.ensure_connected()
337 control.poll(timeout=0.2)
338 status.tick({
"trains": 1})
346 now = time.monotonic()
347 if now >= next_il_status_report:
349 next_il_status_report = now + ROUTE_STATUS_PERIOD_S
350 if route_table
and now >= next_route_status_report:
351 for route
in route_table:
352 key = (route[
"start_signal"], route[
"end_signal"])
353 locked = route_lock_state.get(key,
False)
354 link.send_all(CATALOG.build_envelope(
"ROUTE_STATUS_REPORT", train_id, {
355 "start_signal": route[
"start_signal"],
356 "end_signal": route[
"end_signal"],
357 "route_status": 1
if locked
else 0,
359 log.info(
"[IO] [%s] [c-west,c-east] [ROUTE_STATUS_REPORT] [IL periodic route-status broadcast] "
360 "[route_count=%d locked_count=%d]",
361 f
"il-{site_tag.lower()}", len(route_table), sum(1
for v
in route_lock_state.values()
if v))
362 next_route_status_report = now + ROUTE_STATUS_PERIOD_S
364 for site, data
in link.poll_recv(timeout=0.0):
373 if data
and data[0] == il_wire.KIND_IL_ROUTE_INDICATION:
374 ind = il_wire.decode_route_indication(data)
383 last_received.setdefault(ind[
"allocated_train_nid_engine"], {})[
"IL_ROUTE_INDICATION"] = ind
384 if ind[
"route_id"]
is not None and ind[
"route_id"] >= 0:
385 last_received.setdefault(-(1000 + ind[
"route_id"]), {})[
"IL_ROUTE_INDICATION"] = ind
387 fields_str =
" ".join(f
"{k}={v}" for k, v
in sorted(ind.items()))
388 log.info(
"[IO] [c-%s] [%s] [IL_ROUTE_INDICATION] [RBC route indication received] "
389 "[%s fsm_state_name=%s]",
390 site.lower(), f
"il-{site_tag.lower()}", fields_str,
391 il_wire.route_fsm_state_name(ind[
"route_fsm_state"]))
394 decoded = CATALOG.decode_to_message(data)
396 log.warning(
"[%s] [internal] [MALFORMED] [Malformed envelope dropped] [site=%s]",
397 f
"il-{site_tag.lower()}", site)
399 nid_engine, message, fields = decoded
400 last_received.setdefault(nid_engine, {})[message] = fields
401 fields_str =
" ".join(f
"{k}={v}" for k, v
in sorted(fields.items()))
402 if message ==
"TrainPositionInRoute":
403 log.info(
"[IO] [c-%s] [%s] [TrainPositionInRoute] [Train position in route received] [nid_engine=%d %s]",
404 site.lower(), f
"il-{site_tag.lower()}", nid_engine, fields_str)
418 if "start_signal" in fields
and "end_signal" in fields
and "route_status" in fields:
419 _mark_route(fields[
"start_signal"], fields[
"end_signal"], bool(fields[
"route_status"]))
421 log.info(
"stopped after %d route(s) added", routes_sent)