Endlich eine App für das Wandernavi

App-Icon von Wander Navi mit Nordpfeil auf grünem Grund und der Splash Screen mit Schriftzug
Links das App-Icon, rechts der Splash Screen. Der Nordpfeil kommt direkt aus den Icons der Firmware.

Das größte Problem von India Navi war nie die Firmware. Es war der Weg der Karte auf die SD-Karte. Bisher hieß das: GPX-Datei nehmen, mit dem Converter die Kacheln für Zoomstufe 14 und 16 herunterladen, jede Kachel in das 4-Bit-Format des Displays umrechnen, alles in die richtige Ordnerstruktur kopieren, SD-Karte raus, SD-Karte rein. Das macht man genau einmal mit Begeisterung.

Das Repository IndiaNavi_App gibt es seit Oktober 2022. Es enthielt genau einen Commit: das, was create-expo-app erzeugt. Vier Jahre später wurde daraus eine echte App. Zuerst das Update von Expo SDK 46 auf 57, also React Native 0.86 und React 19. Der alte GPX-Parser brauchte Node-Bibliotheken, die sich in React Native nicht bündeln lassen, also flog er raus. Und die App bekam einen neuen Namen: Wander Navi.

Vom Track zur SD-Karte

Die App öffnet eine GPX-Datei über den Dateidialog oder über „Öffnen mit“ auf Android. Sie berechnet den Bereich um den Track, lädt die Kacheln und rechnet sie in die sieben Farben des Displays um. Die Farbumrechnung ist aus dem alten Rust-Converter portiert. Mit dem Standardfilter ist die Ausgabe Byte für Byte identisch mit der Rust-Version. Dabei ist auch ein alter Fehler aufgefallen: Der Converter hat den Rand um den Track nicht auf allen Seiten berücksichtigt.

Inzwischen geht es auch ohne Track. Die Startansicht ist eine Karte, die man verschieben und zoomen kann, mit Suche nach Orten und Koordinaten. Der Bereich um das Kreuz in der Mitte wird geladen. Als Kartenquelle ist Thunderforest Outdoors voreingestellt, der API-Key hängt die App unsichtbar an jede Anfrage.

Sieben Farben sind nicht viel

Ein E-Paper mit Schwarz, Weiß, Grün, Blau, Rot, Gelb und Orange zeigt keine Pastelltöne. Im Filterbildschirm der App lassen sich deshalb zwei Displayfarben in beliebigem Anteil mischen, in Schritten von 1/16. Gemischt wird mit einem 8 × 8 Bayer-Muster, das über die Kachelgrenzen hinweg weiterläuft. Linien und Schrift würden im Muster zerfallen. Pixel, die deutlich dunkler sind als ihr hellster Nachbar, werden deshalb in der dunkleren Farbe durchgezogen. Original und Umrechnung kann man nebeneinander vergleichen, per Pinch bis auf den einzelnen Pixel. Den Filter kann man als JSON exportieren und wieder importieren.

Übertragung per WLAN

Damit das Kopieren der SD-Karte entfällt, hat die Firmware einen WLAN-Access-Point bekommen. Hängt India Navi am Ladegerät, öffnet es ein Netz IndiaNavi-XXXX mit WPA2 und zeigt Name und Passwort als QR-Code auf dem Display. Die App scannt den Code, verbindet sich und lädt die Dateien über eine kleine HTTP-API hoch. Weil das Netz kein Internet hat, braucht es dafür ein eigenes Expo-Modul, das die App an genau dieses WLAN bindet.

Display von India Navi mit Fortschrittsanzeige Upload 8 von 12 Dateien
Die App kündigt die Anzahl der Dateien an, das Display zeigt den Fortschritt. Screenshot aus dem Linux-Simulator der Firmware.

Kacheln, die schon auf der Karte liegen, werden übersprungen. Hochgeladen wird über zwei Verbindungen mit Wiederholung, track.gpx kommt als letzte Datei. Die Firmware schreibt jede Datei erst in eine .tmp und benennt sie um, wenn alle Bytes angekommen sind. Ohne Ladegerät schaltet sich das WLAN nach 10 Minuten ohne Nutzung ab.

Eine App, die aussieht wie das Gerät

Am 5. Oktober bekam die App ein Redesign nach dem Vorbild des Displays: nur die sieben Farben des E-Papers, flache Flächen mit schwarzen Rändern und harten Schatten. Die Icons erzeugt ein Skript direkt aus den Quellen der Firmware. Seitdem sieht die App aus wie ein Stück India Navi auf dem Handy. Mit einem GitHub Workflow, der bei jedem Push ein APK baut, ging es dann in kurzen Schritten bis zur Version 1.4.0 am 25. Oktober.

India Navi lag zwei Jahre in der Schublade

Der letzte Commit im India Navi Repository vor der langen Pause trägt das Datum 31. Mai 2024. Power Management für Light Sleep, eine neue Batterieanzeige, GPS-Punkte auf die SD-Karte loggen. Danach war Schluss. Das Studium, die Gruppenleitung, das Leben. Das Gerät lag in der Schublade, die Firmware auf GitHub und beide haben geduldig gewartet.

Balkendiagramm der Commits pro Quartal in Firmware und App von 2021 bis 2026, mit gut zwei Jahren Pause
Commits pro Quartal auf main in IndiaNavi_Firmware und IndiaNavi_App. Nach Mai 2024 war gut zwei Jahre Ruhe, im Oktober 2026 kamen 123 Commits zusammen.

