59 config = sim_config.load()
60 control_host = sim_config.require(config,
"controlServer",
"bindHost")
61 control_port = sim_config.require(config,
"controlServer",
"port")
66 logging.basicConfig(level=logging.INFO, format=
"[%(levelname)s] %(message)s", stream=sys.stdout)
67 log = logging.getLogger(
"ctc")
70 "WEST": (sim_config.require(config,
"rbcWest",
"host"), sim_config.require(config,
"rbcWest",
"port")),
71 "EAST": (sim_config.require(config,
"rbcEast",
"host"), sim_config.require(config,
"rbcEast",
"port")),
73 link = DualHomedLink(log, targets, ENVELOPE_SIZE)
80 site_health = {
"WEST": {
"last_seen":
None,
"msg_count": 0},
"EAST": {
"last_seen":
None,
"msg_count": 0}}
83 start_time = time.monotonic()
85 def handle_ping(_args):
88 def handle_get_message(request):
89 nid_engine = request[
"nidEngine"]
90 message = request[
"message"]
91 packets = last_received.get(nid_engine, {}).get(message)
93 return {
"status":
"ERR",
"reason": f
"no {message} received yet for nidEngine {nid_engine}"}
94 return {
"status":
"OK",
"message": message,
"packets": {message: packets}}
96 def handle_get_status(_request):
97 connected_sites = link.connected_sites()
99 for s
in (
"WEST",
"EAST"):
100 tgt = targets.get(s, (
"127.0.0.1", 0))
102 "connected": s
in connected_sites,
105 "last_seen": site_health.get(s, {}).get(
"last_seen"),
106 "msg_count": site_health.get(s, {}).get(
"msg_count", 0),
111 "trains": train_status,
112 "alarms": alarms[-20:],
114 "indications_seen": seen,
115 "connected_sites": connected_sites,
116 "uptime_s": round(time.monotonic() - start_time, 1),
120 def handle_clear_messages(_request):
121 last_received.clear()
123 return {
"status":
"OK"}
125 control = ControlServer(
128 {
"PING": handle_ping},
130 "getMessage": handle_get_message,
131 "getStatus": handle_get_status,
132 "getRbcStatus": handle_get_status,
133 "clearMessages": handle_clear_messages,
135 bind_host=control_host,
140 def _stop(signum, _frame):
142 log.info(
"[GENERAL] [ctc] [internal] [SIGNAL] [CTC sim shutting down] [signal=%d]", signum)
145 signal.signal(signal.SIGTERM, _stop)
146 signal.signal(signal.SIGINT, _stop)
148 log.info(
"[GENERAL] [ctc] [internal] [INIT] [CTC sim starting - overseeing WEST, EAST] []")
152 status = StatusReporter(log, link,
"SIM-CTC",
"ctc")
156 link.ensure_connected()
157 control.poll(timeout=0.0)
159 for site, data
in link.poll_recv(timeout=0.2):
162 decoded = CATALOG.decode_to_message(data)
164 log.warning(
"[%s] [internal] [MALFORMED] [Malformed indication dropped] [site=%s]",
"ctc", site)
166 nid_engine, message, fields = decoded
167 last_received.setdefault(nid_engine, {})[message] = fields
170 site_health.setdefault(site, {})[
"last_seen"] = now_ts
171 site_health[site][
"msg_count"] = site_health[site].get(
"msg_count", 0) + 1
173 fields_str =
" ".join(f
"{k}={v}" for k, v
in sorted(fields.items()))
174 if message ==
"CTC_CONNECTED":
175 train_status[nid_engine] = {
177 "d_lrbg": fields.get(
"d_lrbg", 0),
181 log.info(
"[IO] [c-%s] [ctc] [CTC_CONNECTED] [CTC Connected indication received] [nid_engine=%d %s]",
182 site.lower(), nid_engine, fields_str)
183 elif message ==
"CTC_MA_GRANTED":
184 t_entry = train_status.setdefault(nid_engine, {
"connected":
True})
185 t_entry[
"ma_length"] = fields.get(
"ma_length")
186 t_entry[
"ma_seq"] = fields.get(
"ma_seq")
187 t_entry[
"site"] = site
188 t_entry[
"last_seen"] = now_ts
189 log.info(
"[IO] [c-%s] [ctc] [CTC_MA_GRANTED] [CTC Movement Authority Granted received] [nid_engine=%d %s]",
190 site.lower(), nid_engine, fields_str)
191 elif message ==
"CTC_MA_EXTENDED":
192 t_entry = train_status.setdefault(nid_engine, {
"connected":
True})
193 t_entry[
"ma_length"] = fields.get(
"ma_length")
194 t_entry[
"ma_seq"] = fields.get(
"ma_seq")
195 t_entry[
"site"] = site
196 t_entry[
"last_seen"] = now_ts
197 log.info(
"[IO] [c-%s] [ctc] [CTC_MA_EXTENDED] [CTC Movement Authority Extended received] [nid_engine=%d %s]",
198 site.lower(), nid_engine, fields_str)
199 elif message ==
"CTC_TRAIN_INFO":
200 t_entry = train_status.setdefault(nid_engine, {
"connected":
True})
201 t_entry[
"d_lrbg"] = fields.get(
"d_lrbg")
202 t_entry[
"ma_status"] =
"GRANTED" if fields.get(
"msg_type") == 1
else "NONE"
203 t_entry[
"ma_target_signal"] = fields.get(
"route_len")
204 t_entry[
"front_signal"] = fields.get(
"t_train")
205 t_entry[
"dist_to_front_signal"] = fields.get(
"cycle")
206 t_entry[
"ma_seq"] = fields.get(
"ma_seq")
211 raw_offset = fields.get(
"ma_length", 0)
212 ma_offset = raw_offset - 0x100000000
if raw_offset >= 0x80000000
else raw_offset
213 t_entry[
"ma_offset"] = ma_offset
214 t_entry[
"ma_length_from_balise"] = -ma_offset
215 t_entry[
"nid_lrbg"] = fields.get(
"nid_lrbg")
216 t_entry[
"v_train"] = fields.get(
"v_train")
217 t_entry[
"m_mode"] = fields.get(
"m_mode")
218 t_entry[
"site"] = site
219 t_entry[
"last_seen"] = now_ts
220 log.info(
"[IO] [c-%s] [ctc] [CTC_TRAIN_INFO] [CTC Train Info received] [nid_engine=%d %s]",
221 site.lower(), nid_engine, fields_str)
222 elif message ==
"CTC_ALARM":
223 severity = fields.get(
"msg_type")
224 level = _ALARM_SEVERITY_LOG.get(severity, logging.WARNING)
225 severity_name = _ALARM_SEVERITY_NAME.get(severity, f
"UNKNOWN({severity})")
228 "severity": severity_name,
229 "severity_code": severity,
230 "ma_seq": fields.get(
"ma_seq"),
231 "cycle": fields.get(
"cycle"),
232 "t_train": fields.get(
"t_train"),
241 log.log(level,
"[IO] [c-%s] [ctc] [CTC_ALARM] [%s alarm from RBC] [%s]",
242 site.lower(), severity_name, fields_str)
244 log.info(
"[IO] [c-%s] [ctc] [%s] [CTC indication received] [nid_engine=%d %s]",
245 site.lower(), message, nid_engine, fields_str)
247 log.info(
"[GENERAL] [ctc] [internal] [INIT] [CTC sim stopped] [indications_seen=%d]", seen)