At a glance
Where the idea came from
I own a VW Polo 6R, and modifying it has become my hobby, so this project did not come out of nowhere. A few years ago I collected the available options in an overview of additional gauges for the Polo 6R. For a few years I have also been driving with the German CANchecked display, which I wrote up in a review. It works, but for a device that costs more than 650 euros it is poorly made. The housing is a cheap 3D print, and the display has an outdated micro-USB port that did not fit well even with an angled plug, so the manufacturer simply trimmed it to make it fit. Extending it costs extra too: a strip of a few shift-light LEDs alone is about PLN 300.
I like DIY, so I made my own: open, and something I can adapt and extend however I like. I approach websites the same way, because there is always something to fix or improve.
If all I wanted were classic dials, the auxiliary gauges from a Beetle or a Scirocco would have done. I was after a digital gauge I can customise freely, which might one day go into the housing of the Scirocco’s centre gauges. Above all, the project was a test of what is possible.
It still had to suit the car, hence the project’s main rule: by default everything looks like the factory gauges, meaning the VW font, red zones made of dots as on the 6R, and LCD segments like the fuel gauge. Every extra is an option you switch on.
The dials came together by trial and error. Claude Code designed them, and I asked for corrections many times, comparing the result with my own close-up photos of the 6R gauges: move this element, make that line thicker, change a gap. I wanted them to look like factory dials while still letting you change the style and the values shown.
Three requirements came on top of that:
- Configuration from the phone. The phone connects to the gauge over Bluetooth and sets the pages, layouts, channels and colours, and the preview in the app matches the display pixel for pixel.
- Extensibility. A new data channel, a new layout or a new source (CAN, OBD) must not mean rewriting half the code.
- Android Auto. Live values, page switching and fault codes on the head unit.
What it does today
- Pages and layouts: a classic dial, several values at once, a sides layout with arcs, a clock and a map.
- Channel icons in the style of VW warning lights.
- A battery voltage channel, added in one afternoon.
- An OpenStreetMap map, added partly for fun: it looks like the GTA minimap, has colour themes and works offline.
- Fault codes (DTC) and Android Auto.
The car’s data will come from the CAN bus. The CAN module is the next step, so for now the values come from a simulator built into the firmware. Everything else, the display, the app, the protocol and Android Auto, runs on real hardware.
This is the gauge wired up for testing on my desk:
In a short clip from the desk, posted on the polo.blue channel, the gauge cycles through its pages:
Architecture
- Display ESP32-S3 with a round 360×360 GC9B72 screen. C++, LVGL 9, NimBLE, PlatformIO.
- SpokoMeter app Kotlin, Material 3, screens moving to Jetpack Compose. A BLE link service, drive history, offline maps, UI tests in Robolectric.
- Android Auto Car App Library templates: live values, page control and a list of fault codes on the head unit.
The display and the phone connect over Bluetooth Low Energy (BLE) and talk through a small frame protocol of their own: settings, pages, live values, position, and larger uploads split into chunks with a checksum (dial photos, map areas). One document describes the protocol, and both I and the agents working on either side use it.
How I work
Claude Code (on the Max 20x plan) leads the implementation, but a single prompt in a chat would not have been enough here. Everything rests on the same process I use on web projects.
- Step 1
Description and photo
I say in Polish what I want, usually with a photo: a close-up of the factory 6R dial, an auxiliary Beetle gauge, other round displays.
- Step 2
Issue and plan
Claude turns it into an issue and a plan. For visual changes it first renders quick previews of a few variants, and I pick one before anything goes to the hardware.
- Step 3
Agents in parallel
Sub-agents work separately on the firmware and the app. Some GitHub issues are handled on their own by Cezar, the agent runner from Open Mercato, which opens pull requests.
- Step 4
Local gates
Build, unit tests and lint run locally, because I do not rely on GitHub Actions. On top of that, a check on the real display over the serial port: a scripted screenshot, fps and memory use.
- Step 5
Review and merge
Every PR goes through code review with the om-code-review skill, and /simplify runs before each code commit. Nothing is merged without my OK; the agent does not merge its own work.
- Step 6
On the desk
I flash the display, install the app on the phone and look at the result live. Only then does the next iteration start.
Some of the work goes to Cezar, an open-source runner that runs agents in parallel, and code review is done by the om-code-review skill from the Open Mercato set. The project also has its own rules in CLAUDE.md files that the agents read at the start: code and docs in English, the conversation in Polish, defaults as on the factory gauges, every setting handled in both repositories at once.

