TrainRBCSim
ADR-029 Train (EVC) simulator — dual-homed TCP client speaking the RBC wire protocol
Loading...
Searching...
No Matches
train.train_sim Namespace Reference

Functions

 _pad (frame)
 _default_lrbg (tid)
 main ()

Variables

 CATALOG
int TRAIN_WIRE_SIZE = 512
int MAX_TRAINS = 100

Detailed Description

Train sim (ADR-029) - a real, dual-homed UDP client. Connects to BOTH
sites' C (c-west and c-east) at once and keeps both sockets open for the
whole run, so a site failover is invisible from this process's own point
of view (see simcore.dual_link's own header) - it sends its own
P0/136/146 to both sites unconditionally and simply acts on whichever
site actually answers (the STANDBY site's own C has nothing to forward
back, since its A/B pair isn't computing decisions - see monitor_c.c).

Message names/keys are this project's own (message_catalog.json, right
next to this script) - real Subset-026 message numbers where a real
counterpart exists ("3" = real message 3 Movement Authority, "146" =
real message 146 Acknowledgement - see that file's own
"_subset026Reference" for the real spec text), and clearly-labeled
project-specific names where none does ("P0" handshake -
neither corresponds to any real Subset-026 message). These are still
encoded with today's simplified flat fields, not the real nested
Subset-026 packet structure - see message_catalog.json's own per-message
comments and root CLAUDE.md's Subset-026 note for what that gap actually
is. Msg 136 (Train Position Report) is the one exception, switched this
session on direct request: this sim's autonomous cyclic reporting AND
the REPORT/sendTrainMessage commands all send the REAL Subset-026 Msg
136/Packet 0 now (rbc_ertms_wire.py's own "M136" encoder, "ertms"
wireFormat via message_catalog.json's implicit "M"-prefixed-name
default), not a flat kind - see send_position_report()'s own doc.

Configuration: one JSON file (common/sim_config.py, SIM_CONFIG_PATH env
var, default /app/config.json) supplies everything instance-specific -
trainIndex (0 or 1, selects which of this project's
SAFEAPI_EXAMPLE_MAX_TRAINS train slots this instance is - train_id is
trainIndex + 1), siteTag (display-only, kept for log-prefix consistency
with every other role - not a site selector, this sim talks to both
sites regardless), rbcWest/rbcEast (host+port to reach each site's C),
controlServer (bindHost+port for the command channel below), and
simPeriodSeconds. See sims/train/config/*.json for the actual files and
docker-compose.yml's own `volumes:` entries for how each container gets
its own. Message vocabulary (which messages/fields this sim may send or
receive) comes from message_catalog.json, right next to this script -
safeAPITestEnv/doc/design/SIM_INTERFACES.md documents the same table in
prose.

Train lifecycle is test-managed, not process-managed: the TCP transport
itself connects (and reconnects-forever, dual_link.py's own convention)
the moment this process starts, same as always, but NEITHER the P0
handshake NOR the autonomous cyclic "136" position reporting fires until
a test explicitly connects this train (CONNECT / the connectTrain JSON
command below) - a test that never connects a given train leaves it
genuinely absent from the RBC (ab_gp_train.c's own RBC_MSG_P0/
RBC_MSG_M136 handling never creates that train's session at all), not
just quiet on this sim's own side. Once connected, position reporting
keeps running autonomously (a real train reports position continuously,
not on command) until disconnectTrain (below) - a test harness
(safeAPITestEnv/robot/) can also drive specific position reports on
demand via the command channel (common/control_server.py) regardless of
connection state, e.g. to deterministically hit the D_LRBG single-
channel-fault value without waiting on the random walk.

Commands (controlServer.port, config file - see common/control_server.py):
  REPORT <d_lrbg>   Sends one message "136" (position report) immediately
                    with the given position (does not wait for the next
                    autonomous tick; also resets the autonomous random
                    walk to continue from this value). Works regardless
                    of connection state (an explicit test-driven send,
                    not the autonomous loop) - a test using this without
                    ever connecting the train is exercising the RBC's own
                    documented lenient "M136 arriving before/without a
                    P0" tolerance. Replies "OK\\n".
  PING              Replies "PONG\\n".
  CONNECT           Explicitly connects this train to the RBC: sends P0
                    now (not waiting for the next reconnect edge) and
                    starts autonomous "136" reporting. Replies "OK\\n".
                    See the structured connectTrain/disconnectTrain
                    commands below for the nidEngine-explicit,
                    multi-train-aware equivalents this file's own test
                    suite actually uses.
  DISCONNECT        Closes BOTH site connections right now (simulating
                    the train losing radio contact with the RBC
                    entirely) - reconnects automatically afterward, same
                    as any other transport-level disconnect. Deliberately
                    does NOT touch connected_to_rbc or send
                    RBC_MSG_TRAIN_DISCONNECT - this simulates losing
                    radio contact, not a formal disconnect; the RBC-side
                    session is untouched and autonomous reporting resumes
                    the moment the socket reconnects. Replies "OK\\n".
  DISCONNECTWITHONLINE
                    Closes the connection to whichever site this sim
                    currently believes is ONLINE (see "online-site
                    tracking" below) - simulating losing contact with
                    the active RBC specifically. Replies "OK\\n", or
                    "ERR <reason>\\n" if no site has been heard from yet
                    (nothing to call "online").
  DISCONNECTWITHSTANDBY
                    Same as DISCONNECTWITHONLINE but for the OTHER site
                    (whichever this sim does NOT currently believe is
                    ONLINE) - simulating losing contact with the standby
                    RBC specifically, while the active one stays
                    reachable. Same ERR case as DISCONNECTWITHONLINE.

Online-site tracking: this sim has no explicit ONLINE/STANDBY signal
from the wire (the RBC side doesn't tell a train which of A/B/site is
currently active) - "online" here is inferred as whichever site most
recently sent this sim ANY decodable message (updated in the main
receive loop, below), the same de-facto signal the sim already uses
operationally (only the ONLINE site's own C ever answers - this file's
own header). Purely a heuristic for the two DISCONNECTWITH* commands
above; not itself sent anywhere or asserted on by anything else.

Structured JSON commands (safeAPITestEnv/doc/design/DESIGN.md,
SIM_INTERFACES.md - same port, see common/control_server.py's own doc
for how the two command styles share one line-based channel):
  sendMessage   {"cmd":"sendMessage","nidEngine":N,"message":"136","fields":{...}}
                Sends one message NOW (any message this train can
                originate - P0/136/146, message_catalog.json) with the
                given fields; unset fields default to 0. Does NOT touch
                the autonomous cycle counter/random-walk state the way
                REPORT does - a purely explicit, one-shot send.
  getMessage    {"cmd":"getMessage","nidEngine":N,"message":"3"}
                Returns the most recently received message of that kind
                for that nidEngine ("PositionReportAck" or "3" - the
                only kinds this sim ever receives), or an ERR if none
                has arrived yet.
  connectTrain  {"cmd":"connectTrain","nidEngine":N,"nidLrbg":M,"dLrbg":D}
                Explicitly connects train N to the RBC: sends P0 now and
                starts this train's autonomous "136" reporting (see this
                file's own "Train lifecycle" paragraph above) - the
                nidEngine-explicit structured equivalent of the plain
                CONNECT command, and what safeAPITestEnv/robot/'s own
                "Connect Train" keyword actually calls. "nidLrbg"/"dLrbg"
                are both optional (either, neither, or both) - override
                this train's starting balise/position instead of
                _default_lrbg()'s own west/east heuristic, applied
                before the first real "136" - see handle_connect_train()'s
                own doc for why both state["position_m"] and
                train_vars["d_lrbg"] get set. Replies {"status":"OK"}.
  disconnectTrain
                {"cmd":"disconnectTrain","nidEngine":N}
                Sends RBC_MSG_TRAIN_DISCONNECT (rbc_wire_types.h) for
                train N and stops its autonomous reporting - the RBC-side
                session is actually removed (ab_gp_train.c releases every
                route the train held, then removes it), not just starved
                of traffic. Does NOT close the TCP transport (see
                DISCONNECT above for that, a different, radio-silence
                concept) - "Disconnect Train" in the Robot library.
                Replies {"status":"OK"}.
  setTrainVariable
                {"cmd":"setTrainVariable","nidEngine":N,"variable":"d_lrbg","value":123}
                Stores one SS026-style field for this train (nid_lrbg/
                d_lrbg/v_train/m_mode/m_level), persisting until
                overwritten - sendTrainMessage (below) pulls whatever a
                given message's own catalog entry declares out of this
                store at send time. Replies {"status":"OK"}.
  sendTrainMessage
                {"cmd":"sendTrainMessage","nidEngine":N,"message":"136"}
                Sends message N NOW, built from whatever setTrainVariable
                has stored for this train (message_catalog.json's own
                field list for that message) - an explicit, one-shot send
                like sendMessage above, but from the persistent variable
                store rather than an inline fields dict. Works regardless
                of connection state, same as REPORT/sendMessage. Replies
                {"status":"OK"} or {"status":"ERR","reason":"..."}.

Function Documentation

◆ _default_lrbg()

_default_lrbg ( tid)
protected
A train's own starting LRBG before any real position report has
set one - purely a plausible default so an early sendTrainMessage/136
isn't sending nid_lrbg=0. Same "train 1 near the west end, everything
else near the east end" heuristic this file always used, just now a
per-train function instead of a single value precomputed from a
static roster's first entry (there is no roster to read "first
entry" from any more).

Definition at line 221 of file train_sim.py.

◆ _pad()

_pad ( frame)
protected
Zero-pads @frame up to TRAIN_WIRE_SIZE - both wire families
already put their own real length in their own header (l_message),
so the receiving side reads exactly the meaningful part regardless
of this trailing padding; this padding exists purely to satisfy the
transport's own exact-datagram-size expectation on this channel.

Definition at line 200 of file train_sim.py.

◆ main()

main ( )

Definition at line 232 of file train_sim.py.

Variable Documentation

◆ CATALOG

train.train_sim.CATALOG
Initial value:
= MessageCatalog(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "message_catalog.json"),
ertms_codec=ertms_codec,
ertms_encoders=ertms_adapters.ENCODERS,
ertms_flatteners=ertms_adapters.FLATTENERS,
)

Definition at line 181 of file train_sim.py.

◆ MAX_TRAINS

int train.train_sim.MAX_TRAINS = 100

Definition at line 218 of file train_sim.py.

◆ TRAIN_WIRE_SIZE

int train.train_sim.TRAIN_WIRE_SIZE = 512

Definition at line 197 of file train_sim.py.