Skip to content

Data paths and automation

How bytes and values leave the device: bridges and sockets, WebSocket channels, cyclic pushes to MQTT, HTTP and ABRP, the SD card logger, the rules engine, scripts, and the Home Assistant webhook.

Bridges and sockets

A bridge joins two endpoints (the OBD chip, the native CAN bus, a TCP server, a WebSocket channel, BLE, the USB serial link) with an optional translator (slcan, gvret, realdash, raw). Out of the box the OBD chip is bridged to the /ws/obd WebSocket channel. Bridges are the bridge_manager settings; socket servers the socket_manager settings. These routes are the live counters.

Route Method What it does
/api/bridges GET Every bridge with its two ends, translator, enabled, up, and byte and chunk counters in both directions plus send and codec errors.
/api/sockets GET Every TCP or UDP server with its port, enabled, up, bridge (the connection that uses the server, null when none does: such a server accepts clients and sends them nothing), clients, bytes, drops, reconnects and refused connections.
/api/ws GET Every WebSocket channel with its path, mode, client limit, enabled, up, bridge (as above; null on a channel is not a fault, /ws/log is fed by the log sinks), frame and byte counters, drops and refused connections.
{
  "bridges": [
    {
      "name": "br_obd",
      "a": "obd",
      "b": "ws_obd",
      "translator": "raw",
      "enabled": true,
      "stats": {
        "a2b_chunks": 5342,
        "b2a_chunks": 0,
        "a2b_bytes": 34770,
        "b2a_bytes": 0,
        "send_errors": 0,
        "codec_errors": 0
      },
      "up": true
    },
    {
      "name": "br_tcp_obd",
      "a": "obd0",
      "b": "obd",
      "translator": "raw",
      "enabled": true,
      "stats": {
        "a2b_chunks": 0,
        "b2a_chunks": 5342,
        "a2b_bytes": 0,
        "b2a_bytes": 34770,
        "send_errors": 0,
        "codec_errors": 0
      },
      "up": true
    },
    {
      "name": "br_usb_obd",
      "a": "usb_obd",
      "b": "obd",
      "translator": "raw",
      "enabled": true,
      "stats": {
        "a2b_chunks": 0,
        "b2a_chunks": 5342,
        "a2b_bytes": 0,
        "b2a_bytes": 34770,
        "send_errors": 0,
        "codec_errors": 0
      },
      "up": true
    }
  ]
}
{
  "servers": [
    {
      "name": "obd0",
      "proto": "tcp",
      "port": 35000,
      "enabled": true,
      "bridge": "br_tcp_obd",
      "stats": {
        "clients": 0,
        "bytes_in": 0,
        "bytes_out": 0,
        "rx_drops": 0,
        "tx_drops": 0,
        "reconnects": 1,
        "refused": 0
      },
      "up": true
    },
    {
      "name": "slcan0",
      "proto": "tcp",
      "port": 3333,
      "enabled": false,
      "bridge": null,
      "stats": {
        "clients": 0,
        "bytes_in": 0,
        "bytes_out": 0,
        "rx_drops": 0,
        "tx_drops": 0,
        "reconnects": 0,
        "refused": 0
      },
      "up": true
    },
    {
      "name": "gvret0",
      "proto": "tcp",
      "port": 23,
      "enabled": false,
      "bridge": null,
      "stats": {
        "clients": 0,
        "bytes_in": 0,
        "bytes_out": 0,
        "rx_drops": 0,
        "tx_drops": 0,
        "reconnects": 0,
        "refused": 0
      },
      "up": true
    },
    {
      "name": "udp0",
      "proto": "udp",
      "port": 17,
      "enabled": false,
      "bridge": null,
      "stats": {
        "clients": 0,
        "bytes_in": 0,
        "bytes_out": 0,
        "rx_drops": 0,
        "tx_drops": 0,
        "reconnects": 0,
        "refused": 0
      },
      "up": true
    }
  ]
}

WebSocket channels

