The automotive service owns a VehicleStateSourceIf and publishes complete SI-normalized VehicleState snapshots onto the OpenRoadCode ZeroMQ telemetry bus.
Applications such as Car TUI consume the public vehicle-state topic. They do not own the OBD-II adapter or simulation source.
The telemetry contract remains SI regardless of how a UI displays values. Metric/imperial conversion belongs at the presentation layer and uses common.units.
The automotive service is configured through the same runtime TOML used by the other producer services.
Simulation example:
Physical serial ELM327 example for Linux/Raspberry Pi:
Termux uses the Android Bluetooth bridge over localhost TCP rather than a local serial device:
Elm327Device owns the serial transport and Elm327TcpDevice owns the TCP transport. Both feed the same Elm327ObdAdapter, Obd2Manager, AutomotiveRuntime, and public VehicleState contract. This keeps the Raspberry Pi/Linux and Termux compositions symmetric above the transport boundary.
Real ELM327/Bluetooth links are request-limited, not CPU-limited. The Termux KONNWEI path measured about 170 ms per request, or roughly six physical OBD transactions per second. request_rate_hz therefore represents a physical request budget, not a per-signal refresh rate.
Obd2Manager performs at most one PID request per service tick and returns a complete snapshot from cached values. The active AutomotiveTelemetryProfile changes how that scarce request budget is spent:
HOME keeps glance telemetry fresh while preserving Trip inputs.PERFORMANCE strongly favors RPM and MAP/boost.ENGINE favors temperatures, load, and engine-health measurements.ECU favors trims, lambda, load, throttle-control, timing, and fuel-control data.TRIP favors fuel and trip-accounting inputs.BACKGROUND is deliberately Trip-biased while keeping low-rate engine context.Unsupported PIDs discovered during Mode 01 capability discovery are omitted entirely. A transient missing response does not erase the last valid cached measurement.
The profile is a semantic priority hint, not an ELM327 contract. A future passive-CAN or other automotive source may interpret the same profile differently or ignore it when all signals are already available continuously.
See Automotive Architecture for the full source, domain, scheduling, ECU-analysis, Trip, and UI boundaries.
Road speed is not polled from OBD. Navigation ground motion owns vehicle speed and is composed with the cached OBD engine state before publication.
VehicleStateSourceIf remains the hardware-facing automotive interface for both ordinary gauges and ECU-oriented telemetry. RPM, MAP, throttle, load, commanded equivalence ratio, and similar values are all decoded vehicle state, so they do not require a separate ECU transport contract.
A dedicated ECU-domain interface should be introduced only when OpenRoadCode gains a controller that owns richer derived ECU concepts such as closed-loop state, enrichment strategy, fuel trims, knock response, or control-state history. Keeping that distinction avoids duplicating the same raw telemetry across multiple interfaces.
The automotive runtime can augment each published VehicleState with an estimated transmission_gear. The estimator compares engine speed and road speed against a learned ratio profile.
By default the service looks for vehicle_gears.learned.toml. A different profile can be supplied with --gear-profile. If the profile does not exist, gear estimation is safely disabled.
Learn a manual-transmission profile automatically from stable samples collected across a drive:
For a deliberate per-gear calibration, use guided mode:
A single physical gear can be recaptured without discarding the other learned centers:
The learner rejects unstable RPM/speed windows and validates that learned ratios decrease as gear number increases. The default output is vehicle_gears.learned.toml; --output selects another profile path. Interactive prompts must be handled while safely stopped or by a passenger, never while driving.
The ratio estimator identifies forward gears only. RPM and road speed alone cannot reliably distinguish neutral or reverse, and the estimator intentionally returns an unknown gear during shifts, clutch slip, very low speed, or a poor ratio match.
transmission_gear is part of the public openroad.vehicle.state schema. After deploying a wire-contract change, restart long-running supervised producer processes so an older service does not continue publishing the prior schema.
Start the ZeroMQ broker first:
Simulation:
Physical vehicle:
For a serial Bluetooth ELM327 on Linux/Raspberry Pi, /dev/rfcomm0 must already exist and be connected before starting the service. Termux instead uses the configured Android bridge TCP endpoint.
The service publishes to [messaging].publisher_endpoint at the configured rate_hz.
The service configures the shared ORC JSON Lines logger at startup. Its records use the same rotating store and live viewer as navigation and the UI; no extra install dependencies are required.
Attach to automotive logs while ORC is running:
automotive.service records configuration, disabled service/publishing, and startup failures. automotive.runtime records publishing lifecycle, initial source unavailability, connection loss, recovery, and fatal/cleanup failures. Repeated failed connection attempts remain quiet until recovery; each connection attempt gets a local operation ID shared with ELM327 initialization and PID discovery.
automotive.elm327 records adapter connection/disconnection and DEBUG request outcomes (mode, obd_pid, response count, exception type). automotive.obd records capability counts, discovery fallback, profile changes, and DEBUG missing responses. pid always identifies the logging process; obd_pid is the OBD protocol PID. A missing response retains the existing cached measurement.
automotive.profiles correlates profile application with OBD profile changes. Repeated unchanged profiles stay quiet at INFO. IDs are generated inside this service and do not change the existing ZeroMQ profile-request or ELM327 wire contracts. automotive.motion reports subscription lifecycle and speed availability changes on received messages; this is not a stale-data timeout.
automotive.trip records start, pause/resume, completion, and reset using one local operation ID per trip. Repeated observations and repeated finish/reset calls stay quiet. UI processes need logging configured at their entry point (ORCui already does this).
Structured automotive records exclude raw ELM327/CAN/ECU payloads, VINs, diagnostic data, device addresses/paths, coordinates, driving measurements, trip summaries, and exception messages. DEBUG enables request metadata and publication events, without enabling payload dumps. Configure the service's environment and restart it to change its threshold:
The Logging quality gate tests these events, schema, correlation, privacy exclusions, quiet polling, reconnect behavior, and failure cleanup without a vehicle or running broker. Real ELM327/Bluetooth behavior still needs an installed system smoke test.
Car TUI already subscribes to vehicle telemetry through its shared VehicleBusState. With the broker and automotive service running, start it normally:
The Vehicle screen updates as new VehicleState messages arrive. No automotive simulation or OBD-II object is constructed inside Car TUI.
Producer services own hardware and simulation sources. Applications consume messaging contracts. This keeps the consumer path identical between bench simulation and the vehicle:
Switching between simulation and physical hardware therefore changes service composition, not application code or the wire contract.