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] [IL] [RBC West, RBC East] [IL_STATUS] [IL status set] [il_status=%d]",
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] [IL] [RBC West, RBC East] [IL_ROUTE_CMD] [IL route command sent] "
259 "[%s type=%s status=%s]",
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] [IL] [RBC West, RBC East] [IL_STATUS] [IL status sent] [il_status=%d]",
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] [IL] [RBC West, RBC East] [%s] [IL message sent] [nid_engine=%d %s]",
282 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 def handle_cleanup(_request):
298 """Comprehensive reset of this sim's own local state (per direct
299 request: sims shall forget historical data between tests, the
300 connection can stay up). IL owns no per-route memory of its own
301 beyond the received-message log below - which routes currently
302 exist is GP's db, not this sim's - so this is currently
303 equivalent to clearMessages; kept as its own named command for
304 API symmetry with train_sim.py's/ctc_sim.py's own cleanup (which
305 do have more to reset) and so a Robot-side "Cleanup All Sims"
306 keyword can call the same verb name on every sim uniformly."""
307 last_received.clear()
308 return {
"status":
"OK"}
310 control = ControlServer(
313 {
"ADD_ROUTE": handle_add_route,
"RELEASE_ROUTE": handle_release_route,
"PING": handle_ping,
314 "SET_IL_STATUS": handle_set_il_status},
316 "sendMessage": handle_send_message,
317 "getMessage": handle_get_message,
318 "cleanup": handle_cleanup,
319 "clearMessages": handle_clear_messages,
321 bind_host=control_host,
326 def _stop(signum, _frame):
328 log.info(
"[GENERAL] [IL] [internal] [SIGNAL] [IL sim shutting down] [signal=%d]",
332 signal.signal(signal.SIGTERM, _stop)
333 signal.signal(signal.SIGINT, _stop)
335 log.info(
"[GENERAL] [IL] [internal] [INIT] [IL sim starting] [port=%d]",
342 status = StatusReporter(log, link,
"SIM-IL",
"il")
343 status.emit({
"trains": 1})
346 next_route_status_report = time.monotonic()
347 next_il_status_report = time.monotonic()
350 link.ensure_connected()
351 control.poll(timeout=0.2)
352 status.tick({
"trains": 1})
360 now = time.monotonic()
361 if now >= next_il_status_report:
363 next_il_status_report = now + ROUTE_STATUS_PERIOD_S
364 if route_table
and now >= next_route_status_report:
365 for route
in route_table:
366 key = (route[
"start_signal"], route[
"end_signal"])
367 locked = route_lock_state.get(key,
False)
368 link.send_all(CATALOG.build_envelope(
"ROUTE_STATUS_REPORT", train_id, {
369 "start_signal": route[
"start_signal"],
370 "end_signal": route[
"end_signal"],
371 "route_status": 1
if locked
else 0,
373 log.info(
"[IO] [IL] [RBC West, RBC East] [ROUTE_STATUS_REPORT] [IL periodic route-status broadcast] "
374 "[route_count=%d locked_count=%d]",
375 len(route_table), sum(1
for v
in route_lock_state.values()
if v))
376 next_route_status_report = now + ROUTE_STATUS_PERIOD_S
378 for site, data
in link.poll_recv(timeout=0.0):
387 if data
and data[0] == il_wire.KIND_IL_ROUTE_INDICATION:
388 ind = il_wire.decode_route_indication(data)
397 last_received.setdefault(ind[
"allocated_train_nid_engine"], {})[
"IL_ROUTE_INDICATION"] = ind
398 if ind[
"route_id"]
is not None and ind[
"route_id"] >= 0:
399 last_received.setdefault(-(1000 + ind[
"route_id"]), {})[
"IL_ROUTE_INDICATION"] = ind
401 fields_str =
" ".join(f
"{k}={v}" for k, v
in sorted(ind.items()))
402 log.info(
"[IO] [RBC %s] [IL] [IL_ROUTE_INDICATION] [RBC route indication received] "
403 "[%s fsm_state_name=%s]",
404 site.title(), fields_str,
405 il_wire.route_fsm_state_name(ind[
"route_fsm_state"]))
408 decoded = CATALOG.decode_to_message(data)
410 log.warning(
"[IL] [internal] [MALFORMED] [Malformed envelope dropped] [site=%s]", site)
412 nid_engine, message, fields = decoded
413 last_received.setdefault(nid_engine, {})[message] = fields
414 fields_str =
" ".join(f
"{k}={v}" for k, v
in sorted(fields.items()))
415 if message ==
"TrainPositionInRoute":
416 log.info(
"[IO] [RBC %s] [IL] [TrainPositionInRoute] [Train position in route received] [nid_engine=%d %s]",
417 site.title(), nid_engine, fields_str)
431 if "start_signal" in fields
and "end_signal" in fields
and "route_status" in fields:
432 _mark_route(fields[
"start_signal"], fields[
"end_signal"], bool(fields[
"route_status"]))
434 log.info(
"stopped after %d route(s) added", routes_sent)