Zum Inhalt springen
Alle Beiträge

26. September 20267 Min. Lesezeit

Ein billiger LED-Streifen und ein undokumentiertes Protokoll

Der LED-Streifen-Controller in meinem Auto nahm jeden Bluetooth-Frame an und tat nichts damit. Ich beschreibe, wie ich herausfand, was er erwartet, und wie daraus die App WireAmbi mit Musikmodus wurde, in dem vom erkannten Beat bis zum Bluetooth-Schreibvorgang im Schnitt 6 ms vergehen.

Ein billiger LED-Streifen und ein undokumentiertes Protokoll

Der LED-Streifen in meinem Auto wird von einem MELK-OC21 gesteuert, einem Controller aus der ELK-BLEDOM-Familie. Solche Controller sind günstig und weit verbreitet, haben aber keinerlei Dokumentation, und die App des Herstellers läuft nicht auf dem Android-Auto-Display. Ich wollte das Licht vom Autodisplay aus ändern und habe deshalb eine eigene App geschrieben: WireAmbi. Die meiste Zeit ging dabei nicht für die Oberfläche drauf, sondern für den Controller selbst, der lange alles annahm und nichts tat.

Die erste Verbindung

Nach dem Verbinden zeigt der Controller einen einzigen GATT-Dienst, FFF0. Befehle werden ohne Bestätigung in die Characteristic FFF3 geschrieben, und FFF4 sendet in der Theorie Benachrichtigungen. Mein Controller hat darüber nie etwas geschickt, nicht einmal nach einem Frame mit Statusabfrage. Die App muss sich also selbst merken, was sie eingestellt hat.

Zu Beginn haben mich noch zwei Dinge erwischt. Der Controller kündigt den Dienst FFF0 nicht im Advertising-Paket an, deshalb fand ein Scan mit UUID-Filter nichts und ich musste nach dem Namen filtern. Außerdem akzeptiert er nur eine Verbindung gleichzeitig: Solange eine andere App oder das Gateway im Auto sie hält, verbindet sich das Handy nicht.

Das Frame, auf das der Streifen nicht reagierte

Jeder Befehl hat 9 Bytes: den Header 7E, das Ende EF und sieben Bytes dazwischen. Die erste Version von WireAmbi sendete die Farbe als 7E 07 05 03 R G B 10 EF. Jeder Bluetooth-Schreibvorgang war erfolgreich, aber der Streifen änderte seine Farbe nicht. Das sah nach einem Problem mit der Verbindung oder einem Fehler in der App aus, schuld war aber das Frame selbst: Dieses Farbformat kennt kein Modell aus der öffentlichen Variantenliste.

Diese Liste ist die Modelltabelle der elkbledom-Integration für Home Assistant, die am breitesten getestete öffentliche Übersicht dieser Controller. Daraus geht hervor, dass Controller, die als ELK-BLEDOM, MELK oder LEDBLE verkauft werden, dasselbe Grundgerüst des Frames haben, sich aber im Byte nach dem Header, im Füllwert und manchmal sogar in den Befehlswerten unterscheiden. Die Muster habe ich dieser Tabelle entnommen und die MELK-Variante an meinem Controller geprüft:

Befehl ELK-BLEDOM MELK (meiner) MELK-Ox
Einschalten 7E 00 04 F0 00 01 FF 00 EF 7E 00 04 01 00 00 00 00 EF 7E 04 04 F0 00 01 FF 00 EF
Farbe 7E 00 05 03 RR GG BB 00 EF dasselbe dasselbe
Helligkeit 7E 00 01 i FF 00 FF 00 EF 7E 04 01 i FF 00 FF 00 EF 7E 04 01 i 01 FF FF 00 EF

Daraus entstand die Protokollschicht in WireAmbi. Sie kennt drei Varianten und wählt anhand des Gerätenamens eine davon, nach dem längsten passenden Präfix. MELK-OC21 passt nicht zu MELK-OC10 und landet daher in der allgemeinen MELK-Variante. Hier wartete noch eine weitere Falle. Nach dem Verbinden kann BluetoothDevice.getName() für ein Gerät, das das System nicht gekoppelt hat, null zurückgeben, der Name muss also aus dem Scan-Ergebnis oder aus dem gespeicherten Gerät stammen. Bei mir wurde der Dialekt eine Zeit lang nur deshalb richtig gewählt, weil MELK die Standardvariante ist.

