Vehicle¶
Everything that talks to the car: the AutoPID loop and its tables, the vehicle store, detection, trouble codes, DBC signals, the UDS terminal, the native CAN bus, the J1939 listener, the OBD chip and the J2534 server.
AutoPID¶
AutoPID is the loop that asks the vehicle for values and publishes them. Its tables (groups, PIDs, filters) live in a file, its knobs (on or off, per-type enables, init strings, pause voltage) in the autopid settings.
| Route | Method | What it does |
|---|---|---|
/api/autopid |
GET | The dashboard view: groups, every parameter with its latest value and timestamp, and stats (running or why not, poll counters, the bus guard's verdict). |
/api/autopid/data |
GET | The flat snapshot {"Name":value,...} of every parameter that has polled at least once. The shape integrations consume. |
/api/autopid/config |
GET | The tables file as stored: groups, pids, filters. |
/api/autopid/config |
PUT | The whole tables document (up to 256 KB): validated (expressions dry-run, references, bounds), saved, and applied live without a reboot. 400 names the first bad entry and the old tables keep running. |
/api/autopid/std_scan |
POST | Starts the vehicle detection job (the same as /vehicles/detect): which protocol and dialect answer, the VIN, the supported standard PIDs, and whether the network is J1939. 202 {"started":true}; 409 while one runs or while the bus guard forbids transmitting. |
/api/autopid/std_scan |
GET | The job's state: idle, running (with phase: protocol, vin, pids, network), done or failed with error. |
/api/autopid/std_scan/result |
GET | The stored result: protocol, dialect (obd2, uds, j1939), VIN, and the supported PIDs as ready-made table rows. |
/api/autopid/std_table |
GET | The full built-in table of standard PIDs in the current vehicle's dialect: the "all possible standard PIDs" list. |
/api/autopid/test |
POST | Sends one PID through the real runner path and returns the raw reply, the decoded value and a transcript of every exchange. 409 while another test runs or the bus guard forbids it. |
/api/autopid/group |
POST | {"name":"...","enabled":true,"period_ms":1000} toggles a group until the next reboot. 404 unknown group. |
{
"groups": [
{
"name": "default",
"enabled": true,
"period_ms": 5000
}
],
"params": [
{
"name": "EngineCoolantTemp",
"unit": "degC",
"value": 90,
"ts_us": 4468562430
},
{
"name": "EngineRPM",
"unit": "rpm",
"value": 800,
"ts_us": 4468615488
},
{
"name": "VehicleSpeed",
"unit": "km/h",
"value": 0,
"ts_us": 4468660630
},
{
"name": "gps_latitude",
"unit": "°",
"value": -37.9051209,
"ts_us": 321552108,
"external": true
},
{
"name": "gps_longitude",
"unit": "°",
"value": 145.1450692,
"ts_us": 321552148,
"external": true
},
{
"name": "gps_altitude",
"unit": "m",
"value": 86.563,
"ts_us": 321552156,
"external": true
},
{
"name": "gps_speed",
"unit": "km/h",
"value": 0,
"ts_us": 321552168,
"external": true
},
{
"name": "gps_heading",
"unit": "°",
"value": 0,
"ts_us": 321552177,
"external": true
},
{
"name": "gps_satellites",
"unit": "",
"value": 14,
"ts_us": 321552187,
"external": true
}
],
"stats": {
"running": true,
"paused_voltage": false,
"paused_client": false,
"paused_diag": false,
"paused_bus": false,
"polls_ok": 2661,
"polls_failed": 0,
"passive_ok": 0,
"passive_failed": 0,
"passive_published": 0,
"passive_requested": 0,
"passive_refused": 0,
"j1939_listening": false,
"j1939_active": false,
"pids": 3,
"filters": 0,
"period_floor_ms": 50,
"sub_floor_pids": 0,
"now_us": 4469363488
},
"bus_guard": {
"bus": "silent",
"bus_kbps": 0,
"verdict": "allow",
"parked": false,
"reason": "vehicle bus silent",
"refused": 0
}
}
In stats: running is false while the loop pauses, and the reason is one of paused_voltage (battery under the pause voltage), paused_client (an app is using the OBD chip through a bridge; the loop yields for 10 s after its last write), paused_diag (the UDS Tool, a script or a J2534 tester holds the bus) or paused_bus (the vehicle bus runs at another bit rate than the protocol in use, nothing is sent). bus_guard says what the native controller heard before the chip was allowed to talk and what it decided: allow, search (the chip's own protocol search is used) or park. sub_floor_pids counts periods set below what the chip can do (about 53 ms per request).
{
"EngineCoolantTemp": 90,
"EngineRPM": 800,
"VehicleSpeed": 0,
"gps_latitude": -37.9051209,
"gps_longitude": 145.1450692,
"gps_altitude": 86.563,
"gps_speed": 0,
"gps_heading": 0,
"gps_satellites": 14
}
{
"groups": [
{
"name": "default",
"enabled_default": true,
"period_ms": 5000
}
],
"pids": [
{
"name": "EngineCoolantTemp",
"type": "std",
"cmd": "0105",
"group": "default",
"parameters": [
{
"name": "EngineCoolantTemp",
"expression": "B2-40",
"unit": "degC",
"class": "temperature",
"min": -40,
"max": 215
}
]
},
{
"name": "EngineRPM",
"type": "std",
"cmd": "010C",
"group": "default",
"parameters": [
{
"name": "EngineRPM",
"expression": "[B2:B3]*0.25",
"unit": "rpm",
"class": "speed",
"min": 0,
"max": 16384
}
]
},
{
"name": "VehicleSpeed",
"type": "std",
"cmd": "010D",
"group": "default",
"parameters": [
{
"name": "VehicleSpeed",
"expression": "B2",
"unit": "km/h",
"class": "speed",
"min": 0,
"max": 255
}
]
}
],
"filters": []
}
A pids[] row whose cmd reads PGN:<hex> is a J1939 parameter group read from the listener's store rather than asked of the OBD chip: PGN:F004 (engine controller 1 from the lowest source heard), PGN:FEEE@0 (pinned to source 0), PGN:FEE5? (a group sent on request). Such a row takes no init or rxheader.
Detection and the vehicle store¶
The device remembers up to 8 vehicles by VIN (or by a fingerprint of the responding ECUs when no VIN is readable), each with its protocol, dialect, tables and profile. First contact at boot recognises the current car, switches tables live when another stored car answers, and runs the detection job for an unknown one.
| Route | Method | What it does |
|---|---|---|
/api/autopid/vehicles |
GET | The store: current, max (8), and every vehicle with its key, VIN, name, protocol, dialect, j1939 flag, profile, the responder set, pending_profile, first and last seen. |
/api/autopid/vehicles/detect |
POST | Starts detection (same job as std_scan). 202; 409 while running. |
/api/autopid/vehicles/<key> |
PUT | {"name":"...","profile":"...","specific_init":"..."}, any subset. profile:"" keeps the car on standard PIDs only. 404 unknown key. |
/api/autopid/vehicles/<key>/activate |
POST | Switches to that car by hand: its tables are loaded live. 404 unknown key. |
/api/autopid/vehicles/<key> |
DELETE | Forgets the car and its tables. 204. |
{
"version": 1,
"current": "1WCAN0FW0P0000001",
"max": 8,
"vehicles": [
{
"key": "1WCAN0FW0P0000001",
"vin": "1WCAN0FW0P0000001",
"fingerprint": "bf3423ae",
"name": "1WC 0001",
"protocol": "6",
"chip_protocol": "",
"dialect": "obd2",
"j1939": false,
"profile": "",
"specific_init": "",
"ecus": "7E8:183F8003",
"std_supported": 16,
"pending_profile": false,
"first_seen": 1791486786,
"last_seen": 1791486786,
"scan_ts": 1791486786,
"current": true
}
]
}
{
"version": 1,
"protocol": "0",
"protocol_detected": "",
"dialect": "j1939",
"vin": "1WCANJ1939TRUCK01",
"fingerprint": "b84d5512",
"supported": [
{
"pgn": "F004",
"spn": 190,
"cmd": "PGN:F004",
"name": "EngineSpeed",
"parameters": [
{
"name": "EngineSpeed",
"expression": "(B3+B4*256)*0.125",
"unit": "rpm",
"class": "none",
"min": 0,
"max": 8031.875
}
]
},
{
"pgn": "F004",
"spn": 513,
"cmd": "PGN:F004",
"name": "ActualEnginePercentTorque",
"parameters": [
{
"name": "ActualEnginePercentTorque",
"expression": "B2-125",
"unit": "%",
"class": "none",
"min": -125,
"max": 125
}
]
},
An unknown car's detected standard PIDs are stored with "enabled": false: nothing is read until the autopid setting std_enabled is on and the user has ticked rows (in Quick Setup or under Automate, Parameters).
Trouble codes¶
Scanning (OBD modes 01, 03, 07, 0A; the WWH-OBD services on a UDS-dialect vehicle; DM1 and DM2 on a J1939 network) and clearing (mode 04, 14 FF FF 33, DM11 and DM3). Two gates in the autopid settings: dtc_enabled for scans, and dtc_allow_clear on top for clears.
| Route | Method | What it does |
|---|---|---|
/api/autopid/dtc |
GET | The gates, whether a scan runs, path (what a scan would do now: obd, wwh, uds, j1939) and the last report: lamp state and count per ECU, stored, pending, permanent and new codes, items with who reported what, descriptions from the uploaded databases. |
/api/autopid/dtc/scan |
POST | Starts a scan. 202; 403 when scanning is off; 409 while another chip job runs. Right after boot the chip needs about 15 s before a scan can answer. |
/api/autopid/dtc/clear |
POST | {"confirm":true,"mode":"always"} clears. mode if_any or if_only with codes gates the wipe on what is present. 403 unless clearing is allowed; 409 busy. Clearing wipes readiness monitors and the lamp as well; permanent codes survive by design. |
/api/autopid/dtc/db |
GET | The uploaded code description databases (up to 8). |
/api/autopid/dtc/db?name=<n> |
POST | Uploads a database: CSV, TSV, JSON map, JSON array or plain text, format detected. 400 names the first rejected line; 409 when the slots are full. |
/api/autopid/dtc/db?name=<n> |
DELETE | Removes a database. |
/api/autopid/dtc/db/search?q=&db=&offset=&limit= |
GET | Searches codes by prefix or description text (limit up to 100). |
/api/autopid/dtc/lookup?codes=P0420,P0171 |
GET | Descriptions for a list of codes, null for unknown ones. |
{
"enabled": false,
"allow_clear": false,
"scanning": false,
"path": "obd",
"report": {
"valid": false,
"ts": 0,
"mil": false,
"mil_count": 0,
"ecus": 0,
"protocol": "obd",
"j1939": false,
"stored": [],
"pending": [],
"permanent": [],
"new": [],
"sources": [],
"items": []
}
}
DBC files¶
Upload .dbc files (up to 4), browse their signals, and add chosen signals as AutoPID filters: each signal becomes a decoding expression, applied live.
| Route | Method | What it does |
|---|---|---|
/api/autopid/dbc |
GET | The uploaded files with message and signal counts. |
/api/autopid/dbc?name=<n> |
POST | Uploads a .dbc body. 400 when no signal could be read; 409 when the slots are full. |
/api/autopid/dbc?name=<n> |
DELETE | Removes a file. |
/api/autopid/dbc/signals?db=&q=&offset=&limit= |
GET | The signals with their byte layout, factor and offset, and whether each one is supported with its expression, or why not. |
/api/autopid/dbc/add |
POST | {"db":"...","signals":["Name",...],"group":"...","period_ms":1000} adds the signals as filters. 400 when the merged tables fail validation (nothing saved). |
UDS terminal¶
One request, one final response, over the native CAN bus (firmware ISO-TP, the recommended path) or through the OBD chip.
| Route | Method | What it does |
|---|---|---|
/api/uds |
GET | The live path without sending anything: backend setting and the one in use, whether CAN runs, the Exclusive bus switch, an open session, limits, the last transaction. |
/api/uds |
POST | {"exclusive":true} sets the runtime Exclusive bus switch: while the tool is in use, AutoPID and trouble code scans stay off the bus. |
/api/uds/session |
POST | {"action":"begin","tx_id":"7E0","rx_id":"7E8"} opens a tester-present session (3E 80 every tester_present_ms); {"action":"end"} closes it. 409 while another transaction owns the bus. |
/api/uds/request |
POST | {"tx_id":"7E0","rx_id":"7E8","data":"22 F1 90"} sends one request (up to 64 bytes) and returns the whole response (up to 4096 bytes), positive, the negative response code with its name, and the time it took. Handles the response-pending loop. 409 while another request is in flight. |
{
"backend_setting": "auto",
"backend_active": "obd_chip",
"provider": "esp_isotp",
"can_running": false,
"exclusive": true,
"exclusive_default": true,
"holding": false,
"autopid_paused": false,
"session_active": false,
"exclusive_idle_ms": 10000,
"max_request": 64,
"max_response": 4096,
"last": null,
"provider_stats": {
"sessions_open": 0,
"pdus_tx": 0,
"pdus_rx": 0,
"frames_fed": 0,
"tx_timeouts": 0,
"rx_dropped": 0
}
}
curl -X POST -H "Content-Type: application/json" -d "{\"tx_id\":\"7E0\",\"rx_id\":\"7E8\",\"data\":\"22 F1 90\"}" http://192.168.1.23/api/uds/request
Settings: uds_manager (backend auto, obd_chip or isotp; p2_ms, p2star_ms, tester_present_ms, exclusive).
Native CAN bus¶
The ESP32's own CAN controller, shared by every consumer: the CAN Monitor, slcan and GVRET bridges, ISO-TP for UDS and J2534, the J1939 listener.
| Route | Method | What it does |
|---|---|---|
/api/can |
GET | Enabled, running, silent, the bit rate (auto detection state and the rate proven by frames), the bus state, error counters by kind, who subscribes to the bus and what each queue lost, the last look a bus guard took. |
{
"enabled": false,
"running": false,
"silent": false,
"baud_auto": false,
"baud_kbps": 500,
"baud_detected": 0,
"state": "stopped",
"listen_only": true,
"verified": false,
"tx": 0,
"rx": 0,
"tx_errors": 0,
"rx_errors": 0,
"arb_lost": 0,
"bus_errors": 0,
"rx_missed": 0,
"rx_overrun": 0,
"tx_done": 0,
"tx_retries": 0,
"tx_lost": 0,
"dispatch_drops": 0,
"rx_bad": 0,
"rx_storms": 0,
"rx_deaf": 0,
"tx_refused": 0,
"link_switches": 0,
"link_demotions": 0,
"bus_off": 0,
"recoveries": 0,
"err": {
"stuff": 0,
"form": 0,
"bit": 0,
"ack": 0,
"other": 0
},
"probe": {
"result": "silent",
"baud_kbps": 0,
"frames": 0,
"age_ms": 4465950
},
"subscribers": [],
"subscribers_max": 16
}
The node always starts listening before it talks: state reads detecting (automatic bit rate, none proven yet), listening (fixed rate, no verdict yet) or mismatch (the bus carries traffic this rate cannot read) until frames prove the rate. Bus-off recovery is automatic with a growing back-off. Settings: can_manager (enabled, baud auto or 33 to 1000 kbit/s, silent).
J1939 listener¶
| Route | Method | What it does |
|---|---|---|
/api/j1939 |
GET | State (off, no_bus, listening), whether the bus looks like J1939, listen or active mode with the address claim, the VIN heard, decoded values of the built-in table for every group somebody sends, the sources, and each controller's DM1 lamps and codes. |
/api/j1939?pgns=1 |
GET | Everything in the store: every parameter group with source, destination, length, count, period, age and the payload in hex. |
/api/j1939?pgn=FEEC&sa=0 |
GET | One group's newest message. 404 when nobody sent it. |
/api/j1939?request=FECB&da=0 |
GET | Active mode only: sends a Request for that group and reports the outcome; the data lands in the store a moment later. 409 in listen mode or without an address on the bus. |
{
"enabled": false,
"state": "off",
"bus": "unknown",
"mode": "listen",
"claim": {
"state": "idle",
"address": 254,
"preferred": 249,
"tx_ready": false,
"name": "0000000000000000",
"claims_sent": 0,
"contests": 0,
"won": 0,
"lost": 0,
"held": 0,
"requests_answered": 0,
"cannot": 0
},
"tx": {
"frames": 0,
"failed": 0,
"requests": 0,
"acks": 0,
"nacks": 0,
"nacks_sent": 0,
"tp_to_me": 0,
"tp_cts": 0,
"tp_eoma": 0,
"tp_aborts": 0,
The listener needs the native CAN bus enabled, bit rate auto, silent. Settings: j1939 (enabled, mode listen or active).
OBD chip¶
| Route | Method | What it does |
|---|---|---|
/api/obd_chip |
GET | The OBD interpreter chip's wire and fan-out counters: ready, who holds it, client_idle_ms since a bridged app last wrote, UART bytes and overflows, per-subscriber drops, the EEPROM guard counters. The place to localise a lost frame between the chip and a socket. |
{
"ready": true,
"claim": "none",
"client_idle_ms": 4294967295,
"eeprom_guard": {
"rewrites": 0,
"blocked": 0,
"protocol_saves": 0
},
"uart": {
"rx_bytes": 34770,
"rx_chunks": 5342,
"rx_max_chunk": 27,
"rx_overflows": 0,
"rx_buffered": 0,
"tx_bytes": 13450
},
"subscribers": [
{
"idx": 0,
"name": "bridge",
"dropped": 0,
"queued": 0,
"depth": 64
},
{
"idx": 1,
"name": "bridge",
"dropped": 0,
"queued": 0,
"depth": 64
},
{
"idx": 2,
"name": "bridge",
"dropped": 0,
"queued": 0,
"depth": 64
}
]
}
J2534 PassThru¶
| Route | Method | What it does |
|---|---|---|
/api/j2534 |
GET | The PassThru server: enabled, port, listening, a tester attached and on which transport (tcp, serial, ble), channels, frame counters, the reflash and LAN gates, the Exclusive bus switch. |
/api/j2534 |
POST | {"exclusive":true} sets the runtime Exclusive bus switch, also for an attached tester. |
{
"enabled": false,
"port": 6809,
"listening": false,
"client_connected": false,
"device_open": false,
"channels": 0,
"frames_rx": 0,
"frames_tx": 0,
"allow_reflash": false,
"allow_lan": false,
"exclusive": true,
"autopid_paused": false,
"transport": "none",
"phase": "2 (CAN + ISO15765 channels)"
}
Settings: j2534_server (enabled default off, port 6809, allow_reflash default off, allow_lan default off: accepts only on the access point and the USB device link, exclusive). The Windows driver and the wire protocol are covered under J2534 PassThru.