Files
ninlight-w600/tools/station/README.md
Michael Sauer 4e1a965cbb feat(station): Abschlussbild beim Herunterfahren und beim Neustart
E-Paper behaelt sein Bild dauerhaft, auch stromlos. Faehrt der Pi
herunter, wird der Anzeigedienst mit abgeraeumt und das zuletzt
gezeichnete Bild bleibt fuer immer stehen - bei der Abnahme stand dort
die Rueckfrage des Tastermenues, und der ausgeschaltete Pi sah aus, als
warte er noch auf eine Eingabe.

Zwei neue Units, ninlight-poweroff.service (WantedBy=poweroff.target)
und ninlight-reboot.service (WantedBy=reboot.target). Sie haengen am
System, nicht am Tasterdienst: so greifen sie bei jedem Herunterfahren -
Menue, "poweroff" per SSH, "reboot" - und nicht nur auf einem von drei
Wegen. Zwei Units statt einer Fallunterscheidung, weil systemd einem
ExecStart nicht sagt, wohin die Reise geht.

Die Texte unterscheiden sich: "AUSGESCHALTET / Strom kann getrennt
werden" gegen "NEUSTART / Strom NICHT trennen!" samt Hinweis auf die
rund zwei Minuten, die der Zero W bis zum Anzeigedienst braucht. An
diesem Pi hat ein Stromzug in genau dieser Wartezeit schon eine
SD-Karte gekostet.

Reihenfolge: die Units ordnen sich per After=ninlight-display.service
hinter dessen Stop ein, und "ninlight-station display final <kind>"
wartet zusaetzlich, bis systemctl is-active nicht mehr anschlaegt -
dasselbe Muster wie display_handover(), nur ohne Wiederanfahren. Gibt
der Dienst die Leitungen nicht frei, wird nicht gezeichnet (GPIO busy),
sondern gewarnt: ein Anzeigefehler haelt das Herunterfahren nie auf.

Nebenbei behoben: display_run() reichte nur "$1" an display.py weiter
und haette "--final poweroff" halbiert.
2026-09-15 03:02:21 +02:00

155 lines
8.9 KiB
Markdown

# 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:
| Umschalterstellung | Verzeichnis auf dem Pi | gebaut mit | die migrierte Lampe … |
|---|---|---|---|
| `ohne-wlan` (**Vorgabe**) | `images/migrate-nocreds/` | `tools/build_nocreds.sh` | startet im eigenen Onboarding-Zugangspunkt und wartet auf ein WLAN |
| `mit-wlan` | `images/migrate-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**: 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.
**Fehlt der gewählte Satz** (nie ausgeliefert, oder `tools/build_nocreds.sh` nie gelaufen),
wird **nichts** ausgeliefert — kein stilles Ausweichen auf den anderen Satz. Der Server
antwortet mit 404 und `status: noimageset`, `ninlight-station status` sagt „FEHLT", und auf
dem Panel steht „SATZ FEHLT". „Vorhanden" heißt dabei überall dasselbe: Verzeichnis da
**und** mindestens ein Image darin.
`deploy.sh` liefert beide Sätze aus. Fehlt `build-nocreds/`, bleibt der Satz `ohne-wlan`
auf dem Pi unangetastet (statt geleert zu werden) und der Lauf meldet, womit er entsteht.
## 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.