# SpokoMeter: cyfrowy wskaźnik samochodowy

Cyfrowy wskaźnik samochodowy na ESP32 z aplikacją na telefon, zbudowany dzięki AI w Kotlinie i C++.

**Canonical:** https://spoko.space/pl/cyfrowy-wskaznik-samochodowy-claude-code/  
**Language:** pl  
**Published:** 2026-10-08  
**Tags:** claude-code, kotlin, android, esp32, lvgl, ble  
**Category:** Portfolio

---
## W skrócie {#w-skrócie}

|  |  |
| --- | --- |
| Problem | Gotowe cyfrowe wskaźniki do auta kosztują kilkaset euro, są zamknięte, a ich rozbudowa jest droga. Chciałem własny, który mogę dowolnie dostosować i rozwijać. |
| Co powstało | Cyfrowy wskaźnik samochodowy z okrągłym ekranem 360×360 na ESP32-S3, który wygląda jak część z fabryki VW, aplikacja SpokoMeter na Androida, która łączy się z nim przez Bluetooth i go konfiguruje, i widok w Android Auto na ekranie radia. |
| Moja rola | Pomysł, kierunek wizualny, prowadzenie agentów AI i review każdej zmiany, a poza kodem elektronika, lutowanie i obudowa drukowana w 3D. |
| Co jest w tym ciekawego | Wcześniej nie napisałem linijki ani w Kotlinie, ani w C++ i nie zrobiłem żadnej natywnej aplikacji mobilnej, tylko PWA i APK z Vue przez Capacitor. Zadziałała metoda pracy przeniesiona z projektów webowych. |
| Efekt | Działający wskaźnik i aplikacja po kilku dniach: ponad 130 zmergowanych pull requestów w dwóch repozytoriach, każdy przez lokalny build, testy i review. |

## Skąd pomysł {#skąd-pomysł}

