Skip to content

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.