Here Cezar is working on the wide-screen layout. Before opening a pull request it renders the app at several screen sizes and looks at the result, so what reaches review is a change the agent has already seen. The final judgement is still mine, on the phone.
Quality gates that mattered
- Tests for design rules Nothing overlaps the scale, the side scales are symmetric, the pointer never covers a numeral, the segments are equal. I wrote the visual rules as tests, so review does not have to police them.
- One geometry The preview in the app and the image on the display are computed from the same dimensions. I compare them side by side, so any drift shows at once.
- Budgets on the device Frames per second, the slowest frame and LVGL memory fragmentation, measured on the real ESP32. The classic dial with dots holds 44-45 fps.
Three stories from the project
Faithful to the original: dots and segments
Two details show it well. The red zone on the factory 6R dial is a halftone of dots that get denser towards the end of the scale. The first version used triangles, the second dots, and later ones corrected their shape and direction from my close-up photos. Twice the AI misread a photo: it reversed the direction of the gradient, and once it decided a pointer was reversed when it was not. A side-by-side comparison at four times the size settled it.

The fuel gauge went the same way. I replaced a plain arc with LCD segments like on the base Polo, with a separate reserve slot and one red segment when fuel is low.

Diagnosing with evidence: slow map downloads
Downloading the offline map was very slow. Instead of guessing, I measured where the time went, and it turned out the public Overpass servers were holding it back. After switching to OpenFreeMap vector tiles, 50 km of roads around home download in 11 seconds, where before only 2 of 37 pieces were done after several minutes.

A preview on the computer before anything reaches the gauge
At first, small visual tweaks went through the full cycle: change, build, flash, screenshot from the display, so every round of corrections dragged on for a long time. I reversed the order: first I look at a few variants side by side, rendered on the computer, and only the chosen one goes to the hardware. Most visual changes were made this way, from the first version of the dial to the current one.
The preview and the display are drawn from the same geometry, so before anything is merged I put them one above the other. Two changes from the last stage, checked this way:


Where I stumbled
- The visual layer. Getting the look right took the most time. AI handles logic and tests noticeably better than reproducing a design pixel for pixel.
- Reference photos. The model misreads details in photos: direction, proportions, what is flipped. Putting both versions side by side, enlarged, clears up doubts quickly.
- Shared resources. Parallel agents shared one serial port to the display. Once a test build that did not know about a new channel wiped the pages I had set up. I restored them from a backup, and the project rules gained a new one: read the owner’s state, restore it after the test, and check against the newest build.
- Permissions. The agent does not merge its own PRs without my explicit OK. That is a gate I kept on purpose.
Beyond the code: soldering iron, calipers, printer
Claude Code speeds up every layer of the project, but there are a few things it cannot do for me.
Electronics. The display module ships with a loose pin header you have to solder in. The GND pin sits on a large copper area, so a cold joint is easy, and the symptom is sneaky: the backlight goes out when you move the wires. Then the display has to be wired to the ESP32-S3 according to the pin table, keeping in mind that the module takes 3.3 V, not 5 V, and a voltage divider has to be built for measuring the battery, which I calibrate with a multimeter.

Housing. Instead of clicking it together in a CAD program, I described it in OpenSCAD code. I took the module’s dimensions from the manufacturer’s drawing and filled in the ones it lacks, such as the glass height above the board or the thickness of the air-vent slat, with caliper measurements. The model is parametric: the wall thickness matches two printer perimeters, and the fit clearances are tuned for printing. Each version also has a separate test part, just the front, for a quick print and a test fit. It is the same idea as previewing dials on the computer, only in plastic.

Claude wrote the model, but measuring, test-fitting and deciding whether it fits stayed with me.
The app and Android Auto
SpokoMeter is my first native app. It has five tabs: a preview of the display with data from the current drive, pages and their layouts, appearance, car settings (engine version, scale, red zone, fault codes) and drive history with CSV export. Any part of the dial can be tapped in the preview and changed, and the display updates straight away.
At first it was just an app: classic Android Views, a dial preview and a few settings. The next morning Material 3 arrived, that evening the Jetpack Compose foundation, and after it the current layout: five tabs, app settings behind a gear icon, and the preview beside the settings on wide screens. Screens move to Compose one at a time, without rewriting everything at once.
On the head unit the app shows live values, lets you switch the display’s pages and lists the fault codes.
What is next
- The CAN module and real data from the car: first passive sniffing of the bus, then diagnostic requests for boost and oil temperature.
- Fault codes from OBD instead of simulated ones.
- Three displays on one ESP32, in place of the Scirocco’s auxiliary gauges.
- Support for the MQB platform, not just PQ25.
What I learned
Before this project I had never written a line of Kotlin or C++. A few days later the app works together with the display and Android Auto. What helped were habits from building websites and web apps: a plan before code, tests, a review of every change, and checking instead of guessing. AI writes code faster than I do, but I decide what goes in and I answer for the result.
If you have a problem that means connecting an app, a device or an existing system, get in touch. I am happy to advise and help you solve it.



