Mam VW Polo 6R, przy którym modyfikacje stały się moim hobby, więc ten projekt nie wziął się z przypadku. Kilka lat temu zebrałem dostępne opcje w [przeglądzie dodatkowych wskaźników do Polo 6R](https://polo.blue/pl/dodatkowe-wskazniki-do-polo-6r-przeglad-dostepnych-opcji/). Od kilku lat jeżdżę też z niemieckim wyświetlaczem CANchecked, który [opisałem w recenzji](https://polo.blue/pl/wyswietlacz-canchecked-w-polo-6r-recenzja/). Działa, ale jak na urządzenie za ponad 650 euro jest wykonany słabo. Obudowa to tani wydruk 3D, a wyświetlacz ma przestarzałe złącze microUSB, które nawet z kątową wtyczką nie mieściło się dobrze, więc producent po prostu je podciął, żeby pasowało. Rozbudowa też kosztuje: sam pasek kilku diod do sygnalizacji zmiany biegu to około 300 zł.

Lubię DIY, więc zrobiłem własne rozwiązanie: otwarte i takie, które mogę dowolnie dostosować i rozwijać. Tak samo podchodzę do stron internetowych, bo zawsze jest coś do poprawienia i ulepszenia.

Gdyby chodziło tylko o klasyczne zegary, wystarczyłyby dodatkowe wskaźniki z Beetle albo Scirocco. Mnie interesował wskaźnik cyfrowy, który da się dowolnie dostosować, a kiedyś może trafi do obudowy środkowych zegarów ze Scirocco. Projekt był przede wszystkim testem możliwości.

Wygląd miał jednak pasować do auta, stąd główna zasada projektu: **domyślnie wszystko wygląda jak oryginalne zegary**, czyli font VW, czerwone pola z kropek jak w 6R i segmenty LCD jak we wskaźniku paliwa. Każdy dodatek jest opcją, którą się włącza.

Tarcze powstawały metodą prób i błędów. Projektował je Claude Code, a ja wiele razy zlecałem poprawki, porównując wynik z moimi zdjęciami zegarów 6R z bliska: tu przesunąć element, tam pogrubić linię albo zmienić odstęp. Chciałem, żeby wyglądały jak fabryczne, a jednocześnie pozwalały zmienić styl i wyświetlane parametry.

Do tego doszły trzy wymagania:

- **Konfiguracja z telefonu.** Telefon łączy się ze wskaźnikiem przez Bluetooth i ustawia strony, układy, kanały i kolory, a podgląd w aplikacji zgadza się z wyświetlaczem co do piksela.
- **Rozszerzalność.** Nowy kanał danych, nowy układ albo nowe źródło (CAN, OBD) nie może oznaczać przepisywania połowy kodu.
- **Android Auto.** Wartości na żywo, przełączanie stron i kody usterek na ekranie radia.

<div class="grid grid-cols-2 md:grid-cols-4 gap-4">

</div>

## Co już potrafi {#co-już-potrafi}

- Strony i układy: klasyczna tarcza, kilka wartości naraz, układ boczny z łukami, zegar i mapa.
- Ikony kanałów w stylu kontrolek VW.
- Kanał napięcia akumulatora, dodany jednego popołudnia.
- Mapę z OpenStreetMap, dodaną trochę dla zabawy: wygląda jak minimapa z GTA, ma motywy kolorów i działa offline.
- Kody usterek (DTC) i Android Auto.

Źródłem danych z auta będzie magistrala CAN. Moduł CAN jest następnym krokiem, więc na razie wartości pochodzą z symulatora wbudowanego w firmware. Cała reszta, czyli wyświetlacz, aplikacja, protokół i Android Auto, działa na prawdziwym sprzęcie.

Tak wygląda wskaźnik podłączony testowo na moim biurku:

<div class="grid grid-cols-2 md:grid-cols-4 gap-4">

</div>

Na [krótkim filmie z biurka](https://youtube.com/shorts/DEk-MXvRFso), opublikowanym na kanale polo.blue, wskaźnik przełącza kolejne strony:

## Architektura {#architektura}

- **Wyświetlacz** — ESP32-S3 z okrągłym ekranem GC9B72 360×360. C++, LVGL 9, NimBLE, PlatformIO.
- **Aplikacja SpokoMeter** — Kotlin, Material 3, ekrany przenoszone na Jetpack Compose. Usługa łącza BLE, historia jazd, mapy offline, testy UI w Robolectricu.
- **Android Auto** — Car App Library na szablonach: wartości na żywo, sterowanie stronami i lista kodów usterek na ekranie radia.

Wyświetlacz i telefon łączą się przez Bluetooth Low Energy (BLE) i rozmawiają własnym, małym protokołem ramek: ustawienia, strony, wartości na żywo, pozycja i większe paczki dzielone na kawałki z sumą kontrolną (zdjęcia tarcz, obszary mapy). Protokół opisuje jeden dokument, z którego korzystam ja i agenci pracujący nad obiema stronami.

## Jak pracuję {#jak-pracuję}

Claude Code (plan Max 20x) prowadzi implementację, ale jedno polecenie w czacie by tu nie wystarczyło. Całość opiera się na tym samym procesie, którego używam w projektach webowych.

1. **Opis i zdjęcie** — Mówię po polsku, czego chcę, zwykle ze zdjęciem: fabryczna tarcza 6R z bliska, dodatkowy zegar z Beetle, inne okrągłe wyświetlacze.
2. **Issue i plan** — Claude zamienia to w issue i plan. Przy zmianach wizualnych najpierw robi szybkie podglądy kilku wariantów, a ja wybieram, zanim cokolwiek trafi na sprzęt.
3. **Agenci równolegle** — Sub-agenci pracują osobno nad firmware i aplikacją. Część zadań z GitHuba robi samodzielnie Cezar, runner agentów od Open Mercato, i otwiera pull requesty.
4. **Lokalne bramki** — Build, testy jednostkowe i lint lokalnie, bo nie polegam na GitHub Actions. Do tego test na prawdziwym wyświetlaczu przez port szeregowy: zrzut ekranu skryptem, fps i zużycie pamięci.
5. **Review i merge** — Każdy PR przechodzi code review skillem om-code-review, a przed commitem kodu idzie /simplify. Merge robię dopiero po moim OK, agent nie scala swojej pracy sam.
6. **Na biurku** — Flashuję wyświetlacz, instaluję aplikację na telefonie i oglądam efekt na żywo. Dopiero potem zaczyna się kolejna iteracja.

Część zadań przejmuje [Cezar](https://github.com/open-mercato/cezar), otwartoźródłowy runner, który uruchamia agentów równolegle, a code review robi skill om-code-review z [zestawu Open Mercato](https://github.com/open-mercato/skills). Projekt ma też swoje zasady zapisane w plikach CLAUDE.md, które agenci czytają na starcie: kod i dokumentacja po angielsku, rozmowa po polsku, domyślne wartości jak w oryginalnych zegarach, każde ustawienie obsłużone w obu repozytoriach naraz.

Na zrzucie Cezar pracuje nad układem na szeroki ekran. Zanim otworzy pull request, sam renderuje aplikację w kilku rozmiarach ekranu i ogląda wynik, więc do review trafia zmiana, którą agent już widział. Ostateczną ocenę i tak robię sam, na telefonie.

## Bramki jakości, które miały znaczenie {#bramki-jakości-które-miały-znaczenie}

- **Testy zasad projektu** — Nic nie nachodzi na skalę, boczne skale są symetryczne, wskazówka nigdy nie zasłania cyfry, segmenty są równe. Zasady wizualne zapisałem jako testy, więc review nie musi ich pilnować.
- **Jedna geometria** — Podgląd w aplikacji i obraz na wyświetlaczu liczą się z tych samych wymiarów. Porównuję je obok siebie, więc rozjazd widać od razu.
- **Budżety na urządzeniu** — Klatki na sekundę, najwolniejsza klatka i fragmentacja pamięci LVGL mierzone na prawdziwym ESP32. Klasyczna tarcza z kropkami trzyma 44-45 fps.

## Trzy historie z projektu {#trzy-historie-z-projektu}

### Wierność oryginałowi: kropki i segmenty {#wierność-oryginałowi-kropki-i-segmenty}

Dobrze widać to na dwóch detalach. Czerwone pole na fabrycznej tarczy 6R to raster kropek, które gęstnieją w stronę końca skali. Pierwsza wersja miała trójkąty, druga kropki, a kolejne poprawiały ich kształt i kierunek na podstawie moich zdjęć z bliska. AI dwa razy źle odczytało zdjęcie: odwróciło kierunek gradientu, a raz uznało wskazówkę za odwróconą, choć nie była. Rozstrzygało zestawienie obok siebie w czterokrotnym powiększeniu.

Podobnie było z paliwem. Zwykły łuk zamieniłem na segmenty LCD jak w podstawowym Polo, z wydzieloną rezerwą i jednym czerwonym segmentem, gdy paliwa jest mało.

### Diagnoza na dowodach: wolne pobieranie mapy {#diagnoza-na-dowodach-wolne-pobieranie-mapy}

Pobieranie mapy offline szło bardzo wolno. Zamiast zgadywać, zmierzyłem, gdzie ucieka czas, i okazało się, że hamują je publiczne serwery Overpass. Po przejściu na kafelki wektorowe OpenFreeMap 50 km dróg wokół domu pobiera się w 11 sekund, a wcześniej po kilku minutach było gotowe 2 z 37 fragmentów.

### Podgląd na komputerze, zanim coś trafi na wskaźnik {#podgląd-na-komputerze-zanim-coś-trafi-na-wskaźnik}

Na początku drobne poprawki wyglądu szły pełnym cyklem: zmiana, build, flash, zrzut z wyświetlacza, więc każda seria poprawek ciągnęła się bardzo długo. Odwróciłem kolejność: najpierw oglądam kilka wariantów obok siebie, renderowanych na komputerze, a na sprzęt trafia tylko wybrany. Tak powstawała większość zmian wyglądu, od pierwszej wersji tarczy do obecnej.

<div class="grid grid-cols-2 gap-4 max-w-xl mx-auto">
<figure class="m-0">

<figcaption class="mt-2 text-center text-sm">Początek: paliwo jako zwykły łuk</figcaption>
</figure>
<figure class="m-0">

<figcaption class="mt-2 text-center text-sm">Później: segmenty LCD i lżejsze skale</figcaption>
</figure>
</div>

Podgląd i wyświetlacz rysuje ta sama geometria, więc przed każdym mergem kładę je jedno nad drugim. Dwie zmiany z ostatniego etapu sprawdzone w ten sposób:

<figure class="m-0 mt-6">

<figcaption class="mt-2 text-center text-sm">Czcionka i wielkość wartości na środku, wybierane w aplikacji: u góry wyświetlacz, na dole podgląd</figcaption>
</figure>
<div class="max-w-md mx-auto">
<figure class="m-0 mt-6">

<figcaption class="mt-2 text-center text-sm">Małe wskazówki na skali zegara cyfrowego, dodane po jednej uwadze: u góry wyświetlacz, na dole podgląd</figcaption>
</figure>
</div>

## Na czym się potykałem {#na-czym-się-potykałem}

- **Warstwa wizualna.** Najwięcej czasu zajęło dopracowanie wyglądu. Z logiką i testami AI radzi sobie wyraźnie lepiej niż z odwzorowaniem projektu co do piksela.
- **Zdjęcia wzorcowe.** Model źle odczytuje szczegóły ze zdjęć: kierunek, proporcje, co jest odwrócone. Wątpliwości szybko rozwiewa zestawienie obu wersji obok siebie w powiększeniu.
- **Wspólne zasoby.** Równolegli agenci dzielili jeden port szeregowy do wyświetlacza. Raz build testowy, który nie znał nowego kanału, skasował moje ustawione strony. Odtworzyłem je z kopii, a w zasadach projektu pojawiła się nowa: odczytaj stan właściciela, przywróć go po teście i sprawdzaj na najnowszym buildzie.
- **Uprawnienia.** Agent nie scala własnych PR-ów bez mojego wyraźnego OK. To bramka, którą zostawiłem celowo.

## Poza kodem: lutownica, suwmiarka, drukarka {#poza-kodem-lutownica-suwmiarka-drukarka}

Claude Code przyspiesza każdą warstwę projektu, ale kilku rzeczy nie zrobi za mnie.

**Elektronika.** Moduł wyświetlacza przychodzi z luźną listwą pinów do wlutowania. Pin GND siedzi na dużym polu miedzi, więc łatwo o zimny lut, a objaw jest podstępny: podświetlenie gaśnie, gdy poruszy się przewodami. Potem trzeba połączyć wyświetlacz z ESP32-S3 według tabeli pinów, pamiętając, że moduł bierze 3,3 V, a nie 5 V, i zbudować dzielnik napięcia do pomiaru akumulatora, który kalibruję multimetrem.

**Obudowa.** Zamiast wyklikać ją w programie CAD, opisałem ją kodem w OpenSCAD. Wymiary modułu wziąłem z rysunku producenta, a tych, których tam nie ma, jak grubość szkła nad płytką czy grubość lameli nawiewu, dopisałem z pomiarów suwmiarką. Model jest parametryczny: grubość ścianki odpowiada dwóm ścieżkom drukarki, a luzy pasowania są dobrane pod wydruk. Każda wersja ma też osobną część testową, czyli sam przód do szybkiego wydruku i przymiarki. To ta sama zasada co z podglądem tarcz na komputerze, tylko w plastiku.

Model napisał Claude, ale pomiar, przymiarka i ocena „pasuje czy nie” zostały po mojej stronie.

## Aplikacja i Android Auto {#aplikacja-i-android-auto}

SpokoMeter to moja pierwsza natywna aplikacja. Ma pięć zakładek: podgląd wyświetlacza z danymi z bieżącej jazdy, strony i ich układy, wygląd, ustawienia auta (wersja silnika, skala, pole czerwone, kody usterek) i historię jazd z eksportem do CSV. Każdy element tarczy da się kliknąć w podglądzie i zmienić, a wyświetlacz odświeża się od razu.

Na początku była to po prostu aplikacja: klasyczne widoki Androida, podgląd tarczy i kilka ustawień. Następnego dnia rano doszło Material 3, wieczorem podstawy Jetpack Compose, a po nich obecny układ: pięć zakładek, ustawienia aplikacji pod ikoną koła zębatego i podgląd obok ustawień na szerokim ekranie. Ekrany przechodzą na Compose po kolei, bez przepisywania całości naraz.

<div class="grid grid-cols-2 gap-4 max-w-md mx-auto">
<figure class="m-0">

<figcaption class="mt-2 text-center text-sm">Początek: jasna wersja „Spoko Gauge”</figcaption>
</figure>
<figure class="m-0">

<figcaption class="mt-2 text-center text-sm">Później: SpokoMeter</figcaption>
</figure>
</div>

<div class="grid grid-cols-2 md:grid-cols-4 gap-4">

</div>

Na ekranie radia aplikacja pokazuje wartości na żywo, pozwala przełączać strony wyświetlacza i wyświetla listę kodów usterek.

<div class="grid grid-cols-1 md:grid-cols-2 gap-4">

</div>

## Co dalej {#co-dalej}

- Moduł CAN i prawdziwe dane z auta: najpierw pasywny podsłuch magistrali, potem zapytania diagnostyczne o doładowanie i temperaturę oleju.
- Kody usterek z OBD zamiast symulowanych.
- Trzy wyświetlacze na jednym ESP32, w miejscu dodatkowych zegarów ze Scirocco.
- Obsługa platformy MQB, a nie tylko PQ25.

## Czego się nauczyłem {#czego-się-nauczyłem}

Przed tym projektem nie napisałem linijki ani w Kotlinie, ani w C++. Po kilku dniach aplikacja działa razem z wyświetlaczem i Android Auto. Pomogły mi nawyki z pracy nad stronami i aplikacjami webowymi: plan przed kodem, testy, review każdej zmiany i sprawdzanie zamiast zgadywania. AI pisze kod szybciej niż ja, ale to ja decyduję, co wchodzi, i odpowiadam za wynik.

Jeśli masz problem, przy którym trzeba połączyć aplikację, urządzenie albo istniejący system, [napisz do mnie](https://spoko.space/pl/kontakt/). Chętnie doradzę i pomogę go rozwiązać.
