I’m rebuilding my 2015 Toyota Highlander’s factory “vehicle status” screen as a read-only dashboard inside the Home Assistant companion app. The twist: every value on it is reverse-engineered off the car’s CAN bus by hand, because no public definition exists for this car’s engine. No OEM cloud, no OBD-II PID polling, no third-party dongle service — just passively listening to the bus and decoding raw frames myself.
Here’s where it is right now — my own dashboard, rebuilt entirely from decoded signals:
Why this isn’t just “plug in an OBD dongle”
This is a China-market 2015 Highlander with the 8AR-FTS 2.0T turbo engine. The North American XU50 never got that engine, so the open-source Toyota DBCs (opendbc) only decoded about 56% (64 of 115 IDs) for my car. The remaining ~51 IDs are unique to this powertrain and have zero public definition anywhere — as far as I can tell, decoding them is the first public decode of this exact combo.
And an OBD-II dongle only gives you a small generic PID subset. I wanted the real broadcast signals — the same ones the instrument cluster itself uses — which means sniffing the bus passively and decoding the raw frames.
The dashboard so far (three tabs)
Everything is driven 1:1 by decoded signals. The Vehicle Status tab above is the top-down car reacting live to the body signals — doors, windows, mirrors, turn signals, headlights, trunk, central lock.
Instrument Cluster — RPM, speed, gear, trip computer, chassis dynamics, per-wheel speed:
HVAC Panel — compressor, blower, air direction, rear defog. It also carries an honest evidence grid of what the bus can’t tell me — including “front defrost”, which after a full bit-by-bit scan turned out to simply not be broadcast on this bus at all (more below):
How I decoded it (action-differential method)
- Capture with a CANable V2.0 Pro (isolated), forced into silent / listen-only mode — the adapter never ACKs or transmits on the vehicle bus.
- 8 real drive logs so far.
- Method: do exactly one action at a time (open one door, blip the throttle, steer one way), 3–5 s apart, and note the timestamp. Back home, slice the log by timestamp and see which byte of which ID moved with it. Then correlate candidate bytes against known quantities (RPM, speed) — a single byte with |r| > 0.7 encodes that quantity; a 16-bit pair means higher resolution.
- Validate against physics: the 8AR-FTS maxes out around 1.17 bar (17 psi) boost and 350 N·m. If a byte saturates at full load and scales to ~1.2 bar, that’s the boost signal. Correlation high + units correct = confirmed.
- Result so far: 121 usable signals across 45 CAN IDs, 6 domains — every value measured on real data, not copied from a document.
| Domain | Signals | Examples (CAN ID · formula) |
|---|---|---|
| Powertrain (25) | RPM 0x1C4 ×0.78125 · Turbo boost 0x4AF ×1.25−100 kPa · Torque 0x2C1 ×0.03125 Nm · Throttle · MAF 0x49D g/s · Injected fuel 0x3D3 ×0.0005 ml |
|
| Driveline (6) | Gear 1–6 0x1D0 · PRND 0x3BC · Input-shaft RPM 0x1D0 ×0.390625 · Gear ratio 0x2D5 |
|
| Chassis (20) | Speed 0x0B4 · Steering angle 0x025 ×1.5° · Yaw rate 0x024 · Lateral accel 0x024 · Brake pressure 0x226 ×0.02 MPa |
|
| Climate (18) | Cabin temp 0x3B0 · Outside temp 0x380 ×0.625 · Blower · A/C compressor 0x380 B0.bit6 |
|
| Body (37) | Doors 0x620 · Windows 0x638 · Mirrors 0x623 · Lights 0x622 · Central lock · Seatbelts |
|
| Service (15) | Odometer 0x611 · Trip A 0x614 · Range 0x612 · Fuel level 0x3C3 · Service reminder 0x583 |
The stuff you only learn by sniffing a real car
This was the fun part — none of it is in any DBC, you only hit it on the actual vehicle:
- Body electronics live on a separate 250 kbps bus that isn’t wired to the OBD port at all. That’s why interior lights, wipers and seat heaters read nothing at the OBD-II connector. Toyota’s own manual even says the body MS-CAN resistance can’t be measured at the diagnostic connector.
- The fuel gauge lies on wake-up. After sitting, it reads 28.5 L and then falls to 16.5 L within seconds — float/sender damping settling. You have to take the median, never the first frame.
- Range doesn’t update after a refuel until the wheels turn. Fill up, sit still — the range display doesn’t budge. It jumps 0.42 s after the car starts moving. The car owner noticed it before the data confirmed it.
- Average economy only recalculates on the first drive-off after refuel, not on the refuel event itself. It stays frozen across the fill and restart, and zeroes on the first move.
- The coolant temp needle is deliberately damped — it sits dead-center while the real value climbs from ~40 to ~90 °C. Toyota does this on purpose so drivers don’t panic.
- The powertrain ECU goes silent the instant you switch off — you can’t observe heat-soak at all unless you drop back to IG-ON, where it keeps broadcasting.
- A negative result, proven: “front defrost” isn’t on this bus at all. I first assumed it was a linked action chain, but a full bit-by-bit scan across a clean on/off session showed zero bits changing. The switch simply isn’t broadcast on the powertrain bus — a genuine, evidenced “not readable”, not a decode I missed.
- The sunroof won’t open on IG-ON alone — it needs the engine running (anti-battery-drain interlock), which means sunroof captures have to be scheduled after ignition.
Hardware / going always-on
- Dev rig: laptop + CANable V2.0 Pro.
- Permanent collector: a LILYGO T-2CAN-FD (ESP32-S3). I picked the FD variant specifically for its MCP2518FD’s 2 KB RX FIFO — the bus runs 1147 frames/s (one every ~0.87 ms), and the cheaper MCP2515’s 2-message buffer drops frames every time the ESP32’s WiFi stack steals the CPU for a couple ms. Not for CAN-FD (the car is CAN 2.0) — purely for that FIFO.
- Runs straight off 12 V and sleeps on ignition-off so it won’t drain the battery.
Architecture
CAR (collector = dumb pipe, decodes nothing):
CAN adapter -> read raw frames -> 100 ms batch + compress + local write-ahead buffer -> MQTT
| (over the car's 4G / VPN link)
v
BACKEND:
MQTT broker -> decode against my DBC -> time-series DB -> Home Assistant sensors + dashboard
The collector’s SoC is weak, so it decodes nothing — all decoding is server-side, and the raw frames are archived so I can re-decode as the DBC keeps growing. The write-ahead buffer means a dropped mobile link loses nothing; frames replay in order on reconnect.
Safety
Listen-only, always. The adapter is forced silent and never transmits — doubly important because I also log a Tesla Model 3, where some frames are genuine control messages. I only ever read.
One trap worth flagging: this CANable’s firmware is a non-standard slcan — the usual “open in listen-only” command silently fails and opens the bus in normal mode (which would ACK on the car’s bus). So I wrote a thin layer of my own that makes silent mode the default and impossible to bypass.
Why I’m posting
Before I wire up the final MQTT → HA sensors, I’d love input from anyone who’s built an OEM-style, car-silhouette dashboard in HA:
- picture-elements vs a custom card vs Mushroom for the “car graphic + live overlays” look?
- cleanest way to manage 100+ MQTT sensors without drowning the entity list?
- anyone got a layout like this they can share?
Happy to open up the DBC and the decode scripts as this matures. I’ll keep this thread as a build log — more drive logs and the always-on collector are next.


