sdk/PATCHES.md, README.md und docs/hardware-checklist.md beschrieben teils einen ueberholten Stand: die Watchdog-Einschaetzung in PATCHES.md Punkt 4 stammte noch von vor dem Drei-Heartbeat-Gate in app_main.c und vor der Zeitobergrenze in nl_recovery.c; README behauptete eine harte Obergrenze ueber dem gesamten Download statt nur ueber einem echten Stillstand; der Boot-Scan ist aktiv (~3 s), nicht passiv wie an einer Stelle der Spec beschrieben (Spec bleibt unveraendert). PATCHES.md Punkt 1 ergaenzt den sys/nl_ota.h-Include in wm_fwup.c, Punkt 5 den fls_lock-printf. hardware-checklist.md: Recovery-Zeile auf "Gegenstelle sendet nichts mehr" praezisiert; die beiden Werksreset-Haken aus Plan 1 zurueckgesetzt, weil der vorgelagerte Recovery-Scan das Boot-Timing verschiebt und sie neu geprueft werden muessen; acht neue Pruefpunkte fuer Plan 3 ergaenzt (Zweitversuch nach Ablehnung, REST-Erreichbarkeit waehrend/nach Ablehnung, Lichtverhalten ueber OTA mit pob=off/on, fw_version gegen Trailer, echter Rettungsfall, Rettungsstation nach Erfolg angelassen, AP-Modus + auth_all) sowie ein Betriebshinweis zu NINLIGHT_RECOVERY_PASSWORD/NINLIGHT_TOKEN fuer den Flottenbau.
148 lines
8.1 KiB
Markdown
148 lines
8.1 KiB
Markdown
# ninlight
|
||
|
||
Lokale Firmware für die alten Klyqa-Lampen mit WinnerMicro W600 (E27/E14/GU10 RGB+CW, E27/E14/GU10 CW, G95 Vintage) und das W600-EVK.
|
||
|
||
## Voraussetzungen
|
||
|
||
- **Docker Desktop** (primärer Build-Pfad, erste Ausführung baut das Image `ninlight-build`)
|
||
- **CMake ≥ 3.20**, Ninja, host C compiler (für Host-Tests)
|
||
- **uv** (für die serielle Konsole)
|
||
- ARM GNU Toolchain mit newlib: `brew install --cask gcc-arm-embedded` (nur Fallback für nativen Build)
|
||
|
||
## Bauen
|
||
|
||
Docker (empfohlen – kein lokaler ARM-Compiler nötig):
|
||
|
||
tools/docker.sh cmake -S . -B build/evk -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake -DNINLIGHT_DEVICE=evk
|
||
tools/docker.sh cmake --build build/evk
|
||
tools/docker.sh tools/build_all.sh # alle 8 Geräte
|
||
|
||
Für die tägliche Entwicklung: nur das Gerät bauen, das du verwendest (z.B. `tools/docker.sh cmake --build build/evk`). Der emulierte Docker-Build ist langsam — `build_all.sh` ist nur für Releases gedacht.
|
||
|
||
Native Builds (mit lokal installierter ARM-Toolchain):
|
||
|
||
cmake -S . -B build/evk -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake -DNINLIGHT_DEVICE=evk
|
||
cmake --build build/evk
|
||
|
||
**Geräte:** evk, e27_rgb, e14_rgb, gu10_rgb, e27_cw, e14_cw, gu10_cw, g95_cw.
|
||
|
||
**Lokale Einstellungen** (Token, später WLAN): `config.local.cmake.example` → `config.local.cmake`.
|
||
|
||
## Tests
|
||
|
||
Host-basierte Unit-Tests (laufen nativ auf dem Mac):
|
||
|
||
tools/test_host.sh
|
||
|
||
## Flashen und Konsole
|
||
|
||
Verfügbare Ports anzeigen:
|
||
|
||
build/host-tools/wm_tool -l
|
||
|
||
Firmware flashen (Standard: App-Image):
|
||
|
||
tools/flash.sh <device> <port>
|
||
|
||
Das Skript druckt `Bitte bei "please manually reset the device" die Reset-Taste drücken.` — wenn wm_tool diese Nachricht zeigt, musst du die Reset-Taste am EVK drücken (Baud 460800). Für blank/erased Chips: `tools/flash.sh <device> <port> full` (flasht stattdessen das Vollbild mit secboot, auf Hardware noch ungetestet).
|
||
|
||
Serielle Konsole (115200 8N1, lokales Echo):
|
||
|
||
tools/monitor.sh /dev/cu.<port>
|
||
|
||
Verfügbare Befehle in der Konsole: `help`.
|
||
|
||
## Netzwerk & REST
|
||
|
||
- WLAN fest einbauen: `NINLIGHT_WIFI_SSID` / `NINLIGHT_WIFI_PASSWORD` in `config.local.cmake` (gespeicherte Einstellungen haben Vorrang).
|
||
- Ohne WLAN-Daten (oder nach Werksreset) öffnet die Lampe den AP `ninlight-<mac4>` (Passwort `NINLIGHT_AP_PASSWORD`, Default `ninlight`) und pulsiert warmweiß. Onboarding:
|
||
|
||
curl -X PUT http://192.168.4.1:3333/api/v1/system/settings \
|
||
-d '{"type":"wifi","ssid":"MeinWLAN","password":"geheim123"}'
|
||
curl http://192.168.4.1:3333/api/v1/system/info # wifi.sta_ip / wifi.error
|
||
|
||
Der Aufruf ohne `Authorization`-Header funktioniert nur im unkonfigurierten AP (noch keine
|
||
gespeicherten Zugangsdaten). Im Fallback-AP (Router war > 5 min weg, Lampe hat aber bereits
|
||
Zugangsdaten) ist der Token-Header wie bei jedem anderen Endpunkt erforderlich.
|
||
Nach Erfolg schließt der AP nach 60 s. Ohne Verbindung für 5 min öffnet die Lampe den AP zusätzlich (Fallback).
|
||
- REST auf Port 3333, Präfix `/api/v1`: `GET|POST device/state`, `GET system/info`, `GET|PUT system/settings` und `PUT system/command` (beide mit Header `Authorization: <token>`, Default-Token `aabbccddeeff0011223344`).
|
||
- Beispiel: `curl -X POST http://ninlight-681f.local:3333/api/v1/device/state -d '{"on":true,"brightness":40,"color_temp":2700}'`
|
||
- Suchen: `dns-sd -B _ninlight._tcp` (macOS), Details `dns-sd -L ninlight-681f _ninlight._tcp`.
|
||
- 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://<host-ip>: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`). Eine ungültige URL (kein `http://`-Präfix, oder zu
|
||
kurz/lang) beantwortet die API sofort mit `400`, ohne dass ein Download versucht wird. Läuft bereits ein
|
||
Update, oder scheitert der Start aus einem anderen Grund, antwortet sie mit `409` und dem Grund im Body
|
||
(gleicher Wert wie `last_ota_error`). 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 |
|
||
|
||
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_<device>_ota.img` (z.B. `ninlight_evk_ota.img`) — der Dateiname enthält den
|
||
Gerätetyp, `<device>` ist der Wert von `NINLIGHT_DEVICE` aus dem Build.
|
||
|
||
Bei jedem Boot scannt die Firmware dafür bis zu ~3 Sekunden aktiv (`tls_wifi_scan()`, kein passiver
|
||
Scan) 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 keine harte Zeitobergrenze — ein
|
||
langsamer, aber weiter laufender Transfer füttert den Watchdog über den SDK-eigenen Update-Task mit
|
||
und darf beliebig lange dauern. Erst ein echter Stillstand (die Gegenstelle bleibt verbunden, sendet
|
||
aber keine Daten mehr, oder eine interne Allokations-Warteschleife hängt) lässt den Watchdog nach
|
||
10 Minuten auslaufen; der Chip startet dann neu, statt unbeobachtet zu hängen, und 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.
|
||
|
||
## Hardwaretests
|
||
|
||
Manuelle Checkliste: `docs/hardware-checklist.md`.
|