A single Linux daemon that reads live EV telemetry (battery, odometer, tire
pressure, temperatures) and forwards it to a local REST consumer — designed for
aa-proxy-rs for Android Auto EV
routing and dashboards. One binary serves three adapter families, selected at
runtime by the config file:
| Adapter type | device.type |
How it talks to the car |
|---|---|---|
| Bluetooth ELM327 dongle | bluetooth |
Bluetooth RFCOMM, ELM327 AT commands |
| USB ELM327 dongle | usb |
USB serial, ELM327 AT commands |
| WiCAN Pro | wican |
BLE GATT, WiCAN AutoPid JSON |
Heritage: aa-proxy-obd was previously known as
canze-rsand forked frommanio/canze-rs— the original Renault Zoe SOC logger, whose full Zoe support (TPMS, odometer, energy) lives on as thezoeprofile. The name is a tribute to the CanZE project, which remains the reference for the underlying PIDs. Its home isaa-proxy/aa-proxy-obd.
| Vehicle | Profile | SOC | Battery (Wh) | External temp | Odometer | Tire pressure |
|---|---|---|---|---|---|---|
| Renault Zoe | zoe |
✅ | ✅ | ✅ | ✅ | ✅ |
| Hyundai Ioniq 5 | ioniq5 |
✅ | ✅ | ✅ | ✅ | ✅ |
| Kia EV6 | ev6 |
✅ | ✅ | ✅ | ✅ | ✅ |
| MG4 | mg4 |
✅ | ✅ | |||
| Kia Niro | niro |
✅ |
Adding a new vehicle requires no Rust changes — see Vehicle profiles.
WiCAN Pro users get whatever their AutoPid configuration exposes (typically SOC
plus outdoor temperature); the WiCAN firmware decodes PIDs itself, so the daemon
does not load a profiles/ file for device.type = "wican".
While the adapter is reachable and the car is awake, the daemon polls and
POSTs JSON to three endpoints on localhost. For each endpoint, it sends only
if at least one relevant field was produced this cycle (absent fields are
omitted); otherwise the endpoint is silently skipped.
| Endpoint | Payload |
|---|---|
POST {api_base}/battery |
battery_level_percentage, battery_level_wh, battery_capacity_wh, external_temp_celsius, battery_state_of_health, battery_voltage, battery_current, interior_temp_celsius (all optional) |
POST {api_base}/odometer |
odometer_km, trip_km (optional) |
POST {api_base}/tire-pressure |
pressures_kpa[] (FL, FR, RL, RR) |
If the active profile reports SOC as a percentage and battery_capacity_wh is
set in config, the daemon derives battery_level_wh = SOC × capacity / 100
automatically when the profile doesn't report Wh directly.
aa-proxy-obd [OPTIONS] [COMMAND]
Commands:
pair Pair the configured Bluetooth/WiCAN device (and optionally save the passkey)
Options:
-c, --config <CONFIG> Config file path [default: /etc/aa-proxy-obd.toml]
-d, --debug Override the configured log level to debug
-h, --help Print help
-V, --version Print version
Only device.type, the device's address (bt_mac for bluetooth/wican,
usb_port for usb), and vehicle.profile (for the ELM327 types) are
required. Everything else is optional and falls back to the defaults shown
below, so a minimal config is just:
[device]
type = "bluetooth"
bt_mac = "AA:BB:CC:DD:EE:FF"
[vehicle]
profile = "ev6"Full set of options with their defaults:
[device]
type = "bluetooth" # bluetooth | usb | wican
bt_mac = "AA:BB:CC:DD:EE:FF" # bluetooth + wican; the adapter's MAC
bt_passkey = 1234 # optional; enables in-process pairing
bt_max_connect_retries = 5 # bluetooth + wican
bt_timeout_secs = 10 # bluetooth + wican; per-attempt connect/response timeout
usb_port = "/dev/ttyUSB0" # ls -l /dev/ttyUSB* /dev/ttyACM* 2>/dev/null
usb_baud = 115200
[vehicle]
profile = "ev6" # ignored when device.type = "wican"
battery_capacity_wh = 77400 # optional; enables derived battery_level_wh
[daemon]
poll_interval_secs = 10
car_sleep_interval_secs = 100 # poll interval while the car is asleep
log_level = "info" # off | error | warn | info | debug | trace
log_file = "/var/log/aa-proxy-obd.log"
api_base_url = "http://localhost"
bridge_dropouts = true
publish_failure_threshold = 5
publish_breaker_secs = 300
cycle_failure_limit = 20The wican device type connects over Bluetooth, so it shares the bt_*
options (bt_mac, bt_passkey, bt_max_connect_retries, bt_timeout_secs).
Keys under [device] that don't apply to the selected type are ignored.
Profile JSON files ship under profiles/. At install time, copy
them to /etc/aa-proxy-obd/<profile>.json (e.g. profiles/ev6.json →
/etc/aa-proxy-obd/ev6.json). A profile lists one or more sources, each with
a kind discriminator.
{
"kind": "broadcast",
"init": ["ATCRA", "ATH1", "ATS1"],
"command": "ATMA",
"deadline_ms": 6000,
"idle_timeout_ms": 600,
"stop_when": ["external_temp_celsius"], // field names that end the scan early
"frames": [
{ "can_id": "673", "fields": [ /* FieldSpec */ ] }
]
}| Key | Type | Required | Behaviour |
|---|---|---|---|
name |
string | yes | Recognised names route into a known endpoint payload; unknown names are logged but not POSTed. |
byte_index |
int | one of byte/bit | uds_pid: offset from after the UDS header (e.g. 62 XX XX), so 0 = first data byte; negative counts from the end. broadcast: offset from the start of the CAN data bytes (no header). |
length |
usize | with byte_index |
1, 2, or 3 bytes |
bit_offset |
int | one of byte/bit | Motorola/big-endian bit index (bit 0 = MSB of byte 0) |
bit_length |
usize | with bit_offset |
1..=16 |
multiplier |
f32 | yes (default 1.0) | raw × multiplier + offset |
offset |
f32 | yes (default 0.0) | as above |
signed |
bool | optional | byte extraction only; sign-extend before scaling |
- Battery:
battery_level_percentage,battery_level_wh,external_temp_celsius,battery_state_of_health,battery_voltage,battery_current,interior_temp_celsius - Odometer:
odometer_km,trip_km - Tire pressure:
tire_fl_kpa,tire_fr_kpa,tire_rl_kpa,tire_rr_kpa
- Identify the OBD ECU TX/RX headers and the PID(s) for the parameters you want. CanZE is an excellent reference.
- Work out each field's byte (or bit) offset, length, multiplier, and offset within the response payload.
- Drop a new JSON file under
profiles/(and copy it to/etc/aa-proxy-obd/<name>.json). - Point
profile = <name>at it in the config. No code changes required.
If [device].bt_passkey is set, the daemon registers a BlueZ agent at connect
time that supplies the passkey during first pairing (this applies to both the
bluetooth and wican device types). BlueZ ignores the agent for
already-paired devices.
For one-shot setup:
aa-proxy-obd pair --passkey 1234 # or omit --passkey to be promptedThe subcommand discovers the device (if not already known to BlueZ), pairs it,
and on success offers to write the passkey back into the config under the
appropriate [device] key. Config comments and formatting are preserved.
- Per-endpoint circuit breaker. After
publish_failure_thresholdconsecutive POST failures on an endpoint (default 5), the daemon stops POSTing there forpublish_breaker_secs(default 300s), then probes once and closes on success. bridge_dropouts. When enabled (default), if a cycle produces no payload for an endpoint but the previous one did, the daemon re-POSTs the last-good value so downstream sees continuity. The cache clears when the car sleeps.- Cycle health watchdog. If
cycle_failure_limitconsecutive cycles produce zero successful POSTs (excluding intentional sleep cycles), the daemon logs an error and exits code 2 for a supervisor to act on.
| Exit code | Meaning |
|---|---|
| 0 | Clean shutdown on SIGINT / SIGTERM |
| 1 | Startup error — invalid config, missing profile, unparseable MAC |
| 2 | Cycle health watchdog tripped |
cargo build --releaseThe vendored .cargo/config.toml defaults the target to
aarch64-unknown-linux-gnu so the release build cross-compiles for common SBCs
(e.g. Radxa Zero 3W running an aa-proxy-rs image). Run the test suite on the
host with cargo test --target <host-triple>.
GPL-2.0. See LICENSE.
- Original
manio/canze-rsby Mariusz Białończyk — the Renault Zoe SOC logger and TPMS/multiframe work this fork builds on. - The CanZE project — primary reference for OBD PIDs.
- Reference projects whose capabilities this daemon absorbs:
aa-proxy-go-obd-feeder(USB OBD) andaa-proxy-wican(WiCAN Pro).

{ "kind": "uds_pid", "ecu_tx": "7E4", // request header "ecu_rx": "7EC", // response filter "pid": "220105", // service + PID (UDS / Mode 22 typical) "pre_request": "10C0", // optional; sent before the PID, errors ignored "multiframe": false, // opt-in to ISO-TP flow-control reassembly "fields": [ /* FieldSpec */ ] }