Documentation
OpenRoadCode Message Bus IDD
Source:
docs/messaging/message_bus_idd.md
Purpose
This document defines the application-facing messaging interface used by OpenRoadCode producers and consumers. The bus is intentionally transport-light: domain contracts own schema and validation, while ZeroMQ only carries topic-prefixed JSON messages.
For copy/paste producer and subscriber examples, see messaging/README.md.
Topology
flowchart LR
publishers["Publishers"] -->|"connect"| ingress["tcp://127.0.0.1:5556<br/>XSUB"]
ingress --> proxy["zmq.proxy<br/>broker"] --> egress["XPUB<br/>tcp://127.0.0.1:5557"]
egress -->|"connect"| subscribers["Subscribers"]
classDef orcApp fill:#dbeafe,stroke:#2563eb,color:#172554;
classDef orcService fill:#ede9fe,stroke:#7c3aed,color:#2e1065;
classDef orcController fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef orcMessage fill:#ffedd5,stroke:#ea580c,color:#7c2d12;
classDef orcAdapter fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
classDef orcExternal fill:#f3f4f6,stroke:#6b7280,color:#1f2937;
class publishers,subscribers orcApp;
class ingress,proxy,egress orcMessage;
The broker is the only process that binds the TCP ports. Publishers and subscribers connect to it.
Default endpoints are defined in messaging/zeromq/endpoints.py:
- broker publisher ingress bind:
tcp://0.0.0.0:5556 - broker subscriber egress bind:
tcp://0.0.0.0:5557 - local publisher connection:
tcp://127.0.0.1:5556 - local subscriber connection:
tcp://127.0.0.1:5557
Message framing
Each bus message is a two-part ZeroMQ multipart message:
- UTF-8 topic string
- JSON object payload
Example conceptual frame:
openroad.vehicle.state
{"version":1,"timestamp":...,"source":"obd2","data":{...}}
Payloads must be JSON objects. Every public contract owns its encoder, decoder, validator, typed decoded representation, schema version, and topic constant.
Contract rules
Public telemetry contracts use SI units unless a contract explicitly documents otherwise. Presentation layers may convert SI values for display, but producers must not publish UI-oriented units such as mph, PSI, Fahrenheit, or percent where the domain representation is m/s, Pa, K, or a 0.0..1.0 ratio.
Encoders validate their own output before publication. Decoders validate before constructing typed messages. Unknown or missing fields are rejected for strict versioned contracts.
Current topics
openroad.navigation.position
Owns absolute position/fix information: latitude and longitude in radians, altitude and accuracy in meters, fix/satellite metadata, source, and cache state. Speed and course are excluded; see the Position IDD.
openroad.navigation.motion
Owns motion information independent of the absolute position contract: heading and course in radians, ground and vertical speed in m/s, turn rate in rad/s, and cache state.
openroad.navigation.attitude
Owns fused vehicle orientation: heading, pitch, and roll in radians. These are angular coordinates, not an XYZ vector.
openroad.navigation.imu
Owns inertial motion vectors: acceleration and linear acceleration in m/s², plus angular velocity in rad/s. Each quantity is represented as an XYZ vector.
openroad.vehicle.state
Owns current automotive telemetry. Version 1 includes:
engine_speed_rad_svehicle_speed_m_sthrottle_positionaccelerator_pedal_positionengine_loadintake_manifold_pressure_pabarometric_pressure_paboost_pressure_pamass_air_flow_kg_scoolant_temperature_kintake_air_temperature_kfuel_levelcontrol_voltage_v
Consumer architecture
Applications should normally create one MessageDispatcher for one subscriber connection and register multiple topics with it.
flowchart TD
subscriber["ZeroMqSubscriber"] --> dispatcher["MessageDispatcher"]
dispatcher --> position["position"]
dispatcher --> motion["motion"]
dispatcher --> attitude["attitude"]
dispatcher --> imu["imu"]
dispatcher --> vehicle["vehicle"]
position --> handlers["Application state handlers"]
motion --> handlers
attitude --> handlers
imu --> handlers
vehicle --> handlers
classDef orcApp fill:#dbeafe,stroke:#2563eb,color:#172554;
classDef orcService fill:#ede9fe,stroke:#7c3aed,color:#2e1065;
classDef orcController fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef orcMessage fill:#ffedd5,stroke:#ea580c,color:#7c2d12;
classDef orcAdapter fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
classDef orcExternal fill:#f3f4f6,stroke:#6b7280,color:#1f2937;
class subscriber,dispatcher,position,motion,attitude,imu,vehicle orcMessage;
class handlers orcApp;
The dispatcher owns one receiver thread. Decoding occurs after receipt and handlers are submitted to an executor so slow handlers do not block bus reception.
A ZeroMQ socket must remain on the thread that owns it. ZeroMqSubscriber therefore creates its socket lazily on the thread that first calls receive() and releases that transport from the same owning thread during shutdown.
UI toolkit objects must not be mutated directly from dispatcher worker threads. Consumers should update a thread-safe state/cache from handlers, then marshal or poll that state on the UI toolkit thread.
Publisher architecture
Hardware and simulators should normalize data into domain state before publication. A typical automotive path is:
flowchart TD
elm["ELM327 transport / simulated adapter"] --> obd["Obd2Manager"] --> state["VehicleState [SI]"]
state --> publisher["VehicleStatePublisher"] --> zmq["ZeroMqPublisher"]
classDef orcApp fill:#dbeafe,stroke:#2563eb,color:#172554;
classDef orcService fill:#ede9fe,stroke:#7c3aed,color:#2e1065;
classDef orcController fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef orcMessage fill:#ffedd5,stroke:#ea580c,color:#7c2d12;
classDef orcAdapter fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
classDef orcExternal fill:#f3f4f6,stroke:#6b7280,color:#1f2937;
class elm orcExternal;
class obd,state orcController;
class publisher,zmq orcMessage;
The navigation path fans one normalized NavigationState sample into public domain topics:
flowchart TD
nav["NavigationController / simulator"] --> state["NavigationState"] --> publisher["NavigationStatePublisher"]
publisher --> position["position"]
publisher --> motion["motion"]
publisher --> attitude["attitude"]
publisher --> imu["imu"]
position --> zmq["ZeroMqPublisher"]
motion --> zmq
attitude --> zmq
imu --> zmq
classDef orcApp fill:#dbeafe,stroke:#2563eb,color:#172554;
classDef orcService fill:#ede9fe,stroke:#7c3aed,color:#2e1065;
classDef orcController fill:#dcfce7,stroke:#16a34a,color:#14532d;
classDef orcMessage fill:#ffedd5,stroke:#ea580c,color:#7c2d12;
classDef orcAdapter fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
classDef orcExternal fill:#f3f4f6,stroke:#6b7280,color:#1f2937;
class nav,state orcController;
class publisher,position,motion,attitude,imu,zmq orcMessage;
Attitude, IMU, and motion use the navigation sample timestamp. Position retains its GPS/source timestamp because fixes may update at a different cadence from inertial sampling.
Consumers must not know whether the source is simulated or physical hardware.
Lifecycle
Recommended startup order for deterministic component testing:
- start broker
- start subscribers/applications
- start publishers
ZeroMQ PUB/SUB subscriptions need time to propagate and early messages may be dropped. Continuous publishers should therefore tolerate slow joiners.
Recommended shutdown order is application-specific, but each dispatcher and publisher must be explicitly closed. MessageDispatcher.close() stops reception and drains/cancels executor work. ZeroMQ sockets should use zero linger for prompt application shutdown unless a future reliability requirement says otherwise.
Diagnostics
Consumer state models may expose a monotonically increasing receive counter and latest message timestamp. These are diagnostic metadata only and are not part of the public telemetry contract. They distinguish:
- no messages arriving
- messages arriving with unchanged values
- stale data
- decode or handler errors
Configuration boundary
Code must consume endpoint constants from messaging/zeromq/endpoints.py rather than duplicating port literals. Application-specific environment variables may override endpoints, but default topology semantics remain publisher ingress on 5556 and subscriber egress on 5557.
Extending the bus
A new public message type should include, at minimum:
- topic constant
- typed decoded message representation
- encoder
- validator
- decoder
- unit tests for valid and invalid payloads
- publisher helper when a domain state object exists
- broker integration coverage when practical
- IDD and
messaging/README.mdupdates documenting topic ownership and units
Do not place hardware-specific parsing or UI-specific presentation logic in a public messaging contract.