From 682235f3ab08b7a9d29b2bd1a0b717cd1fca3933 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Sat, 12 Sep 2026 02:45:27 +0200 Subject: [PATCH] docs: OTA und Recovery dokumentieren --- README.md | 60 ++++++++++++++++++++++++++++++++++++++ bruno/ninlight.yml | 9 +++++- docs/hardware-checklist.md | 11 +++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a1e698..11874ef 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,66 @@ Verfügbare Befehle in der Konsole: `help`. - Bruno: `bruno/ninlight.yml` importieren, Environment „EVK (mDNS)" bzw. „Onboarding-AP" wählen. - Konsole: `net` zeigt Modus, SSID, IP, RSSI und den letzten Onboarding-Fehler. +## Firmware-Update + +Der Build erzeugt pro Gerät zwei Images: + +- **`_gz.img`** — komprimiertes App-Image für das serielle Flashen mit `tools/flash.sh` (siehe oben). +- **`_ota.img`** — dasselbe `_gz.img`, um einen 512-Byte-Trailer ergänzt (`tools/mkimage/nl_mkimage.py`, + automatisch bei jedem Build via `cmake/run_mkimage.cmake`). Der Trailer enthält Gerätetyp, Firmware-Version + und den SHA256 über das komplette `_gz.img` — das ist genau der Hash, den die Firmware beim Schreiben ins + Flash prüft. + +Netz-Update über die REST-API: + + curl -X PUT http://ninlight-681f.local:3333/api/v1/system/command \ + -H 'Authorization: aabbccddeeff0011223344' \ + -d '{"command":{"type":"fw_update","url":"http://:8000/pfad/zur/ninlight_evk_ota.img"}}' + +Antwort `202 {"status":"updating"}` startet den Download im Hintergrund; ein per HTTP-Server bereitgestelltes +`_ota.img` reicht (z.B. `python3 -m http.server 8000`). Läuft der Trailer- bzw. Hash-Check durch, resettet die +Lampe und bootet mit der neuen Version. Wird das Image abgelehnt, gibt es **keinen** Reset — die Lampe läuft +mit der alten Firmware weiter und trägt den Grund in `last_ota_error` (`GET system/info`) ein. Ein +Verbindungsabbruch mitten im Download löst dagegen einen Wiederaufnahmeversuch per HTTP-Range aus (begrenzte +Anzahl Runden); erst wenn auch das ausgeht, landet der Versuch bei `last_ota_error = download`. + +Werte von `last_ota_error` (`null`, solange nichts fehlgeschlagen ist): + +| Wert | Bedeutung | +|---|---| +| `download` | Verbindung/Transfer gescheitert (auch nach ausgeschöpftem Wiederaufnahmeversuch) | +| `crc` | Prüfsumme des SDK-eigenen Update-Mechanismus stimmt nicht | +| `trailer` | Trailer fehlt, ist zu kurz oder hat eine falsche Magic/Version | +| `device_mismatch` | Trailer nennt einen anderen Gerätetyp | +| `hash` | SHA256 im Trailer stimmt nicht mit dem übertragenen Image überein | +| `busy` | es läuft bereits ein Update, oder die übergebene URL war ungültig | + +Ein erneuter `fw_update`-Aufruf mit einem gültigen Image funktioniert nach einer Ablehnung sofort, ohne +manuellen Reboot. + +## Recovery + +Findet die Firmware beim Booten keinen Weg, sich selbst zu aktualisieren (z.B. weil das reguläre WLAN nicht +erreichbar ist), sucht sie kurz nach einem Rettungs-Zugangspunkt: SSID/Passwort sind Build-Parameter +(`NINLIGHT_RECOVERY_SSID`/`NINLIGHT_RECOVERY_PASSWORD` in `config.local.cmake`, Default für beide +`ninlight-recovery`). Wird die Station gefunden, verbindet sich die Lampe und lädt +`http://192.168.8.1/ninlight__ota.img` (z.B. `ninlight_evk_ota.img`) — der Dateiname enthält den +Gerätetyp, `` ist der Wert von `NINLIGHT_DEVICE` aus dem Build. + +Bei jedem Boot scannt die Firmware dafür bis zu ~3 Sekunden nach der Station und loggt das Ergebnis +(gefunden, nichts gefunden, oder Scan fehlgeschlagen). Ist keine Station in Reichweite, verlängert sich der +Boot nur um diese Scan-Zeit; danach startet der normale WLAN-Aufbau wie gewohnt. Über dem Download selbst +liegt eine harte Zeitobergrenze — läuft sie ab, wird der Watchdog nicht mehr bedient und der Chip startet neu, +statt unbeobachtet hängen zu bleiben; der nächste Boot versucht Recovery erneut. + +**Vertrauensgrenze:** Der Trailer prüft Gerätetyp und SHA256 — das ist kein kryptografisches Signaturverfahren. +Der Download läuft über `http://` ohne TLS, und der Recovery-Kanal ist ausschließlich durch das oben genannte +WLAN-Passwort geschützt, das mit einem öffentlichen Default im Repo steht. Wer im selben WLAN ist und das +API-Token kennt (bzw. beim Recovery in Funkreichweite ist und das Passwort kennt), kann der Lampe beliebige +Firmware unterschieben. Das ist eine bewusste Entscheidung dieses Projekts: Ziel ist, die Signaturprüfung der +alten, abgekündigten Original-Firmware zu umgehen, damit die Lampen überhaupt weiter nutzbar bleiben — nicht, +ein neues, gehärtetes Sicherheitsmodell zu bauen. + ## Werksreset Gerät 5× hintereinander einschalten, jeweils kürzer als 5 Sekunden. diff --git a/bruno/ninlight.yml b/bruno/ninlight.yml index ad9fa1d..1ffd180 100644 --- a/bruno/ninlight.yml +++ b/bruno/ninlight.yml @@ -11,6 +11,8 @@ config: value: '3333' - name: device_token value: aabbccddeeff0011223344 + - name: ota_url + value: http://192.168.1.12:8000/ninlight_evk_ota.img - name: Onboarding-AP variables: - name: device_address @@ -19,6 +21,8 @@ config: value: '3333' - name: device_token value: aabbccddeeff0011223344 + - name: ota_url + value: http://192.168.1.12:8000/ninlight_evk_ota.img items: - info: name: Device @@ -205,6 +209,9 @@ items: timeout: 0 followRedirects: true maxRedirects: 5 + docs: |- + Enthält `last_ota_error`: `null`, solange kein Update fehlgeschlagen ist, sonst einer von + `download`, `crc`, `trailer`, `device_mismatch`, `hash`, `busy`. Kein `reset_reason` mehr im Feld-Satz. - info: name: Get System Settings type: http @@ -372,7 +379,7 @@ items: { "command": { "type": "fw_update", - "url": "http://192.168.1.12:8000/ninlight_evk_ota.img" + "url": "{{ota_url}}" } } settings: diff --git a/docs/hardware-checklist.md b/docs/hardware-checklist.md index 4a031b9..a57ac82 100644 --- a/docs/hardware-checklist.md +++ b/docs/hardware-checklist.md @@ -38,6 +38,17 @@ Konsole: `tools/monitor.sh /dev/cu.` (115200 8N1). Flashen: `tools/flash.s - [x] C1: `curl -d '[[[[[[[[[[[[[[[[[[[[' http://:3333/api/v1/device/state` → 400, Lampe läuft unverändert weiter (kein Absturz/Reboot) — 2026-09-12 - [x] C2: Onboarding ok, 60 s warten, dann Reboot und Router-Neustart → AP `ninlight-` darf nicht wiederkommen (Scan am Mac) — 2026-09-12 (Reboot geprüft, Router-Neustart offen) +## OTA und Recovery (Plan 3) +- [ ] OTA: Update über `fw_update` läuft durch, danach meldet `system/info` die neue Version +- [ ] OTA: Image eines anderen Gerätetyps wird sofort abgelehnt (`last_ota_error` = `device_mismatch`), kein Reset, Lampe läuft mit der alten Firmware weiter +- [ ] OTA: Image mit verfälschtem Byte wird abgelehnt (`last_ota_error` = `hash` oder `crc`), kein Reset +- [ ] OTA: HTTP-Server während des Downloads abschalten → Wiederaufnahmeversuch (Range-Resume) sichtbar in der Konsole, nach Ausschöpfen der Runden `last_ota_error` = `download`, Lampe bootet normal +- [ ] OTA: kurzer WLAN-Aussetzer während des Downloads (Verbindung kurz kappen und wiederherstellen) → Download wird fortgesetzt, kein sofortiger `download`-Fehler +- [ ] Recovery: Lampe zieht beim Booten das Image von der Station `ninlight-recovery`, Konsole zeigt Scan-, Join- und Fetch-Meldungen +- [ ] Recovery: ohne Station verlängert sich der Boot um höchstens ~3 s, Konsole loggt das Scan-Ergebnis (gefunden/nichts gefunden/Scan fehlgeschlagen) +- [ ] Recovery: Download hängt/dauert sehr lange → nach Ablauf der Zeitobergrenze bedient die Firmware den Watchdog nicht mehr, Chip startet neu statt hängen zu bleiben +- [ ] Einstellungen bleiben über ein OTA hinweg erhalten (WLAN, Name, Fade-Zeiten) + ## Später (Lampen) - [ ] SM2135EH: Standby-Bit bei allen Kanälen 0 prüfen (Spec §4; alte Firmware setzte es praktisch nie) - [ ] E27 Color (SM2135EH): RGB und WY getrennt, Ströme plausibel (nicht zu heiß)