-
Notifications
You must be signed in to change notification settings - Fork 2
Diagnostics and OBD
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.
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.
| 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.
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.
mcp-can obd-request --service <hex> [--pid <hex>], or the send_obd_request MCP tool. Both decode known PIDs automatically (decoded field).
vehicle.dbc defines DIAGNOSTIC_REQUEST (frame 1047) and four per-ECU DIAGNOSTIC_RESPONSE_<ECU> messages (1048–1051, one each for ENGINE/ABS/AIRBAG/BODY_CONTROL). Constants and shared logic live in diagnostics.py; the responder is DiagnosticResponderThread in simulator/runner.py.
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.
| 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).
| 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 |
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.
Getting started
Reference
Simulation features
Operating it
Contributing