Flugradar auf dem M5Dial

Hier fliegt ständig was über die Stadt und ich wollte einfach wissen, was das ist. Aufs Handy gucken geht natürlich, aber dann muss man erst die App aufmachen. Ich wollte was, das auf dem Schreibtisch steht und läuft. Der M5Stack Dial lag hier noch rum. ESP32-S3, rundes Display mit 240 Pixeln, und außen rum ein Drehknopf. Rundes Display, Radar, das schreit ja förmlich danach. Ist ein normaler Arduino-Sketch geworden, mit den Bibliotheken M5Dial und ArduinoJson.

Was es kann
Drehen zoomt. Reichweite geht von 5 bis 750 km. Ein Flugzeug antippen macht eine Detailkarte auf: Callsign, Land, Höhe, Speed, Steig- oder Sinkrate, Kurs. Nochmal tippen und sie ist wieder zu. Auf den Knopf drücken kommt man in die Einstellungen und kann Standort, Reichweite, Intervall und die Labels ändern. Das landet alles im Flash, nach dem Neustart ist es noch da. Ich wollte nicht jedes Mal neu flashen müssen, nur weil ich woanders bin.
Ein paar Städte liegen als graue Punkte mit drauf. Ohne die weißt du nämlich überhaupt nicht, wo das Flugzeug gerade ist. Ob das jetzt über Oldenburg hängt oder schon über der Nordsee, siehst du sonst nicht. Die Flieger sind nach Höhe eingefärbt. Tief ist amber, hoch ist weiß, am Boden grau.
Woher die Daten kommen
Von OpenSky. Die sammeln ADS-B-Daten von Empfängern auf der ganzen Welt und geben das über eine REST-Schnittstelle raus. Ich hole /states/all mit einer Bounding Box um meinen Standort, und wenn ich zoome, wird die Box neu gerechnet. Anmelden geht inzwischen nur noch über OAuth2 mit Client Credentials, Benutzername und Passwort nimmt die OpenSky REST API nicht mehr. Du legst dir im Account einen API-Client an und kriegst Client-ID und Secret. Ohne Zugangsdaten kommst du zwar auch dran, aber die Limits sind so knapp, dass es für was, das alle paar Sekunden nachlädt, nicht reicht. WLAN und Zugangsdaten stehen in einer config.h, die per gitignore draußen ist. Daneben liegt eine config.h.example. Wer sich das Repo zieht, muss die erst ausfüllen, sonst kompiliert gar nichts.
Die Stelle, wo ich am längsten festhing
Anfrage lief. HTTP 200. Und states leer. Jedes Mal. Kein Fehler, keine Meldung, einfach null Flugzeuge über einer Großstadt. Ich hab die Bounding Box kontrolliert, den Token, die Uhrzeit, die Limits. Alles Sachen, wo es plausibel klemmen könnte. War aber nichts davon. Die Daten kamen komplett an, die 200 stimmte. Falsch gelesen hab ich sie erst selber. OpenSky antwortet nämlich mit chunked transfer encoding. Die Antwort kommt in Stücken, vor jedem steht die Länge. Wenn du das JSON direkt aus http.getStream() an ArduinoJson gibst, sieht der Parser diese Längenangaben mitten in den Daten, kommt raus und liefert dir am Ende ein leeres Array. Ohne zu meckern. Das ist das Fiese daran.
// getString() dekodiert die chunked-Antwort korrekt (getStream() nicht!)
String payload = http.getString();
Erst komplett einlesen, dann parsen. Kostet RAM, weil die ganze Antwort im Speicher liegt, aber auf dem S3 ist genug da. Was mich da echt geärgert hat: Der Fehler sieht aus wie ein Datenproblem. Dass die Übertragung völlig in Ordnung ist und nur mein Parser sie falsch liest, war so ziemlich der letzte Verdacht. 
Noch so eine Sache: die Prototypen
Die Arduino-IDE baut dir die Funktionsprototypen normalerweise automatisch. Macht sie aber nicht, wenn der Rückgabetyp aus einem Namespace kommt. Bei lgfx::LGFXBase* steigt sie einfach aus und sagt nichts. Der Compiler kennt die Funktion an der Aufrufstelle dann nicht und wirft dir eine Fehlermeldung um die Ohren, die mit dem eigentlichen Problem nichts zu tun hat. Forward-Deklarationen von Hand oben in den Sketch, dann läuft es.
Flackern wegkriegen
Wenn du direkt aufs Display malst und sich das Bild alle paar Sekunden neu aufbaut, sieht das unruhig aus. Deswegen wird alles erst in ein Sprite im Speicher gezeichnet und dann in einem Rutsch rübergeschoben. Das Sprite läuft mit 16 Bit. Reicht der Speicher nicht, geht es automatisch auf 8 Bit runter:
if (canvas.createSprite(SCREEN, SCREEN)) {
// 16 Bit, alles gut
} else {
canvas.setColorDepth(8);
useSprite = canvas.createSprite(SCREEN, SCREEN) != nullptr;
}
Die Projektion ist ziemlich simpel: Lat/Lon-Differenz zum Standort in Kilometer umrechnen, dann in Pixel, Norden oben. Bei voller Reichweite ist ein Pixel gut sechs Kilometer. Für die Übersicht reicht das dicke. Wenn du exakte Positionen brauchst, kommst du mit so einer Rechnung auf 750 km natürlich nicht hin, dafür bräuchte es eine richtige Kartenprojektion. Wollte ich aber nicht. 
Wie es jetzt aussieht
Läuft. Gut 700 Zeilen Sketch, hängt am WLAN, aktualisiert sich selber. Was noch fehlt:
- Verschieben per Touch. Im Moment änderst du den Mittelpunkt nur über die Koordinaten im Menü, das ist umständlich.
- Ein eigener Empfänger. Mit einem dump1090 im Haus wäre ich die Schnittstelle und ihre Limits komplett los und hätte aktuellere Daten dazu.
- Küstenlinien. Die Städte als Punkte sind ein Notbehelf. Richtige Geodaten wären schöner, brauchen aber Speicher, den ich auf dem S3 nicht habe.
Der eigene Empfänger reizt mich am meisten. Dann braucht das Ding nichts mehr außer einer Antenne.
Entwurf mit KI aus meinen Projektnotizen, redigiert von mir.Wie diese Texte entstehen