/ws/* is the second reserved namespace. A channel is a settings-defined path that upgrades to a WebSocket and carries whatever bridge it is paired with:

Channel Path Ships Carries
ws_obd /ws/obd enabled the OBD chip (ELM327 ASCII), binary frames
ws_can /ws/can disabled the native CAN bus
ws_cli /ws/cli disabled the command line, text frames
{
  "channels": [
    {
      "name": "ws_obd",
      "path": "/ws/obd",
      "mode": "binary",
      "enabled": true,
      "stats": {
        "clients": 0,
        "frames_in": 0,
        "frames_out": 0,
        "bytes_in": 0,
        "bytes_out": 0,
        "rx_drops": 0,
        "tx_drops": 0,
        "refused": 0
      },
      "up": true
    },
    {
      "name": "ws_can",
      "path": "/ws/can",
      "mode": "binary",
      "enabled": false,
      "stats": {
        "clients": 0,
        "frames_in": 0,
        "frames_out": 0,
        "bytes_in": 0,
        "bytes_out": 0,
        "rx_drops": 0,
        "tx_drops": 0,
        "refused": 0
      },
      "up": true
    },
    {
      "name": "ws_cli",
      "path": "/ws/cli",
      "mode": "text",
      "enabled": false,
      "stats": {
        "clients": 0,
        "frames_in": 0,
        "frames_out": 0,
        "bytes_in": 0,
        "bytes_out": 0,
        "rx_drops": 0,
        "tx_drops": 0,
        "refused": 0
      },
      "up": true
    },
    {
      "name": "ws_log",
      "path": "/ws/log",
      "mode": "text",
      "enabled": true,
      "stats": {
        "clients": 0,
        "frames_in": 0,
        "frames_out": 0,
        "bytes_in": 0,
        "bytes_out": 0,
        "rx_drops": 0,
        "tx_drops": 0,
        "refused": 0
      },
      "up": true
    }
  ]
}

Data destinations

Cyclic pushes of the live AutoPID snapshot to MQTT topics, HTTP or HTTPS endpoints (bearer token, API key in a header or the query, basic auth, a certificate set for private CAs) and the ABRP telemetry API. The table is the data_destinations settings document (up to 8 rows, reboot to apply); the route shows the applied table with live counters.

Route Method What it does
/api/destinations GET Each destination with its type, period, auth kind, success and failure counts, skipped_offline (laps skipped because the link was down, never a failure), the back-off state, the last status and error. 503 before the settings apply.
/api/destinations/test POST {"name":"..."} delivers that destination once, now: the HTTP status, the time it took, an error text. 404 unknown name; 409 while another test runs; 504 when nothing answered within 20 s.
/api/destinations/abrp GET Which of ABRP's main telemetry fields the live parameter set provides (present) and, for each missing one, the parameter name that would fill it (missing: soc from SOC, power from HV_W, is_charging from CHARGING ...), plus the ABRP rate guard's constants: the 5 s period floor and the 2.5 s gap between requests.
{
  "running": true,
  "network": true,
  "mqtt": true,
  "enabled": true,
  "destinations": [
    {
      "name": "autopid",
      "type": "mqtt",
      "enabled": true,
      "url": "~/autopid",
      "period_s": 5,
      "auth": "none",
      "has_token": false,
      "has_api_key": false,
      "cert_set": "",
      "success": 708,
      "fail": 0,
      "skipped_offline": 61,
      "consecutive_failures": 0,
      "backoff_s": 0,
      "next_in_s": 0,
      "last_status": 0,
      "last_error": "",
      "last_error_time": "",
      "last_ok_time": "2026-10-09T04:06:55Z",
      "full_sent": false
    }
  ]
}

Three failures in a row engage a back-off ladder (10, 20, 40 s, up to 10 minutes) that one success clears; an HTTP 429 (rate limited) goes to the ladder's cap at once. The status document's abrp_pacer object counts the ABRP requests that waited for the 2.5 s gap (rows and the Test button alike) and the time they waited.

Data logger

Two streams to the SD card: parameters (dl_<epoch>.*) and CAN frames (can_<epoch>.*), each with its own format, rotation and retention in the data_logger settings. The files are ordinary entries under /sd/logs, downloaded through the file routes.

Route Method What it does
/api/logger GET Enabled, running, paused, storage state, the current file of each stream, rows and files, queued, written, dropped, rotations, records salvaged after a warm reset, corrupt files set aside.
/api/logger/gate POST {"enabled":false} pauses both streams until the next reboot (closes and flushes the files, so they can be downloaded); true resumes.
/api/logger/export?stream=params&since=<epoch>:<offset>&limit=N&format=jsonl GET Incremental pull of the parameter stream from the rotated files, jsonl or csv, optionally one parameter by name. The last line is {"_cursor":"...","more":bool}: feed _cursor back as since to follow the log.
{
  "enabled": false,
  "running": false,
  "paused": false,
  "storage_ok": false,
  "file": "",
  "file_rows": 0,
  "files": 0,
  "queued": 0,
  "written": 0,
  "dropped": 0,
  "errors": 0,
  "rotations": 0,
  "can": {
    "enabled": false,
    "file": "",
    "file_rows": 0,
    "files": 0,
    "queued": 0,
    "frames_written": 0,
    "frames_dropped": 0,
    "rotations": 0
  },
  "salvaged": 0,
  "corrupt": 0,
  "dir": "/sd/logs"
}

The active file of a stream is locked while it is written: a download or delete of it answers 409 instead of corrupting the card. Pause with the gate first, or wait for the rotation.

Rules engine

The Automate rules (when an event or a value condition, then actions) are the event_manager settings. Their vocabulary is discovered, never hard-coded: these routes list the events a rule can wait for, the actions it can run, the values it can read, and what happened.

Route Method What it does
/api/events/sources GET Every event the firmware publishes, with its keys and their types.
/api/events/actions GET Every action with the schema of its parameters and whether it can be undone by a while-rule.
/api/events/values GET The names a rule can read as ${name}; a trailing dot is a family (autopid.).
/api/events/log GET The last 32 events, oldest first, each with its data and the rules it fired, plus the engine's counters. The rule-debugging view.
/api/events/rules GET Per-rule runtime: enabled, fired count, seconds since the last fire, and active for a while-rule whose action is in effect.
[
  "wifi.ssid",
  "wifi.connected",
  "time.iso",
  "time.epoch",
  "battery.voltage",
  "autopid.data",
  "autopid."
]
[
  {
    "name": "bump_event",
    "enabled": true,
    "undo": false,
    "active": false,
    "fired": 0,
    "last_fired_age_s": -1
  },
  {
    "name": "motion_event",
    "enabled": true,
    "undo": false,
    "active": false,
    "fired": 0,
    "last_fired_age_s": -1
  },
  {
    "name": "battery_event",
    "enabled": true,
    "undo": false,
    "active": false,
    "fired": 1,
    "last_fired_age_s": 4460
  }
]
{
  "stats": {
    "running": true,
    "published": 5387,
    "dropped": 0,
    "fired": 1,
    "action_errors": 0,
    "suppressed": 0,
    "blocking_dropped": 0
  },
  "events": [
    {
      "source": "status",
      "name": "bit",
      "ts_us": 4443456048,
      "data": {
        "bit": "autopid_idle",
        "set": false
      },
      "fired": []
    },
    {
      "source": "status",
      "name": "bit",
      "ts_us": 4443490064,
      "data": {
        "bit": "autopid_idle",
        "set": true
      },
      "fired": []

Scripts

Berry scripts run on the device: inline from the Scripts page, stored under /data/scripts, or triggered by a rule. The bindings (uds, can_tx, emit, dtc_scan, log, sleep_ms and the rest) are described by the device itself.

Route Method What it does
/api/scripts GET The stored scripts, whether the engine is enabled and busy, the run time limit. Files are managed through the file routes (/data/scripts/<name>.be).
/api/scripts/run POST {"src":"..."} (inline, up to 8 KB) or {"name":"..."} (stored) runs a script and returns its output (up to 4 KB; errors with a traceback). 404 unknown name; 409 busy or disabled.
/api/scripts/check POST {"src":"..."} compiles without running: {"ok":true} or the syntax error with its line.
/api/scripts/stop POST Stops the running script.
/api/scripts/reference GET The language reference as data: limits, binding groups, every binding with its signature and an example, globals, a primer, common errors. The Scripts page renders this.
/api/scripts/examples GET The built-in example gallery; ?id=<name> returns one example's source as text.
{
  "scripts": [],
  "dir": "/data/scripts",
  "busy": false,
  "enabled": false,
  "max_runtime_ms": 10000
}
{
  "examples": [
    {
      "id": "hello",
      "title": "Hello, WiCAN",
      "desc": "Output, variables, loops and timing. Runs without a vehicle.",
      "needs": "",
      "level": 1,
      "size": 928
    },
    {
      "id": "obd_live",
      "title": "Live OBD-II values",
      "desc": "Poll engine speed, vehicle speed and coolant temperature and decode the answers.",
      "needs": "vehicle",
      "level": 1,
      "size": 1169
    },
    {
      "id": "vin",
      "title": "Read the VIN",
      "desc": "Mode 09 first, UDS data identifier F190 as the fallback, decoded to text.",
      "needs": "vehicle",
      "level": 1,
      "size": 951
    },
    {
      "id": "uds_session",
      "title": "A UDS conversation",
      "desc": "Claim the bus, open an extended session, read data identifiers, handle a refusal.",

Settings: script_engine (enabled default off, max_runtime_ms, allow_reflash default off, exclusive default on: from a script's first ECU access to its end, AutoPID stays off the bus).

Home Assistant webhook

The Home Assistant integration registers its webhook with the device, and the device then pushes status, autopid_data, config and gps to it. Home Assistant does this for you; the routes are listed for completeness.

Route Method What it does
/api/webhook GET The registered webhook and its delivery counters.
/api/webhook POST Registers a webhook target (what the integration calls).
/api/webhook DELETE Removes it.
{
  "url": "",
  "enabled": false,
  "interval": 60,
  "manual_override": false,
  "data_mode": "changed",
  "gzip": false,
  "status": "disabled",
  "last_post": "",
  "retries": 0,
  "success_count": 0,
  "fail_count": 0,
  "last_error": "",
  "last_error_time": ""
}