Skip to content

Diagnostics and OBD

farzad edited this page Aug 19, 2026 · 1 revision

Diagnostics and OBD-II

The simulator implements two independent protocols on top of the same virtual bus: standard OBD-II (SAE J1979) and a UDS-style diagnostic protocol defined in vehicle.dbc itself.

OBD-II (SAE J1979)

Implemented in obd.py, answered by OBDResponderThread (simulator/runner.py) listening on OBD_BROADCAST_ID = 0x7DF, responding on OBD_RESPONSE_BASE_ID = 0x7E8. Requests/responses are single-frame ISO-TP ([length] + payload, padded to 8 bytes) — this is a minimal demo implementation, not a full ISO-TP stack, so multi-frame responses (like a real VIN read) aren't supported.

Supported requests

Service PID Response
0x01 (show current data) 0x00 Supported-PIDs bitmask
0x01 0x05 Engine coolant temp (A - 40, hardcoded to 90°C)
0x01 0x0D Vehicle speed km/h (hardcoded to 50)
0x01 0x2F Fuel tank level % (hardcoded to 50%)
0x01 0x51 Fuel type (hardcoded to gasoline)
0x03 (DTCs) Any active fault-injection DTCs, or none
0x09 (vehicle info) 0x00 Supported-PIDs bitmask
0x09 0x0A ECU name ("MCP-ECU", truncated to fit single-frame)

Note Mode 01's coolant temp/speed/fuel-level PIDs are hardcoded constants, not wired to the correlated driving state — only the DBC-based ENGINE_STATUS/ABS_STATUS frames (and thus decode_can_frame/monitor_signal/get_vehicle_snapshot) reflect that. This is a known gap, not a design choice — see Roadmap.

DTCs (Mode 03)

obd.py::encode_dtc/decode_dtc implement the real J2012-style 2-byte DTC wire format (category bits + 4 hex digits) — e.g. P0217 encodes to (0x02, 0x17), matching the actual published encoding for that code. decode_response() dispatches a Mode 03 response to decode_dtcs() rather than the PID-based decode_pid_value(), since Mode 03 responses have no PID byte at all. See Fault Injection for how DTCs actually get set.

CLI / tool

mcp-can obd-request --service <hex> [--pid <hex>], or the send_obd_request MCP tool. Both decode known PIDs automatically (decoded field).

UDS-style diagnostics

vehicle.dbc defines DIAGNOSTIC_REQUEST (frame 1047) and four per-ECU DIAGNOSTIC_RESPONSE_<ECU> messages (10481051, one each for ENGINE/ABS/AIRBAG/BODY_CONTROL). Constants and shared logic live in diagnostics.py; the responder is DiagnosticResponderThread in simulator/runner.py.

No per-ECU addressing

DIAGNOSTIC_REQUEST has no target-ECU field — it's one shared request frame. The simulator treats every request as functionally addressed to all four simulated ECUs, so send_diagnostic_request/diag-request may (and typically does) return more than one response. This is a property of the sample DBC, not a simulator limitation.

Supported services (SERVICE_ID choices)

Service Hex Behavior
START_DIAGNOSTIC_SESSION 0x10 Acknowledged OK, no data
RESET_ECU 0x11 Acknowledged OK, no data
READ_DATA_BY_ID 0x22 Returns a deterministic-but-varying value: (parameter_id * 37) % 0xFFFFFFFFFF — no real per-parameter state, just enough to make different parameter IDs read back different data
ROUTINE_CONTROL 0x2F SERVICE_NOT_SUPPORTED
READ_MEMORY 0x31 SERVICE_NOT_SUPPORTED
WRITE_MEMORY 0x30 SERVICE_NOT_SUPPORTED
anything unrecognized SERVICE_NOT_SUPPORTED

Logic lives in diagnostics.py::handle_service() — pure function, no I/O, so it's tested independent of the bus (tests/test_diagnostics.py).

Response codes

Code Hex Meaning
OK 0x00 Success
GENERAL_REJECT 0x01 Generic rejection
SERVICE_NOT_SUPPORTED 0x11 Unimplemented service
INVALID_FORMAT 0x21 Malformed request
CONDITION_NOT_CORRECT 0x22 Precondition not met

CLI / tool

mcp-can diag-request --service-id <hex> [--parameter-id] [--data-field], or the send_diagnostic_request MCP tool — both collect every ECU's response within timeout/timeout_s.

Clone this wiki locally