69 config = sim_config.load()
70 control_host = sim_config.require(config,
"controlServer",
"bindHost")
71 control_port = sim_config.require(config,
"controlServer",
"port")
76 logging.basicConfig(level=logging.INFO, format=
"[%(levelname)s] %(message)s", stream=sys.stdout)
77 log = logging.getLogger(
"ctc")
80 "WEST": (sim_config.require(config,
"rbcWest",
"host"), sim_config.require(config,
"rbcWest",
"port")),
81 "EAST": (sim_config.require(config,
"rbcEast",
"host"), sim_config.require(config,
"rbcEast",
"port")),
83 link = DualHomedLink(log, targets, ENVELOPE_SIZE)
90 site_health = {
"WEST": {
"last_seen":
None,
"msg_count": 0},
"EAST": {
"last_seen":
None,
"msg_count": 0}}
93 start_time = time.monotonic()
95 def handle_ping(_args):
98 def handle_get_message(request):
99 nid_engine = request[
"nidEngine"]
100 message = request[
"message"]
101 packets = last_received.get(nid_engine, {}).get(message)
103 return {
"status":
"ERR",
"reason": f
"no {message} received yet for nidEngine {nid_engine}"}
104 return {
"status":
"OK",
"message": message,
"packets": {message: packets}}
106 def handle_get_status(_request):
107 connected_sites = link.connected_sites()
109 for s
in (
"WEST",
"EAST"):
110 tgt = targets.get(s, (
"127.0.0.1", 0))
112 "connected": s
in connected_sites,
115 "last_seen": site_health.get(s, {}).get(
"last_seen"),
116 "msg_count": site_health.get(s, {}).get(
"msg_count", 0),
121 "trains": train_status,
122 "alarms": alarms[-20:],
124 "indications_seen": seen,
125 "connected_sites": connected_sites,
126 "uptime_s": round(time.monotonic() - start_time, 1),
130 def handle_clear_messages(_request):
131 last_received.clear()
133 return {
"status":
"OK"}
135 def handle_cleanup(_request):
136 """Comprehensive reset of this sim's own local state (per direct
137 request: sims shall forget historical data between tests, the
138 connection can stay up). Same as clearMessages plus alarms -
139 clearMessages deliberately left alarms alone (a running audit
140 trail useful mid-test), but a between-tests Cleanup should not
141 leave a prior test's alarms visible to the next one."""
142 last_received.clear()
145 return {
"status":
"OK"}
147 control = ControlServer(
150 {
"PING": handle_ping},
152 "getMessage": handle_get_message,
153 "getStatus": handle_get_status,
154 "getRbcStatus": handle_get_status,
155 "clearMessages": handle_clear_messages,
156 "cleanup": handle_cleanup,
158 bind_host=control_host,
163 def _stop(signum, _frame):
165 log.info(
"[GENERAL] [CTC] [internal] [SIGNAL] [CTC sim shutting down] [signal=%d]", signum)
168 signal.signal(signal.SIGTERM, _stop)
169 signal.signal(signal.SIGINT, _stop)
171 log.info(
"[GENERAL] [CTC] [internal] [INIT] [CTC sim starting - overseeing WEST, EAST] []")
175 status = StatusReporter(log, link,
"SIM-CTC",
"ctc")
179 link.ensure_connected()
180 control.poll(timeout=0.0)
182 for site, data
in link.poll_recv(timeout=0.2):
185 decoded = CATALOG.decode_to_message(data)
187 log.warning(
"[CTC] [internal] [MALFORMED] [Malformed indication dropped] [site=%s]", site)
189 nid_engine, message, fields = decoded
190 last_received.setdefault(nid_engine, {})[message] = fields
193 site_health.setdefault(site, {})[
"last_seen"] = now_ts
194 site_health[site][
"msg_count"] = site_health[site].get(
"msg_count", 0) + 1
196 fields_str =
" ".join(f
"{k}={v}" for k, v
in sorted(fields.items()))
197 rbc_label = f
"RBC {site.title()}"
198 if message ==
"CTC_CONNECTED":
199 train_status[nid_engine] = {
201 "d_lrbg": fields.get(
"d_lrbg", 0),
205 log.info(
"[IO] [%s] [CTC] [CTC_CONNECTED] [CTC Connected indication received] [nid_engine=%d %s]",
206 rbc_label, nid_engine, fields_str)
207 elif message ==
"CTC_MA_GRANTED":
208 t_entry = train_status.setdefault(nid_engine, {
"connected":
True})
209 t_entry[
"ma_length"] = fields.get(
"ma_length")
210 t_entry[
"ma_seq"] = fields.get(
"ma_seq")
211 t_entry[
"site"] = site
212 t_entry[
"last_seen"] = now_ts
213 log.info(
"[IO] [%s] [CTC] [CTC_MA_GRANTED] [CTC Movement Authority Granted received] [nid_engine=%d %s]",
214 rbc_label, nid_engine, fields_str)
215 elif message ==
"CTC_MA_EXTENDED":
216 t_entry = train_status.setdefault(nid_engine, {
"connected":
True})
217 t_entry[
"ma_length"] = fields.get(
"ma_length")
218 t_entry[
"ma_seq"] = fields.get(
"ma_seq")
219 t_entry[
"site"] = site
220 t_entry[
"last_seen"] = now_ts
221 log.info(
"[IO] [%s] [CTC] [CTC_MA_EXTENDED] [CTC Movement Authority Extended received] [nid_engine=%d %s]",
222 rbc_label, nid_engine, fields_str)
223 elif message ==
"CTC_TRAIN_INFO":
224 t_entry = train_status.setdefault(nid_engine, {
"connected":
True})
225 t_entry[
"d_lrbg"] = fields.get(
"d_lrbg")
226 t_entry[
"ma_status"] =
"GRANTED" if fields.get(
"msg_type") == 1
else "NONE"
227 t_entry[
"ma_target_signal"] = fields.get(
"route_len")
228 t_entry[
"front_signal"] = fields.get(
"t_train")
229 t_entry[
"dist_to_front_signal"] = fields.get(
"cycle")
230 t_entry[
"ma_seq"] = fields.get(
"ma_seq")
232 t_entry[
"ma_offset"] = ma_offset
233 t_entry[
"ma_length_from_balise"] = -ma_offset
234 t_entry[
"nid_lrbg"] = fields.get(
"nid_lrbg")
235 t_entry[
"v_train"] = fields.get(
"v_train")
236 t_entry[
"m_mode"] = fields.get(
"m_mode")
237 t_entry[
"site"] = site
238 t_entry[
"last_seen"] = now_ts
239 log.info(
"[IO] [%s] [CTC] [CTC_TRAIN_INFO] [CTC Train Info received] [nid_engine=%d %s]",
240 rbc_label, nid_engine, fields_str)
241 elif message ==
"CTC_ALARM":
242 severity = fields.get(
"msg_type")
243 level = _ALARM_SEVERITY_LOG.get(severity, logging.WARNING)
244 severity_name = _ALARM_SEVERITY_NAME.get(severity, f
"UNKNOWN({severity})")
247 "severity": severity_name,
248 "severity_code": severity,
249 "ma_seq": fields.get(
"ma_seq"),
250 "cycle": fields.get(
"cycle"),
251 "t_train": fields.get(
"t_train"),
260 log.log(level,
"[IO] [%s] [CTC] [CTC_ALARM] [%s alarm from RBC] [%s]",
261 rbc_label, severity_name, fields_str)
263 log.info(
"[IO] [%s] [CTC] [%s] [CTC indication received] [nid_engine=%d %s]",
264 rbc_label, message, nid_engine, fields_str)
266 log.info(
"[GENERAL] [CTC] [internal] [INIT] [CTC sim stopped] [indications_seen=%d]", seen)