Files
ninlight-w600/tools/station
Michael Sauer 1cb5359754 fix(station): Manifest-Merge robust gegen Fehlform, Version pro Eintrag
Fixrunde 2 zu "Migrationsimages beim Deploy signieren":

- N1 (Important): das Merge-Skript im SSH-Heredoc von deploy.sh fing beim
  Bestandsmanifest nur Lesefehler und kaputtes JSON ab (OSError/ValueError).
  War die Datei gueltiges JSON mit falscher Form ("[]", "null", ein String,
  oder ein images-Eintrag, der selbst kein Objekt ist), krachte .get() mit
  AttributeError - und das VOR der Installation von
  server.py/products.py/CLI/Unit und vor dem Neustart, liess einen Pi also
  mit neuen Images, aber altem Stationscode zurueck. Jetzt wird alte wie
  neue Manifest-Struktur konsequent auf isinstance(dict)/isinstance(list)
  geprueft, jede Fehlform faellt auf ein leeres Manifest zurueck statt
  abzustuerzen. Neues Szenario 21 deckt alle vier Faelle aus der
  Review-Tabelle ab (kaputt, leer, falsch geformtes JSON, kaputter Eintrag).

- N2 (Important): das zusammengefuehrte Manifest hat "ninlight_version"
  bisher blind vom neuen Manifest uebernommen, obwohl uebernommene
  Alt-Eintraege (deren Images unveraendert liegen bleiben, siehe Fixrunde 1
  I1) eine andere Firmware-Version tragen koennen - eine konkrete falsche
  Version beim Protokollieren ist schlechter als das vorherige "unbekannt".
  Die Version wandert jetzt PRO EINTRAG ins Manifest (deploy.sh erzeugt sie
  dort, der Merge behaelt sie); das manifestweite Feld bleibt nur als
  Bestandskompatibilitaet und wird nur gesetzt, wenn wirklich alle Eintraege
  uebereinstimmen. server.py liest beim Ausliefern jetzt die Version DES
  AUSGELIEFERTEN EINTRAGS (Station.image_version(target, variant) ersetzt
  manifest_version()) und faellt nur auf das manifestweite Feld zurueck,
  wenn der Eintrag selbst keine eigene traegt. test_server.py bekommt einen
  neuen Test fuer den Vorrang des Eintrags vor dem manifestweiten Feld,
  Szenario 14 in test_deploy.sh prueft jetzt zusaetzlich unterschiedliche
  Versionen zwischen zusammengefuehrten Eintraegen und dass das
  manifestweite Feld dann fehlt statt falsch zu sein. Die Spec
  (docs/superpowers/specs/.../design.md, Abschnitt 4, lokal/nicht
  versioniert) ist nachgezogen. Die dadurch ueberfluessig gewordene
  "sorted(versions)[0]"-Stelle aus Fixrunde 1 ist entfallen.

- Kleinigkeit: die Remote-Meldung, wenn migrate/ von diesem Rechner leer
  ist, behauptet nicht mehr "kein Schluesselverzeichnis" (das stimmt im
  Fall "Verzeichnis vorhanden, aber leer" nicht) - sie nennt jetzt keinen
  bestimmten Grund mehr, nur die Konsequenz.
- Kleinigkeit: --exclude=manifest.json ist jetzt --exclude=/manifest.json,
  konsistent mit dem daneben verankerten --exclude=/migrate.

Alle drei Aenderungen (N1, N2, beide Kleinigkeiten) sind vor dem Fix per
Rot-Probe an einer Kopie von deploy.sh verifiziert: mit den urspruenglichen
Fehlern schlagen Szenario 21 (N1) bzw. Szenario 14 (N2) zuverlaessig fehl.

Bewusst nicht angefasst (Review-Vorgabe): der fehlende HOME-Testfall, und
dass der Merge kein os.path.exists() prueft.
2026-09-13 20:40:03 +02:00
..

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 off         ausschalten

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.