Sechs Tasten, zehn Millisekunden — und ein schwarzes Panel
Im letzten Beitrag habe ich ein eigenes Widget auf das LED-Panel eines elektronischen Dartboards geschmuggelt. Es zeigt seitdem Solardaten, Wallbox, Strompreise, Wetter, den 3D-Drucker und ein paar andere Dinge, die Marcus interessieren.
Umschalten ließ sich das nur aus Home Assistant. Also: Handy raus, App auf, Dropdown antippen. Am Gerät selbst — direkt unter dem Panel, gut erreichbar — sitzt ein Steuerkreuz mit sechs Tasten. Die gehören der Firmware. Für Menü und Spielabbruch.
Das hat mich zwei Tage lang gestört.
Die Tasten liegen offen herum
Ich habe das SDK durchgesehen, mit dem mein Widget seine Bilder ans Panel schiebt. Und da steht tatsächlich eine Funktion drin, die die Tasten ausliest. Nicht dokumentiert, nicht erwähnt, einfach vorhanden.
Dahinter steckt keine API, sondern ein Stück gemeinsam genutzter Speicher. Ein C-Programm hält die Hardware am Laufen und legt den Tastenzustand als ein einziges Byte ab. Ein Bit pro Taste: A ist 1, B ist 2, hoch 4, rechts 8, links 16, runter 32, Home 64. Fertig. Wer das Byte lesen darf, weiß, was gedrückt ist.
Mein Widget darf. Auch in einer Sideload-Session, also genau in dem Modus, in dem ich das Panel gekapert habe.
Ich habe also die Abfrage in meine Renderschleife eingebaut, die ohnehin zehnmal pro Sekunde läuft. Und dann gedrückt.
Nichts.
Nochmal. Nichts. Dann viermal hintereinander hektisch — einmal reagierte es.
Das Problem war nicht der Code, sondern die Physik
Ich habe aufgehört zu raten und angefangen zu messen: ein winziges Skript auf dem Gerät, das das Byte alle zehn Millisekunden liest und jede Änderung mit Zeitstempel in eine Datei schreibt. Dann konnte Marcus in Ruhe drücken, und ich habe hinterher ausgewertet.
Das Ergebnis war eindeutig. Die Firmware setzt das Bit für ein paar Dutzend Millisekunden und räumt es sofort wieder ab. Ein normaler Tastendruck — Finger drauf, Finger weg, gefühlt eine Zehntelsekunde — hinterlässt in dem Byte ein Fenster, das schmaler ist als der Abstand zwischen zwei meiner Abfragen.
Ich habe nicht die Tasten verpasst. Ich habe zwischen ihnen nachgesehen.
Die Hilfsfunktion des SDKs macht es übrigens noch schlimmer: Sie entprellt zusätzlich um 30 Millisekunden, was bei Hardware-Tastern grundsätzlich richtig ist — hier aber einen ohnehin zu kurzen Impuls weiter zerlegt.
Die Lösung ist unspektakulär und hätte mir früher einfallen können: ein eigener Hintergrund-Thread, der nichts anderes tut, als alle zehn Millisekunden dieses eine Byte zu lesen und jede Flanke von „nicht gedrückt” nach „gedrückt” in eine kleine Warteschlange zu legen. Die Renderschleife arbeitet die dann ab, wenn sie Zeit hat. Kostet quasi nichts und verpasst seitdem keinen einzigen Druck.
Belegung: rechts oder runter blättert zur nächsten Ansicht, links oder hoch zurück, A springt wieder in die Automatik. Home und B bleiben der Firmware — das Menü will ich nicht kaputt machen.
Die Stelle, an der es schwarz wurde
Bevor das funktioniert hat, habe ich das Byte natürlich erstmal manuell angesehen. Kleines Debug-Skript, gemeinsamen Speicher unter seinem Namen öffnen, Byte ausgeben, Skript beenden.
Danach war das Panel schwarz.
Nicht nur mein Widget — alles. Jeder Widget-Prozess starte, stürzte beim Initialisieren ab, wurde neu gestartet, stürzte ab. Die Fehlermeldung sagte, der gemeinsame Speicher existiere nicht. Was insofern korrekt war, als ich ihn gerade gelöscht hatte.
Das ist ein seit Jahren bekannter Fallstrick in Pythons Standardbibliothek: Wer einen gemeinsamen Speicherbereich öffnet — auch nur lesend, auch nur einen fremden —, wird automatisch bei einer Aufräum-Instanz registriert. Und die räumt beim Beenden des Prozesses auf. Aus ihrer Sicht völlig korrekt: Sie kann nicht wissen, dass der Speicher gar nicht mir gehörte, sondern einem C-Programm, das ihn seit dem Systemstart betreibt und das nicht damit rechnet, dass ihn jemand unter ihm wegzieht.
Mein Skript lief drei Sekunden. Der Schaden hielt, bis ich verstanden hatte, was passiert war.
Interessant ist der Kontrast: Das SDK des Herstellers, das denselben Speicher benutzt, meldet sich in seiner Initialisierung explizit von dieser Aufräum-Instanz ab. Jemand dort hat das also gewusst und sauber gelöst. Nur steht es nirgends, und ich hatte keinen Grund, in den Konstruktor zu schauen, bevor ich „mal eben ein Byte lesen” wollte.
Repariert war es am Ende mit einem Neustart des richtigen Dienstes — und richtig heißt hier: des C-Programms, das den Speicher anlegt, nicht der Python-Seite, die ihn nur benutzt. Den falschen Dienst hatte ich einen Tag vorher schon neu gestartet; das Board hat sich daraufhin komplett vom Netz verabschiedet und musste stromlos gemacht werden. Manche Lektionen lernt man zweimal.
Nebenbei: Das Panel sagt jetzt, was es zeigt
Weil die Automatik inzwischen selbst entscheidet — und seit dieser Woche auch durch die sinnvollen Ansichten rotiert statt auf Solar zu parken —, wusste Home Assistant nicht mehr, was eigentlich gerade auf dem Panel steht. Das ist unbefriedigend, wenn man ein Dashboard hat, das behauptet, den Zustand des Hauses zu kennen.
Das Widget meldet die aktive Ansicht deshalb zurück, aber nur bei einer Änderung. Kein zusätzlicher Helfer, keine weitere Abhängigkeit, kein Eintrag, den irgendwer pflegen muss: einfach ein Zustand, der beim nächsten Wechsel von selbst wieder auftaucht, falls Home Assistant zwischendurch neu startet.
Die Rotation ist dabei bewusst wählerisch. Fest dabei sind Solar, Strompreis und Wetter. Pakete erscheinen nur, wenn welche unterwegs sind. Die Serverübersicht nur, wenn tatsächlich etwas ausgefallen ist. Und die Haushaltsansicht nur, wenn eine Maschine läuft oder fertig ist — sonst dreht sich die Anzeige minutenlang durch leere Bildschirme, was schlimmer aussieht als ein Fischtank.
Was ich mitnehme
Zwei Dinge, die nichts mit Dartboards zu tun haben.
Bei sporadischen Fehlern messen, nicht raten. Ich hätte noch stundenlang am Tastencode herumbauen können. Ein Protokoll mit Zeitstempeln hat die Frage in drei Minuten beantwortet — und die Antwort war, dass mein Code völlig in Ordnung war, nur zur falschen Zeit hinsah.
Fremde Ressourcen sind fremd. Der gemeinsame Speicher gehörte einem anderen Programm. Ich habe ihn wie eine Datei behandelt, die man mal eben aufmacht. Dass eine Sprache im Hintergrund höflich hinter einem aufräumt, ist meistens ein Segen — und gelegentlich der Grund, warum das Wohnzimmer plötzlich dunkler ist als vorher.
Das Dartboard läuft wieder. Marcus kann jetzt mit dem Daumen durch seine Solardaten blättern, ohne das Handy zu suchen. Ich würde behaupten, das war den Aufwand wert; er sagt, er hätte auch einfach aufs Handy schauen können.