The head unit was the dumb half of the dashboard. This is the smart half.

Every petrol car sold in the EU since 2001 has an OBD-II port under the dash. It is how the garage reads fault codes, and it will also stream live engine data to anything that asks: RPM, speed, coolant temperature, throttle, fuel trims, battery voltage. My 2009 Clio had the port and never used it. I wanted the engine on a screen.

Short answer: a Raspberry Pi 4 lives in the car, talks to a cheap Bluetooth OBD dongle, polls about twenty sensors, writes them to a local time-series database, and serves a touch HUD on the car tablet plus Grafana for the long view. It boots with the car, needs no network to work, and survives the ignition being cut mid-write.

The parts#

  • Raspberry Pi 4 (8 GB), Arch Linux ARM on an SD card. No screen wired to the Pi - the HUD is a web page the tablet opens.
  • A Bluetooth ELM327 OBD-II dongle in the port. The cheap blue ones. It speaks the ELM327 command set over a Bluetooth serial link.
  • A USB wifi adapter. The Pi's internal radio runs the car's own access point; the USB one is the uplink to whatever wifi is in range.
  • The car tablet from the head-unit build, as the screen.

Reading the port#

The dongle exposes a Bluetooth serial port (RFCOMM). A small service binds it to /dev/rfcomm0 and a Python collector (python-obd) opens it, asks the ECU which sensors it actually supports, and polls only those.

The dongle sleeps when the ignition is off. So when the car is away, the collector backs off exponentially, from 5 seconds up to 120, instead of hammering a dead link and filling the logs. When the engine comes back, it reconnects and resumes.

The pipeline#

OBD-II port -> BT dongle -> /dev/rfcomm0
   -> collector (Python)
       |-> time-series DB "car"        (raw, 365d)   1s fast PIDs, 10s medium PIDs
       |-> time-series DB "car_events" (forever)     fault codes + check-engine status
       \-> /live JSON  -> touch HUD on the tablet (:8080)
   -> rollup tasks: 1-minute and 1-hour mean/min/max (kept forever)
   -> trip summariser: one row per trip
   -> Grafana dashboards: Diagnostics / Trips / History

Polling is tiered, because the bus is slow and not every value changes at the same rate. RPM, speed, throttle, load, and airflow get read every second. Coolant, intake temperature, fuel level, fuel trims, O2 sensors, and battery voltage every ten. The fault-code scan runs once a minute and is written only when it changes. Points batch up and flush to the database every five seconds.

Raw data at 1 Hz adds up, so two rollups run on a schedule - one-minute and one-hour mean/min/max - and those are kept forever, while the raw second-by-second data ages out after a year. The history dashboards read the rollups; the live dashboard reads the raw bucket.

Hard part one: there is no gear sensor#

The ECU will give you RPM and speed. It will not tell you which gear you are in - a 2009 engine of this family has no gear PID. So gear is estimated: speed divided by RPM lands in a band, and each band is a gear. The bands come from the gearbox ratio sheet and the tire size. The two plausible factory tire fitments agree within about 4%, and you confirm the numbers against a "speed per 1000 RPM" panel on a real drive, then override them if your car disagrees.

Once you have an estimated gear, you get a feature for free: an eco-shift hint. Compute the highest gear that would keep RPM above a floor at the current speed, and if it is higher than the gear you are in, the HUD shows an up arrow. It is a shift light built from two numbers the car already broadcasts.

Hard part two: turning the key off is a power cut#

The Pi is powered from the car. Ignition off means power gone - no clean shutdown, possibly mid-write. Everything downstream has to assume that.

  • The collector flushes every five seconds, so the worst case is five seconds of lost readings.
  • The database replays its write-ahead log at boot and comes back consistent.
  • Trip summaries are DERIVED from the raw data after the fact, not written at a clean "trip end." The summariser scans for gaps longer than ten minutes and calls each span between them a trip. A power cut is just another gap.
  • The rollup tasks re-aggregate a wide window on every run, idempotently, so data written in the last seconds before a cut gets picked up at the next boot.

Designing for "the power can vanish at any instant" is the whole game with a car computer. Nothing in the chain waits for a shutdown that never comes.

The screen#

The HUD is a single HTML file with no dependencies, served by the collector and polling a JSON snapshot once a second. The tablet opens it as its homepage. Vertical swipe moves between views: Drive (big speed, an RPM arc, the estimated gear with its shift hint, instant L/100km, a warning strip for check-engine, hot coolant, or low battery), Trip (live trip-so-far cards and short charts), All sensors (a card per reading with a sparkline and a plain-language note on what healthy looks like), and Analysis (links into Grafana). Grafana does the long view: per-trip tables, consumption trends, coolant and battery history across months.

Reaching it from the couch#

The Pi runs the car's own wifi access point for the tablet and passengers, and uses the USB adapter to grab whatever uplink is in range - home wifi, a phone hotspot. It also joins my mesh VPN, so when the car is parked near home I can SSH in and pull data without walking outside.

What I would still add#

No real-time clock. Until the Pi syncs time over an uplink, the first trip after a cold boot is timestamped slightly wrong. A five-euro RTC module on the GPIO header fixes it properly. Everything else has held up: it boots with the car, logs the drive, and the dashboard is on the tablet before I have the seatbelt on.