[Project] Decoding my Toyota's CAN bus from scratch to rebuild the factory app — read-only — in HA

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.

Looks amazing! This is something i could use for diagnostics on a hardtop convertible I’m having problems with and don’t want to buy an expense bidirectional scan tool just to fix the roof. I’ll be watching your progress

What hardware do you use for sonnecting to it from HA? Something pluigged into the OBD port, or the car is new enough there is another way?

There is a hardware section in the original post, isn’t it? Or, what else are you looking for?

Good question — and @k8gg is right that there’s a hardware section in the OP, but let me answer the “is it the OBD port?” part directly, since that’s the interesting bit.

Yes, it plugs into the OBD-II port, but not as an OBD dongle — I don’t poll PIDs. I tap the two CAN pins (6 = CAN-H, 14 = CAN-L) and passively sniff the raw 500 kbps powertrain bus, then decode the broadcast frames myself. The car’s a 2015, so there’s no modern telematics/API route — a physical tap is the way in.

Adapter: a Makerbase CANable 2.0 S — a cheap (~$15) STM32G431-based USB-to-CAN stick with a protective case. Any CANable / SLCAN-class adapter works the same way; socketcan + candump on a laptop is all you need to start logging. The one thing I insist on: force it into silent / listen-only mode so it never ACKs or transmits on the car’s bus — read-only by design. (Doubly important because I also log a Tesla, where some frames are real control messages.)

One gotcha: not everything is on the OBD port. On this Toyota the interior lights, wipers and seat heaters sit on a separate 250 kbps body bus that isn’t wired to the diagnostic connector at all — Toyota’s own manual even notes you can’t measure its termination at the DLC. Powertrain, chassis and most climate signals are on the OBD CAN, which covers most of the dashboard; the rest would need a second tap behind the dash.

For the permanent install I’m swapping the laptop+CANable for a LILYGO T-2CAN-FD (ESP32-S3) wired straight to 12 V, forwarding raw frames over MQTT to a backend that does the decoding and feeds HA.