Atmen, im Handy berechnet

Die Farbe war der einzige Befehl, in dem alle Varianten übereinstimmen. Die eingebauten Effekte nummeriert jede Familie auf ihre eigene Weise: 48 ist bei MELK-Ox „Atmen“, beim normalen MELK bedeutet es nichts. Mein Controller bekam genau diese Nummer, und deshalb funktionierte der Atmen-Modus nicht.

Ich habe daher auf die eingebauten Effekte verzichtet, und das Atmen berechnet das Handy. Die App sendet 20 Farb-Frames pro Sekunde, die Helligkeit folgt einer Sinuskurve und sinkt nie unter 12 %. Der Regler stellt die Periode von 6 bis 1,2 s ein. Am MELK-OC21 habe ich Abstände von 49–51 ms gemessen, 19,8 Frames pro Sekunde und keinen einzigen abgelehnten Schreibvorgang.

Damit ein solcher Datenstrom funktioniert, muss die App das Tempo der Schreibvorgänge überwachen. Der Bluetooth-Stack nimmt jeweils nur einen Schreibvorgang an, und ein Schreibvorgang bei vollem Puffer endet mit der Ablehnung „busy“, wobei das Frame stillschweigend verloren geht. WireAmbi wartet daher auf die Bestätigung des Stacks, bevor es das nächste Frame sendet, und wiederholt den Schreibvorgang nach einer Ablehnung nach 15 ms, bis zu vier Versuche. Laufend berechnete Farben landen in einem Puffer mit nur einem Element, in dem nur die neueste bleibt. Vorher gab es dort eine Warteschlange ohne Limit, und je länger die Musik lief, desto ältere Farben zeigte der Streifen. Nach der Korrektur habe ich die Schreibvorgänge rund um eine Pause im Stück gezählt: 167 vor der Pause und null danach, in der Warteschlange blieb also nichts liegen.

Reihenfolge der Kanäle

Günstige Streifen sind manchmal in einer anderen Reihenfolge als RGB verdrahtet, und dann leuchtet „Rot“ grün. Der Controller hat dafür einen eigenen Befehl, 7E 06 81 P1 P2 P3 FF 00 EF, und WireAmbi kennt alle sechs Permutationen. Ich habe das in die Kalibrierung eingebaut: Der Farbtest zeigt die Vertauschung sofort, und ein Tipp stellt die richtige Reihenfolge ein, zum Beispiel GRB, ohne dass etwas umgelötet werden muss.

Musikmodus

Das Protokoll hat keinen Befehl, der im Controller eine Reaktion auf Ton aktivieren oder die Empfindlichkeit einstellen würde. Die Musik muss also das Handy hören, und es sendet fortlaufend die jeweils nächsten Farben.

Die erste Version hörte über das Mikrofon des Handys zu. Bei Android Auto geht der Ton jedoch an das Autoradio (und bei Tests an die Desktop Head Unit) und nicht an den Lautsprecher des Handys, das Mikrofon fing also nur Echo auf. Der Basspegel lag bei 0,00–0,03, und das erkannte Tempo sprang zwischen 87 und 182 BPM. Geholfen hat AudioPlaybackCapture, das den wiedergegebenen Ton abgreift, bevor das System ihn irgendwohin leitet. Nach dieser Änderung stieg der Basspegel auf 0,08–0,51, und das Tempo stabilisierte sich bei 116 BPM. Nebenbei begann die Verzögerung von Android Auto zu meinen Gunsten zu arbeiten: Die Analyse bekommt den Ton früher, als ihn die Lautsprecher im Auto abspielen.

