Design: Bridge

The bridge (package cpm_lab_bridge, executable bridge_node) connects the lab domain and the planner domain. It is the only path between them: it forwards the vehicle states, the map, the commands and the triggers, checks that every component is ready before an experiment can start, and stops the experiment when a vehicle state goes missing. Understanding the Domain IDs explains the idea; this page describes the implementation.

Source: src/bridge/cpm_lab_bridge/ — component/bridge_component.cpp (ROS interface), src/bridge.cpp (state machine without ROS, unit-tested in test/).

One Process, Two Domains

The control flow starts the bridge with ROS_DOMAIN_ID of the planner domain. The node /planner/bridge lives in that domain. For the lab domain, the bridge creates a second rclcpp::Context with lab_domain_id (21) and a node /lab/bridge in it, spun by its own executor thread. Each message is copied from one node to the other; the bridge never relays topics generically.

Lab domain (/lab)

Direction

Planner domain (/planner)

vehicle_<id>/vehicleState (best effort)

➝ collected, sent every period as

vehicle_states (VehicleStateList)

map (transient local)

➝

map (transient local)

vehicle_<id>/command (best effort)

⟵ by vehicle_id

command (reliable)

system_trigger from lab* sources

➝

system_trigger

system_trigger

⟵

system_trigger from planner* sources

Triggers with another source_id prefix are rejected with a warning, and the bridge ignores its own triggers (source_id bridge), so nothing loops between the domains. Commands for vehicles that were not deployed are rejected.

Parameters (config/default.yaml, set by the control flow): vehicle_ids (the deployed vehicles), period_ms (the planner period), lab_domain_id (21) and vehicle_timeout_seconds (1.0).

State Machine

        ---
config:
  layout: elk
  startOnLoad: false

---
stateDiagram-v2
   [*] --> STARTED
   STARTED --> READY: planner READY for every vehicle and a state of every vehicle
   READY --> RUNNING: START from the lab
   RUNNING --> STOPPED: STOP from the lab
   RUNNING --> EMERGENCY_STOP: a vehicle state missing or older than the timeout
    

States of the bridge

Every period, a timer on the lab node acts on the state:

  • STARTED: send READY_REQUEST to both domains. A planner answers with READY and source_id planner_<vehicle_id> for each of its vehicles; a vehicle counts once its state arrived.

  • READY: send READY (source_id bridge), which the control flow waits for.

  • RUNNING: publish the last state of every deployed vehicle as one VehicleStateList to the planner. If a vehicle never sent a state, or its last state is older than vehicle_timeout_seconds (measured from its valid_after_stamp), switch to EMERGENCY_STOP.

  • EMERGENCY_STOP: publish STOP (source_id lab) once to both domains. The vehicles stop, the planner stops, and the control flow tears the experiment down.

The state and the number of received vehicle states are published on /diagnostics.

Timing

The planner period is the timer period of the bridge, so the planner is driven by the vehicle states: it plans once per VehicleStateList and does not need its own timer. The states are the latest received, not interpolated; their valid_after_stamp tells the planner how old they are. The lab domain runs at 20 ms, so periods below 20 ms do not make sense; the default is 400 ms.

Changing the Bridge

  • Keep the logic in Bridge (src/bridge.cpp) free of ROS and extend its unit tests.

  • A new topic between the domains needs a subscription on one node and a publisher on the other; document it in Interface Reference and in the table above.

  • The checks before Start are a safety feature. Relaxing them needs agreement of the team.