Fixrunde 1, Befund 3: RUN_DIR/mode ueberlebt nur einen "systemctl restart" (RuntimeDirectoryPreserve=restart) - "ninlight-station off" fuehrt aber einen echten "systemctl stop" aus, der das RuntimeDirectory leert. build_state() fiel danach unbemerkt auf "recovery" zurueck, auch wenn tatsaechlich eine Migration anstand - die Fusszeile haette dann "in Ordnung" gezeigt, obwohl der Migrationssatz leer ist. Ein Docstring behauptete ausdruecklich das Gegenteil. _read_mode_file() liest jetzt eine neue, dauerhafte LAST_MODE_FILE (unter /var/lib/ninlight-station, wie der Bildsatz-Umschalter selbst) statt RUN_DIR/mode; write_mode() im CLI schreibt beide. Ausserdem: _read_imageset() faengt jetzt auch UnicodeDecodeError ab (Fixrunde 1, Kleinigkeit). Ein neuer Test bildet den echten Ablauf nach (Modus schreiben, RUN_DIR wie bei einem echten Stop leeren, pruefen dass der Modus trotzdem stimmt) - der alte Test fuer den Rettungs-Bildsatz liess die Modusdatei ganz weg und pruefte damit nur den zufaellig gleichen Rueckfallwert, nicht die Wirklichkeit.
ninlight-Recovery-Station
Ein Raspberry Pi spannt den Zugangspunkt ninlight-recovery auf und liefert unter
http://192.168.8.1/ninlight_<target>_ota.img Firmware-Images aus.
Jede ninlight-Lampe sucht beim Booten bis zu drei Sekunden nach dieser Station. Findet sie sie, lädt sie das Image für ihren Gerätetyp und flasht es — ohne dass die Lampe im WLAN erreichbar sein muss. Das ist der Rettungsweg für Lampen, an die man sonst nur noch mit dem Lötkolben käme.
Trägt das angebotene Image dieselbe Version, die bereits läuft, lehnt die Lampe es ab und bootet normal weiter. Eine angelassene Station flasht also nicht in einer Endlosschleife.
Voraussetzung
Der SSH-Benutzer auf dem Pi braucht passwortloses sudo — beide Skripte und die CLI
(ninlight-station on|off|status) rufen sudo nicht-interaktiv auf; ein Passwort-Prompt
würde dort nur mit einer Fehlermeldung enden.
Bedienung
tools/station/setup_pi.sh <pi-host> einmalig einrichten
tools/station/deploy.sh <pi-host> [target] Images ausliefern
ssh <pi-host> ninlight-station on einschalten
ssh <pi-host> ninlight-station status Zustand und letzte Auslieferungen
ssh <pi-host> ninlight-station bildsatz welcher Migrations-Bildsatz gilt
ssh <pi-host> ninlight-station off ausschalten
Bildsatz: mit oder ohne WLAN-Zugangsdaten
Die WLAN-Zugangsdaten stecken zur Bauzeit in der Firmware (config.local.cmake). Es
gibt deshalb zwei Abzüge je Ziel, und die Station hält beide vor — für beide
Betriebsarten, Migration und Rettung:
| Umschalterstellung | Migration | Rettung | gebaut mit | die Lampe … |
|---|---|---|---|---|
ohne-wlan (Vorgabe) |
images/migrate-nocreds/ |
images/rescue-nocreds/ |
tools/build_nocreds.sh |
startet im eigenen Onboarding-Zugangspunkt und wartet auf ein WLAN |
mit-wlan |
images/migrate-wlan/ |
images/rescue-wlan/ |
build/ |
hängt sofort im WLAN aus config.local.cmake |
Umgeschaltet wird per ninlight-station bildsatz ohne-wlan|mit-wlan oder am Taster
(Menüeintrag „Umschalten: …"). Das ist ein Umschalter, keine Abfrage je Migration/
Rettung: der Zustand steht in /var/lib/ninlight-station/imageset, überlebt Neustarts
und bleibt, bis ihn jemand ändert. Genau deshalb steht er auch dauerhaft in der
Fußzeile des Panels — in jedem Modus, auch bei ausgeschalteter Station: man soll vor
jedem Einschalten sehen, was gilt, statt zwanzig Lampen mit den eigenen Zugangsdaten zu
flashen, weil die Einstellung von vorletzter Woche noch stand.
Warum ohne-wlan die Vorgabe ist (und warum jeder Zweifelsfall — Datei fehlt,
unlesbar, unbekannter Inhalt — dorthin führt): im Automatikmodus wird jede alte
Klyqa-Lampe in Funkreichweite migriert, auch fremde. Mit dem Abzug ohne Zugangsdaten ist
ein Fehlgriff ärgerlich, aber harmlos — die Lampe steht im Onboarding-AP und wartet. Mit
dem Abzug mit Zugangsdaten steht das eigene WLAN-Passwort unwiderruflich in einem fremden
Gerät. Der teurere Fehler darf nie der sein, der ohne Zutun passiert.
Der Umschalter galt bisher nur für die Migration — jetzt auch für die Rettung, und das
hat eine spürbare Folge. Bis zu dieser Erweiterung bekam eine Rettung immer ein
Abbild mit Zugangsdaten (der flache Bestand aus build/). Seitdem gilt für die
Rettung dieselbe Vorgabe ohne-wlan wie für die Migration: eine gerettete Lampe steht
danach im eigenen Onboarding-Zugangspunkt statt im eigenen WLAN, bis jemand ihr eines
nennt. Das ist gewollt (dieselbe Abwägung wie oben — der teure Fehler darf nie der sein,
der ohne Zutun passiert), soll aber niemanden überraschen: die Fußzeile des Panels zeigt
den Bildsatz deshalb ausdrücklich auch im Rettungsmodus, und stellt man bewusst auf
mit-wlan um, gilt das für beide Betriebsarten gemeinsam.
Fehlt der gewählte Satz (nie ausgeliefert, oder tools/build_nocreds.sh nie
gelaufen), wird nichts ausgeliefert — kein stilles Ausweichen auf den anderen Satz,
und auch nicht auf einen alten, flachen Bestand direkt in images/ (siehe unten). Der
Server antwortet mit 404 (Migration zusätzlich mit status: noimageset), und auf dem
Panel steht „SATZ FEHLT" — in JEDEM Modus. „Vorhanden" heißt dabei überall dasselbe:
Verzeichnis da und mindestens ein Image darin.
deploy.sh liefert alle vier Sätze aus (migrate-wlan/migrate-nocreds für die Migration,
rescue-wlan/rescue-nocreds für die Rettung). Fehlt build-nocreds/ oder ist es für ein
Ziel unvollständig, bleibt der jeweils betroffene Satz ohne-wlan auf dem Pi unangetastet
(statt geleert zu werden) und der Lauf meldet, womit er entsteht.
Rückwärtskompatibilität bewusst nicht gegeben: vor dieser Erweiterung lagen
Rettungsabbilder flach in images/ (immer mit Zugangsdaten). Ein Pi mit diesem alten
Bestand muss einmal neu per deploy.sh bestückt werden — der Deploy räumt den
flachen Altbestand dabei automatisch weg. Bis dahin liefert die Rettung in der Vorgabe
ohne-wlan sichtbar nichts aus (statt den alten Bestand mit Zugangsdaten stillschweigend
weiter zu bedienen, was den ganzen Sinn dieser Erweiterung unterlaufen würde).
Taster am Gerät
Sobald die Station läuft, wird wlan0 zum Zugangspunkt und jede SSH-Sitzung reißt ab.
Für genau diesen Zustand gibt es einen Taster auf GPIO23; die Rückmeldung kommt aufs
E-Paper. Ein Taster, zwei Zusammenhänge:
| kurz (unter 1 s) | gehalten | |
|---|---|---|
| Ruhe (Statusbild) | Menü öffnen | 3 s → Abbruch (Station aus), weiter 10 s → Herunterfahren |
| Menü offen | nächster Eintrag | 1,5 s → Eintrag auswählen |
| Rückfrage | abbrechen | 1,5 s → bestätigen |
Der Abbruch feuert bei 3 s, nicht erst beim Loslassen — danach läuft auf dem Panel ein Countdown bis zum Herunterfahren, den Loslassen jederzeit abbricht. Das Menü schließt nach 20 s ohne Eingabe, eine Rückfrage verfällt nach 10 s als Abbruch. Die Einträge hängen vom Zustand ab (Station aus / Rettung läuft / Migration läuft); jede verändernde Auswahl bekommt eine Rückfrage.
Die beiden Migrationsarten sind getrennte Einträge. „Migration: horchen"
(migrate on --listen) steht zuerst und ist der naheliegende: der Zugangspunkt steht,
aber es geht nichts raus, bis du eine erkannte Lampe freigibst. „Migration: AUTOMATIK"
(migrate on) folgt danach und bekommt eine andere, deutlichere Rückfrage — der
Migrations-Zugangspunkt hat ein öffentlich bekanntes Passwort (es steht in der alten
Klyqa-Firmware), und im Automatikmodus wird jede alte Klyqa-Lampe in Funkreichweite
ungefragt migriert, auch fremde. Die Rückfrage benennt genau das, statt nur „Wirklich?"
zu fragen.
Der Taster ist nirgends Voraussetzung: fehlt gpiozero oder der Taster, protokolliert der
Dienst eine Zeile und beendet sich ohne Fehler. Er fasst den SPI-Bus nicht an (zwei
Prozesse auf denselben Leitungen ergeben GPIO busy), sondern schreibt seinen
Bedienzustand nach /run/ninlight-button/ui.json; der Anzeigedienst liest ihn und
zeichnet. Ausgeführt wird ausschließlich über das CLI.
Dienst und Unit liegen in tools/station/button/ bzw.
tools/station/ninlight-button.service. setup_pi.sh installiert, aktiviert und startet
den Tasterdienst wie den Anzeigedienst; deploy.sh liefert button/ bei jedem Deploy mit
aus. Ein fehlgeschlagenes Aktivieren/Starten ist dabei nur eine Warnung — eine Station
ohne Taster muss vollständig benutzbar bleiben.
Abschlussbildschirm beim Herunterfahren und beim Neustart
E-Paper behält sein Bild dauerhaft, auch stromlos. Fährt der Pi herunter, wird der Anzeigedienst mit abgeräumt, und das zuletzt gezeichnete Bild bleibt für immer stehen — bei der Abnahme stand dort die Rückfrage des Tastermenüs, und der ausgeschaltete Pi sah aus, als warte er noch auf eine Eingabe.
Dafür gibt es zwei systemd-Units, ninlight-poweroff.service (WantedBy=poweroff.target)
und ninlight-reboot.service (WantedBy=reboot.target). Sie hängen am System, nicht
am Tasterdienst: so greifen sie bei jedem Herunterfahren — über das Menü, über poweroff
per SSH, über reboot — und nicht nur auf einem von drei Wegen.
Die Texte unterscheiden sich, und das ist der Punkt:
| Panel | |
|---|---|
| Herunterfahren | „AUSGESCHALTET — Strom kann getrennt werden" |
| Neustart | „NEUSTART — Strom NICHT trennen!" plus der Hinweis, dass es rund 2 min dauert |
Ein Zero W braucht gut anderthalb Minuten, bis der Anzeigedienst wieder zeichnet. Stünde dort währenddessen „Strom kann getrennt werden", wäre das eine Aufforderung zu genau dem Griff, der an diesem Pi schon eine SD-Karte gekostet hat.
Reihenfolge: der Anzeigedienst hält die GPIO-/SPI-Leitungen; zwei Prozesse darauf
ergeben lgpio.error: 'GPIO busy'. Die Units ordnen sich deshalb per
After=ninlight-display.service hinter dessen Stop ein, und ninlight-station display final <poweroff|reboot> wartet zusätzlich, bis systemctl is-active ninlight-display
nicht mehr anschlägt — dasselbe Muster wie display_handover() bei display test, nur
ohne Wiederanfahren. Gibt der Dienst die Leitungen nicht frei, wird nicht gezeichnet,
sondern eine Zeile ins Journal geschrieben: ein Fehler an der Anzeige darf das
Herunterfahren nie aufhalten.
SSID und Passwort stehen in /etc/ninlight-station.conf auf dem Pi und müssen zu den
Build-Parametern NINLIGHT_RECOVERY_SSID und NINLIGHT_RECOVERY_PASSWORD aus
config.local.cmake passen. Weicht eines ab, findet die Lampe die Station nicht.
Vertrauensgrenze
Der Trailer eines Images prüft Gerätetyp und SHA256. Er ist keine kryptografische
Signatur, und der Download läuft über http:// ohne TLS. Die Station schützt allein ein
WPA2-Passwort, dessen Default im Repo steht. Wer in Funkreichweite ist und es kennt, kann
jeder Lampe beim nächsten Boot beliebige Firmware unterschieben.
Das ist gewollt: Die Signaturprüfung der alten Firmware auszuhebeln ist der Zweck dieses Projekts. Daraus folgen zwei Regeln.
Setz ein eigenes Passwort. In config.local.cmake und in
/etc/ninlight-station.conf — an beiden Stellen denselben Wert.
Lass die Station nur an, solange du sie brauchst. Nicht nur wegen des Fremdzugriffs: Die Firmware löscht den Power-Cycle-Zähler, bevor sie einen Recovery-Versuch startet. Solange eine Station in Reichweite ist, funktioniert der Werksreset per fünf Stromzyklen bei keiner Lampe in Reichweite mehr.