Die Analysekette besteht aus einer FFT mit einem Fenster von 1024 Samples bei 22 050 Hz, einer logarithmischen Filterbank mit 24 Bändern pro Oktave, dem Onset-Detektor SuperFlux und einer Tempoverfolgung per Autokorrelation. Das Tempo dient der Vorhersage der Schläge, sodass der Blitz auch dann ausgelöst wird, wenn das Tempogitter „jetzt“ sagt, der Detektor aber noch nichts gesehen hat. Die Vorhersage führte jedoch einen neuen Fehler ein: Derselbe Schlag konnte zweimal ausgelöst werden, einmal als vorhergesagter und einmal als erkannter, einige Dutzend Millisekunden später, und auf dem Streifen sah das wie ein verdoppeltes Tempo aus. Jetzt gilt nach jedem Blitz eine Schutzzeit von 0,55 der Schlagperiode, und nur der erste Blitz kommt durch.

Die Verzögerung habe ich in den Logs gemessen. Von der Erkennung eines Schlags bis zum Bluetooth-Schreibvorgang vergehen im Mittel 6 ms (3 bis 13 ms in neun Messungen), das Senden ist also nicht die Quelle der Verzögerung. Den Rest liefert die Analyse: Das FFT-Fenster umfasst etwa 46 ms Ton, was im Mittel etwa 23 ms Verspätung bedeutet, und SuperFlux legt zwei Frames drauf, also etwa 20 ms. Einen Teil davon gleicht die Schlagvorhersage aus. Bei einem Stück mit 123 BPM (Periode 488 ms) lag der Median der Abstände zwischen den Blitzen bei 487 ms, und innerhalb von 20 Sekunden blitzte der Streifen 40-mal bei 41 erwarteten.

Auf derselben Analyse beruhen neun Reaktionsstile, vom Puls bis zu Funken. Der Stil entscheidet nur darüber, wie die Analyseergebnisse in Farbe umgesetzt werden, sodass ein neuer Stil keine Änderungen an der Audiokette erfordert.

Android-Auto-Bildschirm

Android Auto erlaubt fünf Templates pro Aufgabe. Jeder Wechsel des Template-Typs verbraucht eines, und nach Ausschöpfung des Limits schließt der Host die App. Meine ersten Bildschirme wechselten je nach Bluetooth-Zustand zwischen einem Raster und einer Meldung, bei einer instabilen Verbindung war das Limit daher in wenigen Sekunden weg. Jetzt hat jeder Bildschirm einen einzigen Template-Typ, und Fehler sowie leere Zustände zeige ich darin als gewöhnliche Kachel. Die zweite Überraschung: Der Kontext des Autodisplays erbt die Sprache der App nicht, deshalb übersetze ich die Texte fürs Auto separat. Das alles prüfe ich in der Desktop Head Unit.

Wie es heute aussieht

Auf dem Android-Auto-Display habe ich jetzt ein Farbraster, gespeicherte Szenen und Shortcut-Kacheln, die ich auf dem Handy zusammenstelle. Sprachbefehle erkennt Vosk, ohne Internetzugang. Der Musikmodus hört auf das, was gerade läuft, und funktioniert daher auch dann, wenn der Ton an die Lautsprecher im Auto geht.

Im Auto hält die Verbindung zum Streifen jedoch nicht das Handy, sondern das Gateway led-auto auf einem Raspberry Pi, weil der Controller nur eine Verbindung gleichzeitig akzeptiert. Das Handy spricht mit dem Gateway über den verschlüsselten Kanal WireLink. Das Gateway ist dann das einzige Gerät, das an den Controller schreibt, und meldet den Zustand selbst, sodass nebenbei auch das Problem mit dem Streifen verschwindet, der nichts zurückmeldet. Die Verbindung zum Streifen selbst lässt sich nicht verschlüsseln, und die Projektdokumentation sagt das offen.

Sobald das Handy mit dem Gateway gekoppelt ist, ist der Android-Auto-Bildschirm nicht mehr nur eine Fernbedienung für den Streifen, sondern ein Fahrzeugpanel mit den Reitern Auto, Licht und Protokoll. Auf dem Handy zeigt das Fahrzeugpanel den Zustand von Schlössern, Türen, Scheiben und Batterie, zusammen mit dem Golf-Modell.

Videos der Funktion und die Projektbeschreibung: WireAmbi im Portfolio.

Schreiben