Für alle, die India Navi nicht mehr auf dem Schirm haben: India Navi ist ein Offline-Navi zum Wandern. Kein Turn-by-Turn, kein „in 200 Metern links abbiegen“. Es zeigt die eigene Position auf einer Karte von der SD-Karte, dazu einen GPX-Track mit Höhenprofil, und aktualisiert das Bild jede Minute. Herzstück ist ein ESP32-S3 mit 16 MB Flash und 8 MB PSRAM, dazu das 7-Farben E-Paper von Waveshare mit 5,65 Zoll und 600 × 448 Pixeln und ein Quectel L96 als GNSS-Modul. Nicht verirren statt navigieren, das war von Anfang an das Ziel. Wer die Vorgeschichte nachlesen will: das Update vom Juni 2021 und die Firmware 2.0r2.

Der Splash Screen von India Navi: Schriftzug und ein Wanderer mit Rucksack und Stock
Der Wanderer mit Rucksack und Stock ist seit Jahren das Gesicht des Projekts. Die Vorlage liegt jetzt auch in der Dokumentation des Repositories.

Erst einmal wieder bauen können

Wer ein Embedded-Projekt zwei Jahre liegen lässt, kennt das: Nichts baut mehr. Die PlatformIO-Plattform hatte sich weiterbewegt, ESP-IDF auch. Der erste Schritt Ende September war deshalb das Update auf ESP-IDF 6.1 (espressif32 7.1.3) samt GCC 15. Klingt nach Versionsnummer hochdrehen, war es aber nicht. FatFs alloziert seine Puffer jetzt nur noch, wenn der Zeiger im FIL-Objekt NULL ist. Ohne Nullinitialisierung ist das Gerät beim Booten abgestürzt. Dazu ein Pufferüberlauf in ctime_r(), das 26 Byte braucht, und eine Handvoll geänderter API-Aufrufe für SD-Karte, Wakeup-Ursachen und HTTP-Client.

Ein Speicher-Audit, das wehgetan hat

Direkt danach habe ich den Code einmal komplett auf Speicherfehler durchsucht. Die Liste ist lang und liegt als MemoryAudit.md im Repository. Ein paar Highlights: Der NMEA-Parser hat seine Plugins als Zeiger auf den Stack des Aufrufers gespeichert. Ein PMTK001-Ack hat einen Funktionszeiger außerhalb des Arrays aufgerufen. Fehlte eine Kartenkachel, blieb der SD-Mutex gesperrt und das Gerät stand still. Der GPX-Parser hat unvollständige Dateien für immer neu geparst. Und das Höhenprofil hatte einen Heap-Überlauf. Dazu Lecks im OTA-Puffer alle 30 Sekunden und bei jedem Aus- und Einschalten.

Der spannendste Fund war aber kein Bug im klassischen Sinn. Ein GPX-Track mit 154 KB hat den kompletten internen RAM gefressen, 59 Byte waren noch übrig. Jeder Trackpunkt ist eine kleine Allokation, und kleine Allokationen landen im internen RAM. Dann konnte der SD-Kartentreiber seine DMA-Puffer nicht mehr anlegen und die Karte blieb weiß. Seitdem liegen Trackpunkte, Höhenprofil und Dateipuffer im PSRAM. Mit dem gleichen Track bleiben jetzt 30 KB internes RAM frei.

Altlasten abarbeiten

Am 6. Oktober habe ich mir dann die offenen Issues vorgenommen. Manche davon lagen schon peinlich lange herum. Das log.gpx wurde bei jedem Boot geleert und nie beschrieben, weil die GPS-Task auf ein Flag gewartet hat, das niemand gesetzt hat. Die Uhrzeit aus dem GPS war nur in Zeitzonen mit UTC+1 richtig, weil irgendwann jemand (ich) eine Stunde draufaddiert hat. Das Höhenprofil eines Tracks unter Meeresniveau wurde nie gezeichnet, weil der Wertebereich vorzeichenlos war. Und der nächste Wegpunkt war nur dann der nächste, wenn er auf beiden Achsen näher lag. Sechzehn Issues an einem Tag geschlossen, dazu ein Release v3.0.0. Die letzte Version davor war v2.1.1 aus dem Juni 2023.

Wie das in der Zeit möglich war

Ehrlich gesagt: Dieses Tempo hätte ich neben dem Job alleine nicht geschafft. Wer in die Commit-Historie schaut, sieht an vielen Stellen „Claude“ als Autor oder eine Claude-Session im Commit-Text. In meinem letzten Beitrag habe ich gefragt, wie ihr mit euren KI-Agenten redet. India Navi ist mein Versuchsfeld dafür geworden. Das Muster, das sich bewährt hat: Ich beschreibe das Problem mit Kontext und dem Ziel dahinter, der Agent liefert einen Pull Request, ich teste auf echter Hardware und schreibe dann die Fixes, die nur am Gerät auffallen. Nicht alles hat funktioniert. Ein Agent hat parallel angefangen, die ganze Firmware nach Rust zu portieren. Das liegt in eigenen Branches und ist nicht in main gelandet. Rust ist trotzdem ins Projekt gekommen, aber anders, dazu später mehr.

In den nächsten Beiträgen geht es um die App, um Bluetooth, um schnelleres Rendern und um Vektorkarten. In einem Monat ist ganz schön viel passiert.