Compare commits
24
Commits
7fd279721f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0379445e8 | ||
|
|
58cdc73afc | ||
|
|
c53a23daba | ||
|
|
6fe5efd2c0 | ||
|
|
b198fead77 | ||
|
|
71cede19a4 | ||
|
|
ba8f2a10a0 | ||
|
|
bc3eff1a41 | ||
|
|
08ea01f944 | ||
|
|
ef321b37ef | ||
|
|
813d861cd1 | ||
|
|
a022d8ae3e | ||
|
|
20c26d16d7 | ||
|
|
fda2552e50 | ||
|
|
ded09a3516 | ||
|
|
24c4e38e4e | ||
|
|
18eb7b5c01 | ||
|
|
e36add5030 | ||
|
|
416307083f | ||
|
|
8469b92338 | ||
|
|
906a6d55f2 | ||
|
|
1c29735eaf | ||
|
|
d4b683de8f | ||
|
|
543123a60d |
@@ -44,7 +44,7 @@ Firmware flashen (Standard: App-Image):
|
|||||||
|
|
||||||
tools/flash.sh <device> <port>
|
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).
|
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). Das Skript ruft `wm_tool` mit `-rs none` auf, es löst also **keinen** Reset aus: Der Reset muss von Hand in dem Zeitfenster kommen, in dem `wm_tool` auf den Sync wartet. Ein automatischer Reset über die Steuerleitungen (`-rs rts` o. ä.) funktioniert an diesem Board nicht. 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):
|
Serielle Konsole (115200 8N1, lokales Echo):
|
||||||
|
|
||||||
@@ -65,7 +65,16 @@ Verfügbare Befehle in der Konsole: `help`.
|
|||||||
gespeicherten Zugangsdaten). Im Fallback-AP (Router war > 5 min weg, Lampe hat aber bereits
|
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.
|
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).
|
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` (kennt u.a. den Typ `sacn`, siehe unten) und `PUT system/command` (beide mit Header `Authorization: <token>`, Default-Token `aabbccddeeff0011223344`).
|
|
||||||
|
**Alternative beim Einrichten:** Statt ein WLAN anzugeben, kann man die Lampe im selben Schritt
|
||||||
|
dauerhaft auf ihren eigenen Zugangspunkt festlegen — dann braucht sie überhaupt keinen Router.
|
||||||
|
Im unkonfigurierten AP geht das wie das WLAN-Onboarding ohne Token:
|
||||||
|
|
||||||
|
curl -X PUT http://192.168.4.1:3333/api/v1/system/settings \
|
||||||
|
-d '{"type":"standalone","enabled":true,"password":"geheim123"}'
|
||||||
|
|
||||||
|
Einzelheiten unter [Standalone-AP](#standalone-ap).
|
||||||
|
- REST auf Port 3333, Präfix `/api/v1`: `GET|POST device/state`, `GET system/info`, `GET|PUT system/settings` (kennt u.a. die Typen `sacn` und `standalone`, siehe unten) 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}'`
|
- 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`.
|
- 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.
|
- Bruno: `bruno/ninlight.yml` importieren, Environment „EVK (mDNS)" bzw. „Onboarding-AP" wählen.
|
||||||
@@ -107,6 +116,56 @@ speichern. Zum Abschalten: `{"type":"sacn","enabled":false}`.
|
|||||||
|
|
||||||
Zum Ausprobieren liegt ein Sender bei: `tools/sacn_send.py --values 255,255,0,0,0,0 --seconds 5`.
|
Zum Ausprobieren liegt ein Sender bei: `tools/sacn_send.py --values 255,255,0,0,0,0 --seconds 5`.
|
||||||
|
|
||||||
|
### Standalone-AP
|
||||||
|
|
||||||
|
Statt sich in ein WLAN einzuwählen, kann die Lampe dauerhaft einen eigenen Zugangspunkt aufspannen
|
||||||
|
(z. B. für Bühne oder Gartenhaus ohne Router). Der Modus überlebt Neustarts: die Lampe startet
|
||||||
|
direkt im eigenen AP, ohne Onboarding und ohne Onboarding-Pulsen. Die Lampe ist dann unter
|
||||||
|
`192.168.4.1` erreichbar, per mDNS als `<hostname>.local`.
|
||||||
|
|
||||||
|
Einschalten:
|
||||||
|
|
||||||
|
curl -X PUT http://<lampe>:3333/api/v1/system/settings \
|
||||||
|
-H 'Authorization: <token>' \
|
||||||
|
-d '{"type":"standalone","enabled":true,"ssid":"Buehne","password":"geheim123","channel":6}'
|
||||||
|
|
||||||
|
Antwort `202 {"status":"standalone"}`. Der Aufruf braucht den Token, sobald die Lampe konfiguriert
|
||||||
|
ist; im unkonfigurierten Onboarding-AP geht er ohne. Anders als dieser ist der Standalone-AP
|
||||||
|
anschließend **nicht** token-frei: der Tokenschutz gilt dort wie im Normalbetrieb. Am Gerät
|
||||||
|
gemessen (2026-10-02, EVK und eine E27 RGB): `GET system/settings` ohne Token → 401 im
|
||||||
|
Standalone-AP, 200 im unkonfigurierten Onboarding-AP. Felder:
|
||||||
|
|
||||||
|
| Feld | Pflicht | Bedeutung |
|
||||||
|
|---|---|---|
|
||||||
|
| `enabled` | ja | `true` schaltet den Modus ein, `false` aus |
|
||||||
|
| `password` | beim Einschalten, solange noch keines gespeichert ist | AP-Passwort, 8..63 Zeichen |
|
||||||
|
| `ssid` | nein | AP-Name, 1..32 Zeichen; ein leerer String wird mit 400 `invalid ssid` abgelehnt. Weggelassen bleibt der gespeicherte Name erhalten; `ninlight-<mac4>` gilt nur, solange nie ein eigener gesetzt wurde — über die API führt kein Weg zurück zum Vorgabenamen |
|
||||||
|
| `channel` | nein | Funkkanal 1..13, Vorgabe 6 |
|
||||||
|
| `pulse` | nein | Startpuls beim Hochfahren der Lampe an/aus, Vorgabe an (läuft rund 5 s, danach gilt das normale Einschaltverhalten) |
|
||||||
|
|
||||||
|
Ausschalten braucht kein Passwort: `{"type":"standalone","enabled":false}`. Hat die Lampe gespeicherte
|
||||||
|
WLAN-Daten, wählt sie sich damit wieder ins WLAN ein; ohne solche öffnet sie den unkonfigurierten
|
||||||
|
Onboarding-AP `ninlight-<mac4>`.
|
||||||
|
|
||||||
|
`GET system/settings` zeigt einen `standalone`-Block mit `enabled`, dem **gewünschten** AP-Namen
|
||||||
|
(`ssid`: der gespeicherte Name, ohne eigenen Namen der Hostname), `channel` und `pulse` — das
|
||||||
|
Passwort steht nie darin. `GET system/info` meldet `wifi.standalone` und im AP-Betrieb (`mode: "ap"`,
|
||||||
|
also eigener AP oder Onboarding-AP) unter `wifi.ssid` den **laufenden AP-Namen** statt der
|
||||||
|
gespeicherten Stations-SSID — der Name des Heimnetzes steht damit nicht in einer tokenfrei lesbaren
|
||||||
|
Antwort.
|
||||||
|
|
||||||
|
Die beiden Namen können auseinanderlaufen, und das ist Absicht. Scheitert der AP-Aufbau mehrfach,
|
||||||
|
fällt der Treiber auf die zuletzt funktionierenden Daten zurück, und gibt es die nicht, auf Name und
|
||||||
|
Kanal der Vorgaben (das Nutzerpasswort bleibt); die gespeicherte Einstellung behält dabei den
|
||||||
|
Wunschnamen. Nach `{"enabled":false}` behält sie `ap_ssid` ebenfalls, der laufende AP heißt dann
|
||||||
|
wieder `ninlight-<mac4>`. Genau diese Abweichung zwischen `system/settings` (gewünscht) und
|
||||||
|
`system/info` (läuft) ist das Erkennungsmerkmal für einen Rückfall.
|
||||||
|
|
||||||
|
Der Startpuls übermalt als Überlagerung die sACN-Ausgabe: solange er läuft, ist von sACN nichts
|
||||||
|
zu sehen. Beim Ausprobieren von sACN über den eigenen AP den Puls mit `pulse: false` abschalten oder
|
||||||
|
abwarten. Außerdem muss der Sender bei zwei Default-Routen (LAN und AP) das Interface für Multicast
|
||||||
|
festlegen (`IP_MULTICAST_IF`), sonst sendet er ins eigene LAN statt in den AP.
|
||||||
|
|
||||||
## Firmware-Update
|
## Firmware-Update
|
||||||
|
|
||||||
Der Build erzeugt pro Gerät zwei Images:
|
Der Build erzeugt pro Gerät zwei Images:
|
||||||
|
|||||||
+84
-1
@@ -234,6 +234,16 @@ items:
|
|||||||
Enthält `last_ota_error`: `null`, solange kein Update fehlgeschlagen ist, sonst einer von
|
Enthält `last_ota_error`: `null`, solange kein Update fehlgeschlagen ist, sonst einer von
|
||||||
`download`, `crc`, `trailer`, `device_mismatch`, `hash`, `busy` (`busy` heißt: es läuft bereits
|
`download`, `crc`, `trailer`, `device_mismatch`, `hash`, `busy` (`busy` heißt: es läuft bereits
|
||||||
ein Update). Kein `reset_reason` mehr im Feld-Satz.
|
ein Update). Kein `reset_reason` mehr im Feld-Satz.
|
||||||
|
|
||||||
|
`wifi.standalone` unterscheidet den dauerhaften eigenen Zugangspunkt vom
|
||||||
|
Onboarding-AP - `wifi.mode` ist bei beiden `"ap"`. Im AP-Betrieb steht unter
|
||||||
|
`wifi.ssid` der **laufende** AP-Name, nicht die gespeicherte Stations-SSID; der
|
||||||
|
Name des Heimnetzes steht damit nicht in dieser tokenfrei lesbaren Antwort.
|
||||||
|
|
||||||
|
`sacn.running` sagt, ob der Empfangs-Task lebt und einen Socket haelt - zusammen
|
||||||
|
mit `sacn.active` (es kommt gerade ein gueltiger Strom an) und `sacn.multicast`
|
||||||
|
(der letzte Gruppenbeitritt hat geklappt) ist damit unterscheidbar, ob niemand
|
||||||
|
sendet oder ob der Empfang tot ist.
|
||||||
- info:
|
- info:
|
||||||
name: Get System Settings
|
name: Get System Settings
|
||||||
type: http
|
type: http
|
||||||
@@ -251,6 +261,12 @@ items:
|
|||||||
sonst `"compiled"` - dann laeuft die Lampe auf den einkompilierten
|
sonst `"compiled"` - dann laeuft die Lampe auf den einkompilierten
|
||||||
NINLIGHT_WIFI_SSID/-PASSWORD, `wifi.ssid` ist leer, und `GET system/info` kann trotzdem
|
NINLIGHT_WIFI_SSID/-PASSWORD, `wifi.ssid` ist leer, und `GET system/info` kann trotzdem
|
||||||
eine verbundene SSID zeigen.
|
eine verbundene SSID zeigen.
|
||||||
|
|
||||||
|
Der `standalone`-Block zeigt `enabled`, den **gewuenschten** AP-Namen (`ssid`: der
|
||||||
|
gespeicherte, ohne eigenen der Hostname), `channel` und `pulse`. Das AP-Passwort
|
||||||
|
steht nie darin. Der gewuenschte Name kann vom laufenden abweichen, den
|
||||||
|
`GET system/info` meldet - genau das ist das Erkennungsmerkmal fuer einen Rueckfall
|
||||||
|
auf die Vorgaben nach mehrfach gescheitertem AP-Aufbau.
|
||||||
- info:
|
- info:
|
||||||
name: Set WiFi
|
name: Set WiFi
|
||||||
type: http
|
type: http
|
||||||
@@ -350,10 +366,77 @@ items:
|
|||||||
timeout: 0
|
timeout: 0
|
||||||
followRedirects: true
|
followRedirects: true
|
||||||
maxRedirects: 5
|
maxRedirects: 5
|
||||||
|
- info:
|
||||||
|
name: Standalone einschalten
|
||||||
|
type: http
|
||||||
|
seq: 8
|
||||||
|
http:
|
||||||
|
method: PUT
|
||||||
|
url: '{{device_address}}:{{device_port}}/api/v1/system/settings'
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"type": "standalone",
|
||||||
|
"enabled": true,
|
||||||
|
"password": "geheim123",
|
||||||
|
"ssid": "Buehne",
|
||||||
|
"channel": 6,
|
||||||
|
"pulse": true
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
docs: |-
|
||||||
|
Die Lampe spannt dauerhaft einen eigenen Zugangspunkt auf, statt sich in ein WLAN
|
||||||
|
einzuwaehlen - danach ist sie unter `192.168.4.1` erreichbar, per mDNS als
|
||||||
|
`<hostname>.local`. Antwort **202** `{"status":"standalone"}`, nicht 200: die
|
||||||
|
Antwort muss das Geraet verlassen haben, bevor das Netz unter der Verbindung
|
||||||
|
weggezogen wird.
|
||||||
|
|
||||||
|
`password` ist beim Einschalten Pflicht, solange keines gespeichert ist (8..63
|
||||||
|
Zeichen). `ssid` (1..32), `channel` (1..13, Vorgabe 6) und `pulse` sind optional
|
||||||
|
und behalten sonst ihren gespeicherten Wert; ein leerer `ssid`-String wird mit 400
|
||||||
|
`invalid ssid` abgelehnt.
|
||||||
|
|
||||||
|
**Achtung beim Ausprobieren:** Aenderst du Name oder Passwort, waehrend du ueber
|
||||||
|
genau diesen AP verbunden bist, baut die Lampe ihn neu auf und wirft dich hinaus -
|
||||||
|
die 202 kommt noch an, danach musst du dem neuen Netz beitreten.
|
||||||
|
|
||||||
|
Anders als der unkonfigurierte Onboarding-AP ist der Standalone-AP **nicht**
|
||||||
|
token-frei: ab da gilt der Tokenschutz wie im Normalbetrieb.
|
||||||
|
- info:
|
||||||
|
name: Standalone ausschalten
|
||||||
|
type: http
|
||||||
|
seq: 9
|
||||||
|
http:
|
||||||
|
method: PUT
|
||||||
|
url: '{{device_address}}:{{device_port}}/api/v1/system/settings'
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"type": "standalone",
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
docs: |-
|
||||||
|
Braucht kein Passwort. Hat die Lampe gespeicherte WLAN-Daten, waehlt sie sich damit
|
||||||
|
wieder ein; ohne solche oeffnet sie den unkonfigurierten Onboarding-AP
|
||||||
|
`ninlight-<mac4>` - der ist dann wieder token-frei.
|
||||||
|
|
||||||
|
Der gespeicherte AP-Name bleibt dabei in den Einstellungen stehen und greift beim
|
||||||
|
naechsten Einschalten des Modus erneut.
|
||||||
- info:
|
- info:
|
||||||
name: Commands
|
name: Commands
|
||||||
type: folder
|
type: folder
|
||||||
seq: 8
|
seq: 10
|
||||||
items:
|
items:
|
||||||
- info:
|
- info:
|
||||||
name: Ping
|
name: Ping
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Hardware-Checkliste (EVK)
|
# Hardware-Checkliste (EVK)
|
||||||
|
|
||||||
Konsole: `tools/monitor.sh /dev/cu.<port>` (115200 8N1). Flashen: `tools/flash.sh evk <port>`.
|
Konsole: `tools/monitor.sh /dev/cu.<port>` (115200 8N1). Flashen: `tools/flash.sh evk <port>` — das Skript nutzt `-rs none`, die Lampe braucht also einen **manuellen** Reset in dem Zeitfenster, in dem `wm_tool` auf den Sync wartet; ein automatischer Reset über die Steuerleitungen funktioniert an diesem Board nicht.
|
||||||
|
|
||||||
## Licht (Plan 1, Task 11)
|
## Licht (Plan 1, Task 11)
|
||||||
- [x] Nach dem Boot blendet warmweiß 100 % in 0,5 s ein
|
- [x] Nach dem Boot blendet warmweiß 100 % in 0,5 s ein
|
||||||
@@ -65,6 +65,24 @@ Für den echten Flottenbau `NINLIGHT_RECOVERY_PASSWORD` und `NINLIGHT_TOKEN` in
|
|||||||
- [x] G95: Pinbelegung WW=PB15/CW=PB13 — 2026-09-13 an einer migrierten G95 geprüft: unteres Bereichsende deutlich warm, oberes deutlich kühler (damals noch 2500–5000 K, inzwischen 2000–5000 K), Dimmen auf 10 % flackerfrei, weiches Ausblenden
|
- [x] G95: Pinbelegung WW=PB15/CW=PB13 — 2026-09-13 an einer migrierten G95 geprüft: unteres Bereichsende deutlich warm, oberes deutlich kühler (damals noch 2500–5000 K, inzwischen 2000–5000 K), Dimmen auf 10 % flackerfrei, weiches Ausblenden
|
||||||
- [ ] Farbtemperaturbereiche aller Varianten — *die Bereiche stammen seit 2026-09-13 von Michael aus den Produktdaten und stehen so in `nl_board.c`: G95 Vintage **2000–5000 K**, alle übrigen **2700–6500 K**. Vorher standen die vier Weiß-Varianten flach auf 2500–5000 K, was für die E27/E14/GU10 CW den oberen Bereich abgeschnitten und für die G95 den unteren verschenkt hat. Am Gerät gegengeprüft ist bisher nur gu10_rgb; die G95 wurde mit den alten Grenzen getestet und gehört an den neuen wiederholt.*
|
- [ ] Farbtemperaturbereiche aller Varianten — *die Bereiche stammen seit 2026-09-13 von Michael aus den Produktdaten und stehen so in `nl_board.c`: G95 Vintage **2000–5000 K**, alle übrigen **2700–6500 K**. Vorher standen die vier Weiß-Varianten flach auf 2500–5000 K, was für die E27/E14/GU10 CW den oberen Bereich abgeschnitten und für die G95 den unteren verschenkt hat. Am Gerät gegengeprüft ist bisher nur gu10_rgb; die G95 wurde mit den alten Grenzen getestet und gehört an den neuen wiederholt.*
|
||||||
|
|
||||||
|
## Standalone-AP-Modus (Abnahme am EVK, MAC 28:6d:cd:5c:68:1f)
|
||||||
|
|
||||||
|
- [x] `type: standalone` per `PUT system/settings` einschalten → 202 `{"status":"standalone"}`, `system/info` meldet `wifi.standalone: true` — 2026-10-02
|
||||||
|
- [x] Neustart → Lampe startet direkt im eigenen AP: Konsole `wifi: standalone AP, hostname ninlight-681f`, keine Onboarding-Zeile, kein Onboarding-Pulsen (den gibt es nur im Zustand `AP_ONBOARDING`, aus dem Code ableitbar; der Standalone-Startpuls ist davon getrennt und unten offen) — 2026-10-02
|
||||||
|
- [x] Geänderte AP-Daten → `wifi: AP data changed, rebuilding`, danach `wifi: AP ninlight-681f (ch 6) up` — 2026-10-02
|
||||||
|
- [x] Standalone mit exakt den Vorgabedaten auf einem bereits stehenden Onboarding-AP (kein Neuaufbau) löst die mDNS-Bekanntmachung ebenfalls aus — 2026-10-02
|
||||||
|
- [x] mDNS im eigenen AP: Konsole `mdns: ninlight-681f.local _ninlight._tcp up on en1 192.168.4.1`, `dns-sd -G v4 ninlight-681f.local` löst auf `192.168.4.1` auf, `ping` 0 % Verlust — 2026-10-02
|
||||||
|
- [x] REST über den AP antwortet auf `192.168.4.1:3333` — 2026-10-02
|
||||||
|
- [x] Startpuls als Sichtprüfung: Michael hat zugesehen — der Puls lief nach dem Start, hörte nach rund 5 s auf, danach stand die voreingestellte Farbe wieder. Das Einschaltverhalten greift vor dem Netz-Task (`app_main.c:224` vs. `:250`), die Lampe zeigt deshalb erst ihre Farbe und der Puls legt sich darüber — 2026-10-02
|
||||||
|
- [x] REST über den AP verlangt das Token: im Standalone-AP `GET system/settings` ohne Token → 401 `unauthorized`, mit Werkstoken → 200; `GET system/info` ohne Token → 200. Gegenprobe nach dem Werksreset im unkonfigurierten Onboarding-AP: derselbe Aufruf ohne Token → **200** — 2026-10-02
|
||||||
|
- [x] `{"enabled":false}` **ohne** gespeicherte WLAN-Daten: 202, danach heißt der AP wieder `ninlight-681f` (Beitritt mit dem Vorgabepasswort gelang, Name **und** Passwort sind also umgestellt), `system/info` meldet `wifi.ssid: ninlight-681f` und `standalone: false`, `GET system/settings` ohne Token → 200. Der gespeicherte Wunschname bleibt in `system/settings` als `standalone.ssid: Buehne` stehen — 2026-10-02
|
||||||
|
- [x] Standalone-Daten ändern, während der Aufrufer **über den AP** verbunden ist: die 202 kommt an. Direkter Vorher/Nachher-Beleg — derselbe Aufruf lieferte auf dem Stand **vor** der I6-Pause `HTTP 000` (Verbindung riss ab, bevor die Antwort raus war), mit der Pause `{"status":"standalone"} HTTP 202` — 2026-10-02
|
||||||
|
- [x] WLAN eingeben, falsches Passwort → Gerät bleibt im eigenen AP mit eigenen Daten: 202 `connecting`, nach dem Fehlschlag `wifi.error: auth`, `standalone: true`, AP weiterhin `Buehne` mit dem Nutzerpasswort, `GET system/settings` ohne Token weiterhin **401** — 2026-10-02
|
||||||
|
- [x] WLAN eingeben, richtiges Passwort → Gerät im Heimnetz (192.168.2.58, `standalone: false`), nach einem Neustart `mode: sta` und **nicht** wieder im AP — 2026-10-02
|
||||||
|
- [x] Im Standalone-Betrieb meldet `GET system/info` unter `wifi.ssid` den AP-Namen: an einer Lampe mit gespeichertem Heimnetz (`settings.wifi.ssid: ninIOT, source: stored`) meldete die **tokenfreie** Antwort `wifi.ssid: Buehne`, nicht `ninIOT`. Vor dem Wechsel in den Standalone-Modus stand dort korrekt die Stations-SSID — 2026-10-02
|
||||||
|
- [x] Werksreset über 5× ein/aus → wieder unkonfigurierter Onboarding-AP: alte Heimnetz-IP antwortet nicht mehr, AP `ninlight-681f` mit Vorgabepasswort, `settings` ohne Token → 200, alle Werte auf Vorgabe (`name: ''`, `standalone.enabled: false`) — 2026-10-02
|
||||||
|
- [x] sACN über den Standalone-AP, sichtbar an der Lampe: Michael hat grün (unicast an 192.168.4.1) und blau (multicast 239.255.0.1, Sender mit `IP_MULTICAST_IF` auf 192.168.4.2) gesehen, `sacn.active` jeweils true — 2026-10-02
|
||||||
|
|
||||||
## Offene Beobachtung aus der EVK-Session vom 2026-09-12
|
## Offene Beobachtung aus der EVK-Session vom 2026-09-12
|
||||||
|
|
||||||
Beim Test mit dem verfälschten Image meldete lwIP einmalig
|
Beim Test mit dem verfälschten Image meldete lwIP einmalig
|
||||||
|
|||||||
+26
-3
@@ -111,6 +111,19 @@ int nl_app_set_wifi_credentials(const char *ssid, const char *pass)
|
|||||||
return nl_app_save_settings();
|
return nl_app_save_settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int nl_app_clear_standalone(void)
|
||||||
|
{
|
||||||
|
bool was_set;
|
||||||
|
|
||||||
|
nl_app_lock();
|
||||||
|
was_set = s_settings.standalone != 0;
|
||||||
|
s_settings.standalone = 0;
|
||||||
|
nl_app_unlock();
|
||||||
|
if (!was_set)
|
||||||
|
return 0; /* nichts zu schreiben - der Flash-Satz steht schon richtig */
|
||||||
|
return nl_app_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
void nl_app_factory_reset(void)
|
void nl_app_factory_reset(void)
|
||||||
{
|
{
|
||||||
printf("factory reset\r\n");
|
printf("factory reset\r\n");
|
||||||
@@ -228,10 +241,20 @@ static void app_task(void *arg)
|
|||||||
boot = s_settings;
|
boot = s_settings;
|
||||||
nl_app_unlock();
|
nl_app_unlock();
|
||||||
nl_mdns_set_name(boot.name);
|
nl_mdns_set_name(boot.name);
|
||||||
if (boot.wifi_ssid[0])
|
if (boot.standalone)
|
||||||
nl_wifi_start(boot.wifi_ssid, boot.wifi_pass, NINLIGHT_AP_PASSWORD);
|
/* Kein Verbindungsversuch: die Lampe steht ausdruecklich allein. Die gespeicherten
|
||||||
|
* Zugangsdaten gehen trotzdem mit - der Standalone-Zweig der Zustandsmaschine
|
||||||
|
* verbindet nicht, aber sie zaehlen wieder, sobald der Modus ueber die API
|
||||||
|
* zurueckgenommen wird; ohne sie landete die Lampe dann im offenen
|
||||||
|
* Onboarding-AP, obwohl ihr WLAN bekannt ist. */
|
||||||
|
nl_wifi_start(boot.wifi_ssid, boot.wifi_pass,
|
||||||
|
boot.ap_pass[0] ? boot.ap_pass : NINLIGHT_AP_PASSWORD,
|
||||||
|
boot.ap_ssid, boot.ap_channel, true, boot.standalone_pulse != 0);
|
||||||
|
else if (boot.wifi_ssid[0])
|
||||||
|
nl_wifi_start(boot.wifi_ssid, boot.wifi_pass, NINLIGHT_AP_PASSWORD, "", 0, false, false);
|
||||||
else
|
else
|
||||||
nl_wifi_start(NINLIGHT_WIFI_SSID, NINLIGHT_WIFI_PASSWORD, NINLIGHT_AP_PASSWORD);
|
nl_wifi_start(NINLIGHT_WIFI_SSID, NINLIGHT_WIFI_PASSWORD, NINLIGHT_AP_PASSWORD,
|
||||||
|
"", 0, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
nl_http_server_start(nl_app_api_ctx());
|
nl_http_server_start(nl_app_api_ctx());
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ typedef char nl_settings_legacy_check[offsetof(nl_settings_t, sacn_enabled) == 2
|
|||||||
* Vorgabe. */
|
* Vorgabe. */
|
||||||
typedef char nl_settings_layout_offset_check[offsetof(nl_settings_t, sacn_layout) == 216 ? 1 : -1];
|
typedef char nl_settings_layout_offset_check[offsetof(nl_settings_t, sacn_layout) == 216 ? 1 : -1];
|
||||||
|
|
||||||
|
/* Dasselbe fuer die Standalone-Felder: standalone muss bei 220 beginnen, dem
|
||||||
|
* sizeof(nl_settings_t) der Firmware davor. Wer hier anstoesst, hat ein Feld zu frueh
|
||||||
|
* eingefuegt - dann kaeme standalone_pulse aus einem aelteren Satz als Fuellbyte (0)
|
||||||
|
* zurueck statt als Vorgabe (1), und der Startpuls waere still aus. */
|
||||||
|
typedef char nl_settings_standalone_check[offsetof(nl_settings_t, standalone) == 220 ? 1 : -1];
|
||||||
|
|
||||||
static uint32_t s_addr[2];
|
static uint32_t s_addr[2];
|
||||||
|
|
||||||
void nl_settings_defaults(nl_settings_t *s)
|
void nl_settings_defaults(nl_settings_t *s)
|
||||||
@@ -78,6 +84,7 @@ void nl_settings_defaults(nl_settings_t *s)
|
|||||||
s->sacn_start_slot = 1;
|
s->sacn_start_slot = 1;
|
||||||
s->sacn_hold = 0;
|
s->sacn_hold = 0;
|
||||||
s->sacn_layout = 1;
|
s->sacn_layout = 1;
|
||||||
|
s->standalone_pulse = 1; /* standalone, ap_channel, ap_ssid, ap_pass bleiben auf 0/leer */
|
||||||
}
|
}
|
||||||
|
|
||||||
void nl_settings_init(uint32_t addr_a, uint32_t addr_b)
|
void nl_settings_init(uint32_t addr_a, uint32_t addr_b)
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ typedef struct {
|
|||||||
* dem vorherigen sizeof (216), sonst ueberschreibt das Fuellbyte
|
* dem vorherigen sizeof (216), sonst ueberschreibt das Fuellbyte
|
||||||
* eines aelteren Satzes sie. */
|
* eines aelteren Satzes sie. */
|
||||||
uint8_t _padding2[3]; /* haelt sizeof auf einem Vielfachen von 4 */
|
uint8_t _padding2[3]; /* haelt sizeof auf einem Vielfachen von 4 */
|
||||||
|
uint8_t standalone; /* Offset 220 - 1 = eigener AP statt Station. Dieselbe Regel
|
||||||
|
* wie oben: neue Felder beginnen bei oder hinter dem
|
||||||
|
* vorherigen sizeof (220), sonst ueberschreibt das
|
||||||
|
* Fuellbyte eines aelteren Satzes sie. */
|
||||||
|
uint8_t ap_channel; /* 0 = Vorgabe (6), sonst 1..13 */
|
||||||
|
uint8_t standalone_pulse; /* 1 = beim Start kurz pulsen (Vorgabe) */
|
||||||
|
uint8_t _padding3;
|
||||||
|
char ap_ssid[NL_SSID_LEN]; /* leer = "ninlight-<mac4>" */
|
||||||
|
char ap_pass[NL_PASS_LEN]; /* leer = einkompiliertes AP-Passwort. Taucht in keiner
|
||||||
|
* Antwort und in keiner Protokollzeile auf. */
|
||||||
|
uint8_t _padding4[2]; /* haelt sizeof auf einem Vielfachen von 4 (324) */
|
||||||
} nl_settings_t;
|
} nl_settings_t;
|
||||||
|
|
||||||
void nl_settings_defaults(nl_settings_t *s);
|
void nl_settings_defaults(nl_settings_t *s);
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
#define P_SETTINGS "/api/v1/system/settings"
|
#define P_SETTINGS "/api/v1/system/settings"
|
||||||
#define P_COMMAND "/api/v1/system/command"
|
#define P_COMMAND "/api/v1/system/command"
|
||||||
|
|
||||||
|
/* Obergrenze fuer das AP-Passwort des Standalone-Modus (WPA2-ASCII-Passphrase). */
|
||||||
|
#define NL_AP_PASS_MAX 63
|
||||||
|
|
||||||
static const char *const POB_NAMES[] = {"last", "on", "off"};
|
static const char *const POB_NAMES[] = {"last", "on", "off"};
|
||||||
|
|
||||||
static void reply(nl_api_response_t *r, int status, const char *json)
|
static void reply(nl_api_response_t *r, int status, const char *json)
|
||||||
@@ -259,6 +262,10 @@ static void handle_info(const nl_api_ctx_t *ctx, nl_api_response_t *resp)
|
|||||||
cJSON_AddStringToObject(w, "error", in.wifi_error);
|
cJSON_AddStringToObject(w, "error", in.wifi_error);
|
||||||
else
|
else
|
||||||
cJSON_AddNullToObject(w, "error");
|
cJSON_AddNullToObject(w, "error");
|
||||||
|
/* "mode" allein reicht nicht: im Standalone-Betrieb ist es "ap", genau wie beim offenen
|
||||||
|
* Onboarding-AP. Ohne dieses Feld boete die App einer fertig eingerichteten Lampe ein
|
||||||
|
* Onboarding an. */
|
||||||
|
cJSON_AddBoolToObject(w, "standalone", in.wifi_standalone);
|
||||||
cJSON_AddNumberToObject(o, "uptime", in.uptime_s);
|
cJSON_AddNumberToObject(o, "uptime", in.uptime_s);
|
||||||
cJSON_AddNumberToObject(o, "free_heap", in.free_heap);
|
cJSON_AddNumberToObject(o, "free_heap", in.free_heap);
|
||||||
if (in.last_ota_error)
|
if (in.last_ota_error)
|
||||||
@@ -275,6 +282,12 @@ static void handle_info(const nl_api_ctx_t *ctx, nl_api_response_t *resp)
|
|||||||
* der Abnahme nicht zu unterscheiden, ob die Lampe nicht beitritt oder das WLAN die
|
* der Abnahme nicht zu unterscheiden, ob die Lampe nicht beitritt oder das WLAN die
|
||||||
* Gruppe nicht weiterreicht - Unicast laeuft in beiden Faellen. */
|
* Gruppe nicht weiterreicht - Unicast laeuft in beiden Faellen. */
|
||||||
cJSON_AddBoolToObject(sa, "multicast", in.sacn_joined);
|
cJSON_AddBoolToObject(sa, "multicast", in.sacn_joined);
|
||||||
|
/* Ob der Empfangs-Task ueberhaupt laeuft. Ohne dieses Feld war ein toter Task von
|
||||||
|
* "es sendet gerade niemand" nicht zu unterscheiden - beides meldete nur
|
||||||
|
* active=false, und "multicast" bleibt vom letzten gelungenen Beitritt true, auch
|
||||||
|
* wenn der Socket laengst zu ist. Das hat beim Montana-Black-Shield eine halbe
|
||||||
|
* Stunde Suche gekostet. */
|
||||||
|
cJSON_AddBoolToObject(sa, "running", in.sacn_running);
|
||||||
}
|
}
|
||||||
reply_json(resp, 200, o);
|
reply_json(resp, 200, o);
|
||||||
}
|
}
|
||||||
@@ -308,6 +321,16 @@ static void handle_get_settings(const nl_api_ctx_t *ctx, nl_api_response_t *resp
|
|||||||
cJSON_AddStringToObject(sa, "layout", s.sacn_layout ? "rgb" : "dimmer");
|
cJSON_AddStringToObject(sa, "layout", s.sacn_layout ? "rgb" : "dimmer");
|
||||||
cJSON_AddNumberToObject(sa, "slots", nl_board_dmx_slots(ctx->board, s.sacn_layout != 0));
|
cJSON_AddNumberToObject(sa, "slots", nl_board_dmx_slots(ctx->board, s.sacn_layout != 0));
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
cJSON *sa = cJSON_AddObjectToObject(o, "standalone");
|
||||||
|
cJSON_AddBoolToObject(sa, "enabled", s.standalone != 0);
|
||||||
|
/* Der WIRKSAME Name, nicht der gespeicherte: ohne eigenen Namen heisst der AP
|
||||||
|
* wie der Hostname. Wer hier einen leeren String saehe, muesste die Vorgabe
|
||||||
|
* selbst nachbilden. Das AP-Passwort steht bewusst nicht hier. */
|
||||||
|
cJSON_AddStringToObject(sa, "ssid", s.ap_ssid[0] ? s.ap_ssid : ctx->hostname);
|
||||||
|
cJSON_AddNumberToObject(sa, "channel", s.ap_channel ? s.ap_channel : 6);
|
||||||
|
cJSON_AddBoolToObject(sa, "pulse", s.standalone_pulse != 0);
|
||||||
|
}
|
||||||
reply_json(resp, 200, o);
|
reply_json(resp, 200, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +362,67 @@ static void handle_put_settings(const nl_api_ctx_t *ctx, const nl_http_request_t
|
|||||||
cJSON_Delete(j);
|
cJSON_Delete(j);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if (strcmp(type, "standalone") == 0) {
|
||||||
|
const cJSON *en = cJSON_GetObjectItemCaseSensitive(j, "enabled");
|
||||||
|
const cJSON *pu = cJSON_GetObjectItemCaseSensitive(j, "pulse");
|
||||||
|
const char *as = NULL, *ap = NULL;
|
||||||
|
long ch = 0;
|
||||||
|
int asp, app, chp;
|
||||||
|
bool will_enable;
|
||||||
|
nl_settings_t prev = s; /* Stand vor der Aenderung, zum Zurueckrollen bei 503 */
|
||||||
|
|
||||||
|
will_enable = cJSON_IsBool(en) && cJSON_IsTrue(en);
|
||||||
|
asp = get_string(j, "ssid", 1, NL_SSID_LEN - 1, &as);
|
||||||
|
/* 63, nicht NL_PASS_LEN - 1 (64): eine WPA2-ASCII-Passphrase darf hoechstens 63
|
||||||
|
* Zeichen haben. Bei 64 schriebe der Treiber keyinfo.key[64] randvoll ohne
|
||||||
|
* Abschlussbyte, und ob libwlan.a das annimmt, sieht man dem Binaerobjekt nicht an -
|
||||||
|
* ein abgelehnter AP-Aufbau ist hier aber eine ueber die API erreichbare Sackgasse.
|
||||||
|
* Der "wifi"-Zweig bleibt bewusst bei 64: dort haengt kein eigener AP daran. */
|
||||||
|
app = get_string(j, "password", 8, NL_AP_PASS_MAX, &ap);
|
||||||
|
chp = get_int(j, "channel", 1, 13, &ch);
|
||||||
|
|
||||||
|
if (!cJSON_IsBool(en)) {
|
||||||
|
err = "invalid enabled";
|
||||||
|
} else if (asp < 0) {
|
||||||
|
err = "invalid ssid";
|
||||||
|
} else if (app < 0) {
|
||||||
|
/* Die Meldung nennt das Feld, nie den Wert: ein zu kurzes Passwort darf
|
||||||
|
* nicht im Protokoll des Aufrufers landen. */
|
||||||
|
err = "invalid password";
|
||||||
|
} else if (chp < 0) {
|
||||||
|
err = "invalid channel";
|
||||||
|
} else if (will_enable && app != 1 && s.ap_pass[0] == '\0') {
|
||||||
|
err = "password required";
|
||||||
|
} else if (pu && !cJSON_IsBool(pu)) {
|
||||||
|
err = "invalid pulse";
|
||||||
|
} else {
|
||||||
|
if (asp == 1)
|
||||||
|
strcpy(s.ap_ssid, as);
|
||||||
|
if (app == 1)
|
||||||
|
strcpy(s.ap_pass, ap);
|
||||||
|
if (chp == 1)
|
||||||
|
s.ap_channel = (uint8_t)ch;
|
||||||
|
if (pu)
|
||||||
|
s.standalone_pulse = cJSON_IsTrue(pu) ? 1 : 0;
|
||||||
|
s.standalone = will_enable ? 1 : 0;
|
||||||
|
/* Wie bei "wifi" kehrt dieser Zweig hier zurueck statt ans gemeinsame Ende zu
|
||||||
|
* laufen (das antwortet 200): der Wechsel zieht der Verbindung das Netz weg, die
|
||||||
|
* Antwort ist 202 und muss vorher ausgefuellt sein. */
|
||||||
|
if (ctx->save_settings(&s) != 0) {
|
||||||
|
reply_error(resp, 500, "save failed");
|
||||||
|
} else if (ctx->standalone_configure(will_enable, s.ap_ssid, s.ap_pass,
|
||||||
|
s.ap_channel, s.standalone_pulse != 0) != 0) {
|
||||||
|
/* 503 heisst "nicht uebernommen": was schon im Flash steht, muss zurueck,
|
||||||
|
* sonst startet die Lampe nach einem Neustart doch im neuen Modus. Best
|
||||||
|
* effort - scheitert auch das, bleibt nur die Meldung. */
|
||||||
|
(void)ctx->save_settings(&prev);
|
||||||
|
reply_error(resp, 503, "wifi busy");
|
||||||
|
} else {
|
||||||
|
reply(resp, 202, "{\"status\":\"standalone\"}");
|
||||||
|
}
|
||||||
|
cJSON_Delete(j);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (strcmp(type, "device_name") == 0) {
|
} else if (strcmp(type, "device_name") == 0) {
|
||||||
if (get_string(j, "name", 1, NL_NAME_LEN - 1, &a) != 1)
|
if (get_string(j, "name", 1, NL_NAME_LEN - 1, &a) != 1)
|
||||||
err = "invalid name";
|
err = "invalid name";
|
||||||
|
|||||||
@@ -26,18 +26,25 @@ typedef struct {
|
|||||||
const char *last_ota_error; /* NULL until OTA exists */
|
const char *last_ota_error; /* NULL until OTA exists */
|
||||||
bool sacn_active; /* true, solange die Lampe ferngesteuert wird */
|
bool sacn_active; /* true, solange die Lampe ferngesteuert wird */
|
||||||
bool sacn_joined; /* Multicast-Gruppe des Universe beigetreten */
|
bool sacn_joined; /* Multicast-Gruppe des Universe beigetreten */
|
||||||
|
bool sacn_running; /* Empfangs-Task lebt und haelt einen Socket */
|
||||||
|
bool wifi_standalone; /* eigener Dauer-AP, nicht der offene Onboarding-AP */
|
||||||
} nl_sys_info_t;
|
} nl_sys_info_t;
|
||||||
|
|
||||||
/* Everything the API needs from the application; implemented by src/sys/nl_app_api.c. */
|
/* Everything the API needs from the application; implemented by src/sys/nl_app_api.c. */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const nl_board_t *board;
|
const nl_board_t *board;
|
||||||
const char *fw_version;
|
const char *fw_version;
|
||||||
|
const char *hostname; /* "ninlight-<mac4>", Vorgabe fuer den AP-Namen */
|
||||||
const char *build_date;
|
const char *build_date;
|
||||||
void (*get_state)(nl_light_state_t *out);
|
void (*get_state)(nl_light_state_t *out);
|
||||||
nl_ctrl_err_t (*apply)(const nl_light_change_t *c);
|
nl_ctrl_err_t (*apply)(const nl_light_change_t *c);
|
||||||
void (*get_settings)(nl_settings_t *out);
|
void (*get_settings)(nl_settings_t *out);
|
||||||
int (*save_settings)(const nl_settings_t *s); /* 0 = ok; wifi fields are ignored */
|
int (*save_settings)(const nl_settings_t *s); /* 0 = ok; wifi fields are ignored */
|
||||||
int (*wifi_configure)(const char *ssid, const char *pass); /* 0 = onboarding test started */
|
int (*wifi_configure)(const char *ssid, const char *pass); /* 0 = onboarding test started */
|
||||||
|
/* 0 = uebernommen (Antwort 202), sonst 503. Wie wifi_configure wirkt der Wechsel
|
||||||
|
erst nach der Antwort - die Verbindung bricht sonst vor dem Senden weg. */
|
||||||
|
int (*standalone_configure)(bool enabled, const char *ssid, const char *pass,
|
||||||
|
uint8_t channel, bool pulse);
|
||||||
bool (*open_access)(void); /* unconfigured onboarding AP */
|
bool (*open_access)(void); /* unconfigured onboarding AP */
|
||||||
void (*get_info)(nl_sys_info_t *out);
|
void (*get_info)(nl_sys_info_t *out);
|
||||||
void (*factory_reset)(void); /* request only */
|
void (*factory_reset)(void); /* request only */
|
||||||
|
|||||||
+65
-6
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "wm_include.h"
|
#include "wm_include.h"
|
||||||
#include "lwip/apps/mdns.h"
|
#include "lwip/apps/mdns.h"
|
||||||
|
#include "lwip/netif.h"
|
||||||
#include "lwip/tcpip.h"
|
#include "lwip/tcpip.h"
|
||||||
|
|
||||||
#include "config/nl_settings.h"
|
#include "config/nl_settings.h"
|
||||||
@@ -16,8 +17,13 @@
|
|||||||
#define NL_MDNS_TTL 120
|
#define NL_MDNS_TTL 120
|
||||||
|
|
||||||
static bool s_inited;
|
static bool s_inited;
|
||||||
|
static struct netif *s_nif; /* Netif, an dem Antwortdienst und Dienst haengen */
|
||||||
static bool s_netif_added;
|
static bool s_netif_added;
|
||||||
static bool s_service_added;
|
/* Vom Netz-Task gelesen, im tcpip-Thread geschrieben: volatile, und s_announce_pending deckt
|
||||||
|
* das Fenster zwischen dem Anstossen und dem Lauf von announce() ab. Ohne das meldete
|
||||||
|
* nl_mdns_service_up() beim Wechsel Station -> eigener AP noch den Dienst des alten Netifs. */
|
||||||
|
static volatile bool s_service_added;
|
||||||
|
static volatile bool s_announce_pending;
|
||||||
static char s_name[NL_NAME_LEN];
|
static char s_name[NL_NAME_LEN];
|
||||||
static char s_mac[18];
|
static char s_mac[18];
|
||||||
|
|
||||||
@@ -45,15 +51,53 @@ static void txt_cb(struct mdns_service *service, void *data)
|
|||||||
add_txt(service, "name", name);
|
add_txt(service, "name", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Das SDK fuehrt zwei Netifs (wm_ethernet.c, Tcpip_stack_init): tls_get_netif() liefert das
|
||||||
|
* der STATION; das des Softap wurde zuerst angelegt und haengt als ->next dahinter (dieselbe
|
||||||
|
* Annahme treffen DHCPS_Start/DNSS_Start im SDK mit nif->next, ethernetif_input() mit
|
||||||
|
* netif->next). Im Standalone-Modus gibt es keine Station, die Antworten muessen ueber das
|
||||||
|
* AP-Netif gehen, sonst haengt der Dienst an einem Netif ohne Adresse und Funkstrecke. */
|
||||||
|
static struct netif *pick_netif(void)
|
||||||
|
{
|
||||||
|
struct netif *sta = tls_get_netif();
|
||||||
|
struct netif *n;
|
||||||
|
|
||||||
|
if (!sta)
|
||||||
|
return NULL;
|
||||||
|
if (!nl_wifi_is_standalone())
|
||||||
|
return sta;
|
||||||
|
/* Erst der bekannte Platz, dann zur Sicherheit die lwip-Kette: das erste Netif mit
|
||||||
|
* IPv4-Adresse, das nicht die Station ist (Loopback gibt es in diesem SDK nicht). */
|
||||||
|
if (sta->next && !ip4_addr_isany_val(*netif_ip4_addr(sta->next)))
|
||||||
|
return sta->next;
|
||||||
|
for (n = netif_list; n != NULL; n = n->next)
|
||||||
|
if (n != sta && !ip4_addr_isany_val(*netif_ip4_addr(n)))
|
||||||
|
return n;
|
||||||
|
return NULL; /* AP-Netif noch ohne Adresse: lieber nichts melden als am falschen Netif */
|
||||||
|
}
|
||||||
|
|
||||||
/* tcpip thread */
|
/* tcpip thread */
|
||||||
static void announce(void *arg)
|
static void announce(void *arg)
|
||||||
{
|
{
|
||||||
struct netif *nif = tls_get_netif();
|
struct netif *nif = pick_netif();
|
||||||
bool attempted = false;
|
bool attempted = false;
|
||||||
|
|
||||||
(void)arg;
|
(void)arg;
|
||||||
if (!nif)
|
if (!nif) {
|
||||||
|
printf("mdns: no usable netif yet\r\n");
|
||||||
|
s_announce_pending = false;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Anderes Netif als bisher (Station <-> eigener AP): ein blosses
|
||||||
|
* mdns_resp_netif_settings_changed() wuerde am alten haengen bleiben, denn Antwortdienst
|
||||||
|
* und Dienst sind je Netif angelegt. Also am alten Netif abmelden - remove_netif gibt auch
|
||||||
|
* dessen Dienste und die Multicast-Gruppe frei - und am neuen frisch anmelden. */
|
||||||
|
if (s_netif_added && s_nif != nif) {
|
||||||
|
mdns_resp_remove_netif(s_nif);
|
||||||
|
s_netif_added = false;
|
||||||
|
s_service_added = false;
|
||||||
|
}
|
||||||
|
s_nif = nif;
|
||||||
|
|
||||||
if (!s_inited) {
|
if (!s_inited) {
|
||||||
mdns_resp_init();
|
mdns_resp_init();
|
||||||
@@ -69,10 +113,19 @@ static void announce(void *arg)
|
|||||||
NL_MDNS_TTL, txt_cb, NULL) == ERR_OK;
|
NL_MDNS_TTL, txt_cb, NULL) == ERR_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempted)
|
if (attempted) {
|
||||||
printf("mdns: %s.local _ninlight._tcp %s\r\n", nl_wifi_hostname(), s_service_added ? "up" : "failed");
|
/* Welches Netif benutzt wurde, steht in der Zeile selbst: Namenskuerzel, Nummer, IPv4.
|
||||||
else
|
* Der rohe netif-Zeiger stand hier frueher mit (%p) dabei; waehrend der Fehlersuche war
|
||||||
|
* er nuetzlich, in einer Release-Firmware ist er Rauschen - Kuerzel und Adresse
|
||||||
|
* unterscheiden Station und eigenen AP bereits eindeutig, und eine Speicheradresse in
|
||||||
|
* einem Protokoll, das Nutzer weiterreichen, hat keinen Nutzen. */
|
||||||
|
const ip4_addr_t *ip = netif_ip4_addr(nif);
|
||||||
|
printf("mdns: %s.local _ninlight._tcp %s on %c%c%u %u.%u.%u.%u\r\n", nl_wifi_hostname(),
|
||||||
|
s_service_added ? "up" : "failed", nif->name[0], nif->name[1], (unsigned)nif->num,
|
||||||
|
ip4_addr1(ip), ip4_addr2(ip), ip4_addr3(ip), ip4_addr4(ip));
|
||||||
|
} else
|
||||||
mdns_resp_netif_settings_changed(nif); /* re-announce: new IP or TXT */
|
mdns_resp_netif_settings_changed(nif); /* re-announce: new IP or TXT */
|
||||||
|
s_announce_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void nl_mdns_on_ip_up(void)
|
void nl_mdns_on_ip_up(void)
|
||||||
@@ -80,9 +133,15 @@ void nl_mdns_on_ip_up(void)
|
|||||||
uint8_t mac[6];
|
uint8_t mac[6];
|
||||||
nl_wifi_mac(mac);
|
nl_wifi_mac(mac);
|
||||||
nl_names_mac_str(mac, s_mac);
|
nl_names_mac_str(mac, s_mac);
|
||||||
|
s_announce_pending = true;
|
||||||
tcpip_callback(announce, NULL);
|
tcpip_callback(announce, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool nl_mdns_service_up(void)
|
||||||
|
{
|
||||||
|
return !s_announce_pending && s_service_added;
|
||||||
|
}
|
||||||
|
|
||||||
void nl_mdns_set_name(const char *name)
|
void nl_mdns_set_name(const char *name)
|
||||||
{
|
{
|
||||||
u32 sr = tls_os_set_critical();
|
u32 sr = tls_os_set_critical();
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
#ifndef NL_MDNS_H
|
#ifndef NL_MDNS_H
|
||||||
#define NL_MDNS_H
|
#define NL_MDNS_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
/* Called by the network task once the station got an IP (again). */
|
/* Called by the network task once the station got an IP (again). */
|
||||||
void nl_mdns_on_ip_up(void);
|
void nl_mdns_on_ip_up(void);
|
||||||
|
/* True, sobald der Dienst wirklich angemeldet ist - und erst, nachdem die zuletzt
|
||||||
|
* angestossene Bekanntmachung im tcpip-Thread auch gelaufen ist. Der Netz-Task stoesst
|
||||||
|
* im eigenen AP so lange erneut an, bis das hier wahr ist (I5). */
|
||||||
|
bool nl_mdns_service_up(void);
|
||||||
/* Device name for the TXT record; re-announces when mDNS is running. */
|
/* Device name for the TXT record; re-announces when mDNS is running. */
|
||||||
void nl_mdns_set_name(const char *name);
|
void nl_mdns_set_name(const char *name);
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -19,8 +19,12 @@ typedef struct {
|
|||||||
* sure the network task is still alive, not just the light task. */
|
* sure the network task is still alive, not just the light task. */
|
||||||
extern volatile uint32_t nl_net_beat;
|
extern volatile uint32_t nl_net_beat;
|
||||||
|
|
||||||
/* Starts the network task. An empty ssid starts the onboarding AP. Strings are copied. */
|
/* Starts the network task. An empty ssid starts the onboarding AP. Strings are copied.
|
||||||
void nl_wifi_start(const char *ssid, const char *pass, const char *ap_pass);
|
* Ein leerer ap_ssid heisst "wie der Hostname", ap_channel 0 heisst Kanal 6. Mit
|
||||||
|
* standalone == true wird gar nicht erst verbunden: die Lampe oeffnet ihren eigenen,
|
||||||
|
* passwortgeschuetzten AP, pulse laesst sie dabei kurz als Lebenszeichen pulsen. */
|
||||||
|
void nl_wifi_start(const char *ssid, const char *pass, const char *ap_pass,
|
||||||
|
const char *ap_ssid, uint8_t ap_channel, bool standalone, bool pulse);
|
||||||
/* Switches the SDK's own auto-connect off and drops any connection it may already have brought
|
/* Switches the SDK's own auto-connect off and drops any connection it may already have brought
|
||||||
* up on its own. nl_wifi_start() does this too, but the boot recovery path runs before that and
|
* up on its own. nl_wifi_start() does this too, but the boot recovery path runs before that and
|
||||||
* must not have the SDK joining its stored AP in parallel (I3, final review). Safe to call any
|
* must not have the SDK joining its stored AP in parallel (I3, final review). Safe to call any
|
||||||
@@ -34,6 +38,13 @@ void nl_wifi_disable_sdk_auto_connect(void);
|
|||||||
bool nl_wifi_join_blocking(const char *ssid, const char *pass, uint32_t timeout_ms);
|
bool nl_wifi_join_blocking(const char *ssid, const char *pass, uint32_t timeout_ms);
|
||||||
/* Starts an onboarding test with new credentials (from the HTTP task). 0 = queued. */
|
/* Starts an onboarding test with new credentials (from the HTTP task). 0 = queued. */
|
||||||
int nl_wifi_request_onboard(const char *ssid, const char *pass);
|
int nl_wifi_request_onboard(const char *ssid, const char *pass);
|
||||||
|
/* Schaltet den Standalone-Modus zur Laufzeit ein oder aus (aus dem HTTP-Task). Die Werte
|
||||||
|
* werden kopiert und erst im Netz-Task wirksam, nachdem die Antwort raus ist; ein leeres
|
||||||
|
* ssid/pass bzw. channel == 0 bedeutet jeweils die Vorgabe. 0 = uebernommen, sonst 503. */
|
||||||
|
int nl_wifi_request_standalone(bool enabled, const char *ssid, const char *pass,
|
||||||
|
uint8_t channel, bool pulse);
|
||||||
|
/* True, solange die Lampe in ihrem eigenen Dauer-AP steht. */
|
||||||
|
bool nl_wifi_is_standalone(void);
|
||||||
/* True while the unconfigured onboarding AP is open (API without token). */
|
/* True while the unconfigured onboarding AP is open (API without token). */
|
||||||
bool nl_wifi_open_access(void);
|
bool nl_wifi_open_access(void);
|
||||||
void nl_wifi_status(nl_wifi_status_t *out);
|
void nl_wifi_status(nl_wifi_status_t *out);
|
||||||
|
|||||||
+109
-5
@@ -40,7 +40,15 @@ static void arm_ap_stop(nl_wifi_sm_t *sm, uint32_t at)
|
|||||||
static void fail_test(nl_wifi_sm_t *sm, nl_wifi_error_t err, uint32_t now, nl_wifi_actions_t *a)
|
static void fail_test(nl_wifi_sm_t *sm, nl_wifi_error_t err, uint32_t now, nl_wifi_actions_t *a)
|
||||||
{
|
{
|
||||||
sm->error = err;
|
sm->error = err;
|
||||||
if (sm->have_creds) {
|
if (sm->standalone) {
|
||||||
|
/* Zurueck in den EIGENEN AP. Der offene Onboarding-AP waere hier doppelt falsch:
|
||||||
|
* er verloere Name und Passwort des Nutzers, und open_access() machte die Lampe
|
||||||
|
* ohne Token flashbar. Ein Tippfehler im WLAN-Passwort darf sie nicht entsichern. */
|
||||||
|
sm->state = NL_WIFI_ST_STANDALONE;
|
||||||
|
sm->ap_up = true;
|
||||||
|
a->disconnect = true;
|
||||||
|
a->start_ap = true;
|
||||||
|
} else if (sm->have_creds) {
|
||||||
sm->state = NL_WIFI_ST_APSTA_FALLBACK;
|
sm->state = NL_WIFI_ST_APSTA_FALLBACK;
|
||||||
sm->since = now;
|
sm->since = now;
|
||||||
sm->backoff = NL_WIFI_BACKOFF_MIN_MS;
|
sm->backoff = NL_WIFI_BACKOFF_MIN_MS;
|
||||||
@@ -54,13 +62,26 @@ static void fail_test(nl_wifi_sm_t *sm, nl_wifi_error_t err, uint32_t now, nl_wi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nl_wifi_actions_t nl_wifi_sm_start(nl_wifi_sm_t *sm, bool have_creds, uint32_t now)
|
nl_wifi_actions_t nl_wifi_sm_start(nl_wifi_sm_t *sm, bool have_creds, bool standalone, bool pulse, uint32_t now)
|
||||||
{
|
{
|
||||||
nl_wifi_actions_t a = none();
|
nl_wifi_actions_t a = none();
|
||||||
|
|
||||||
memset(sm, 0, sizeof *sm);
|
memset(sm, 0, sizeof *sm);
|
||||||
sm->have_creds = have_creds;
|
sm->have_creds = have_creds;
|
||||||
if (have_creds) {
|
sm->standalone = standalone;
|
||||||
|
if (standalone) {
|
||||||
|
/* No connect attempt, not even with stored credentials: the mode is the
|
||||||
|
* user's explicit choice. */
|
||||||
|
sm->state = NL_WIFI_ST_STANDALONE;
|
||||||
|
sm->ap_up = true;
|
||||||
|
a.start_ap = true;
|
||||||
|
if (pulse) {
|
||||||
|
sm->pulsing = true;
|
||||||
|
sm->pulse_armed = true;
|
||||||
|
sm->pulse_stop_at = now + NL_WIFI_STANDALONE_PULSE_MS;
|
||||||
|
a.pulse_on = true;
|
||||||
|
}
|
||||||
|
} else if (have_creds) {
|
||||||
begin_connecting(sm, now);
|
begin_connecting(sm, now);
|
||||||
a.connect = true;
|
a.connect = true;
|
||||||
sm->awaiting = true;
|
sm->awaiting = true;
|
||||||
@@ -101,6 +122,10 @@ nl_wifi_actions_t nl_wifi_sm_event(nl_wifi_sm_t *sm, nl_wifi_event_t ev, nl_wifi
|
|||||||
case NL_WIFI_ST_ONBOARD_TEST:
|
case NL_WIFI_ST_ONBOARD_TEST:
|
||||||
if (ev == NL_WIFI_EV_CONNECTED) {
|
if (ev == NL_WIFI_EV_CONNECTED) {
|
||||||
a.save_test_creds = true;
|
a.save_test_creds = true;
|
||||||
|
if (sm->standalone) {
|
||||||
|
sm->standalone = false;
|
||||||
|
a.clear_standalone = true;
|
||||||
|
}
|
||||||
sm->have_creds = true;
|
sm->have_creds = true;
|
||||||
sm->error = NL_WIFI_ERR_NONE;
|
sm->error = NL_WIFI_ERR_NONE;
|
||||||
sm->state = NL_WIFI_ST_STA_CONNECTED;
|
sm->state = NL_WIFI_ST_STA_CONNECTED;
|
||||||
@@ -118,6 +143,10 @@ nl_wifi_actions_t nl_wifi_sm_event(nl_wifi_sm_t *sm, nl_wifi_event_t ev, nl_wifi
|
|||||||
if (ev == NL_WIFI_EV_CONNECTED)
|
if (ev == NL_WIFI_EV_CONNECTED)
|
||||||
a.disconnect = true; /* I1: stray success of a timed-out/failed attempt */
|
a.disconnect = true; /* I1: stray success of a timed-out/failed attempt */
|
||||||
break;
|
break;
|
||||||
|
case NL_WIFI_ST_STANDALONE:
|
||||||
|
if (ev == NL_WIFI_EV_CONNECTED)
|
||||||
|
a.disconnect = true; /* stray success of an old attempt */
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -128,6 +157,11 @@ nl_wifi_actions_t nl_wifi_sm_tick(nl_wifi_sm_t *sm, uint32_t now)
|
|||||||
{
|
{
|
||||||
nl_wifi_actions_t a = none();
|
nl_wifi_actions_t a = none();
|
||||||
|
|
||||||
|
if (sm->pulse_armed && due(now, sm->pulse_stop_at)) {
|
||||||
|
sm->pulse_armed = false;
|
||||||
|
sm->pulsing = false;
|
||||||
|
a.pulse_off = true;
|
||||||
|
}
|
||||||
if (sm->ap_stop_armed && due(now, sm->ap_stop_at)) {
|
if (sm->ap_stop_armed && due(now, sm->ap_stop_at)) {
|
||||||
sm->ap_stop_armed = false;
|
sm->ap_stop_armed = false;
|
||||||
sm->ap_up = false;
|
sm->ap_up = false;
|
||||||
@@ -173,20 +207,90 @@ nl_wifi_actions_t nl_wifi_sm_onboard(nl_wifi_sm_t *sm, uint32_t now)
|
|||||||
sm->deadline = now + NL_WIFI_ONBOARD_TIMEOUT_MS;
|
sm->deadline = now + NL_WIFI_ONBOARD_TIMEOUT_MS;
|
||||||
sm->retry_armed = false;
|
sm->retry_armed = false;
|
||||||
sm->ap_stop_armed = false;
|
sm->ap_stop_armed = false;
|
||||||
|
if (sm->pulse_armed) {
|
||||||
|
/* Der Startpuls gehoert nicht in den Test: sonst feuerte spaeter ein pulse_off, das zu
|
||||||
|
* keinem Puls mehr gehoert. Hier abraeumen; pulsing auch, damit er nicht haengen bleibt. */
|
||||||
|
sm->pulse_armed = false;
|
||||||
|
sm->pulsing = false;
|
||||||
|
a.pulse_off = true;
|
||||||
|
}
|
||||||
a.disconnect = true; /* M1: drop any stale station attempt so its events don't count */
|
a.disconnect = true; /* M1: drop any stale station attempt so its events don't count */
|
||||||
a.connect = true;
|
a.connect = true;
|
||||||
a.use_test_creds = true;
|
a.use_test_creds = true;
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nl_wifi_actions_t nl_wifi_sm_standalone(nl_wifi_sm_t *sm, bool pulse, uint32_t now)
|
||||||
|
{
|
||||||
|
nl_wifi_actions_t a = none();
|
||||||
|
|
||||||
|
sm->standalone = true;
|
||||||
|
sm->state = NL_WIFI_ST_STANDALONE;
|
||||||
|
sm->error = NL_WIFI_ERR_NONE;
|
||||||
|
sm->retry_armed = false;
|
||||||
|
sm->ap_stop_armed = false;
|
||||||
|
sm->awaiting = false;
|
||||||
|
a.disconnect = true; /* eine laufende Stationsverbindung aufgeben */
|
||||||
|
if (!sm->ap_up) {
|
||||||
|
sm->ap_up = true;
|
||||||
|
a.start_ap = true;
|
||||||
|
}
|
||||||
|
if (pulse) {
|
||||||
|
sm->pulsing = true;
|
||||||
|
sm->pulse_armed = true;
|
||||||
|
sm->pulse_stop_at = now + NL_WIFI_STANDALONE_PULSE_MS;
|
||||||
|
a.pulse_on = true;
|
||||||
|
} else if (sm->pulsing) {
|
||||||
|
/* Ein laufender unbefristeter Puls (aus dem Onboarding-AP) hat im eigenen AP keinen
|
||||||
|
* Timer mehr, der ihn beendet; er uebermalte dauerhaft die normale Ausgabe. */
|
||||||
|
sm->pulsing = false;
|
||||||
|
sm->pulse_armed = false;
|
||||||
|
a.pulse_off = true;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
nl_wifi_actions_t nl_wifi_sm_leave_standalone(nl_wifi_sm_t *sm, bool have_creds, uint32_t now)
|
||||||
|
{
|
||||||
|
nl_wifi_actions_t a = none();
|
||||||
|
|
||||||
|
sm->standalone = false;
|
||||||
|
sm->have_creds = have_creds;
|
||||||
|
sm->pulse_armed = false;
|
||||||
|
if (have_creds) {
|
||||||
|
begin_connecting(sm, now);
|
||||||
|
a.connect = true;
|
||||||
|
sm->awaiting = true;
|
||||||
|
sm->connect_at = now;
|
||||||
|
if (sm->pulsing) {
|
||||||
|
sm->pulsing = false;
|
||||||
|
a.pulse_off = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Ohne Zugangsdaten bliebe die Lampe sonst unerreichbar: der offene
|
||||||
|
* Onboarding-AP ist hier der gewollte Ausweg. */
|
||||||
|
sm->state = NL_WIFI_ST_AP_ONBOARDING;
|
||||||
|
sm->ap_up = true;
|
||||||
|
sm->pulsing = true;
|
||||||
|
a.start_ap = true;
|
||||||
|
a.pulse_on = true;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
bool nl_wifi_sm_open_access(const nl_wifi_sm_t *sm)
|
bool nl_wifi_sm_open_access(const nl_wifi_sm_t *sm)
|
||||||
{
|
{
|
||||||
return sm->state == NL_WIFI_ST_AP_ONBOARDING || (sm->state == NL_WIFI_ST_ONBOARD_TEST && !sm->have_creds);
|
/* !standalone is NOT redundant: during an onboarding test started from standalone mode
|
||||||
|
* the state is ONBOARD_TEST with have_creds == false, which the second clause alone would
|
||||||
|
* call open. The user's password-protected AP is up then, so the token check must stay on
|
||||||
|
* (up to NL_WIFI_ONBOARD_TIMEOUT_MS), same as in normal operation. */
|
||||||
|
return !sm->standalone &&
|
||||||
|
(sm->state == NL_WIFI_ST_AP_ONBOARDING || (sm->state == NL_WIFI_ST_ONBOARD_TEST && !sm->have_creds));
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *nl_wifi_sm_mode(const nl_wifi_sm_t *sm)
|
const char *nl_wifi_sm_mode(const nl_wifi_sm_t *sm)
|
||||||
{
|
{
|
||||||
if (sm->state == NL_WIFI_ST_AP_ONBOARDING)
|
if (sm->state == NL_WIFI_ST_AP_ONBOARDING || sm->state == NL_WIFI_ST_STANDALONE)
|
||||||
return "ap";
|
return "ap";
|
||||||
return sm->ap_up ? "apsta" : "sta";
|
return sm->ap_up ? "apsta" : "sta";
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-2
@@ -11,13 +11,15 @@
|
|||||||
#define NL_WIFI_BACKOFF_MIN_MS 1000u
|
#define NL_WIFI_BACKOFF_MIN_MS 1000u
|
||||||
#define NL_WIFI_BACKOFF_MAX_MS 60000u
|
#define NL_WIFI_BACKOFF_MAX_MS 60000u
|
||||||
#define NL_WIFI_CONNECT_TIMEOUT_MS 30000u /* no event after a connect -> treat as failed */
|
#define NL_WIFI_CONNECT_TIMEOUT_MS 30000u /* no event after a connect -> treat as failed */
|
||||||
|
#define NL_WIFI_STANDALONE_PULSE_MS 5000u /* short sign-of-life pulse, then normal light */
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
NL_WIFI_ST_AP_ONBOARDING = 0,
|
NL_WIFI_ST_AP_ONBOARDING = 0,
|
||||||
NL_WIFI_ST_STA_CONNECTING,
|
NL_WIFI_ST_STA_CONNECTING,
|
||||||
NL_WIFI_ST_STA_CONNECTED,
|
NL_WIFI_ST_STA_CONNECTED,
|
||||||
NL_WIFI_ST_APSTA_FALLBACK,
|
NL_WIFI_ST_APSTA_FALLBACK,
|
||||||
NL_WIFI_ST_ONBOARD_TEST
|
NL_WIFI_ST_ONBOARD_TEST,
|
||||||
|
NL_WIFI_ST_STANDALONE
|
||||||
} nl_wifi_state_t;
|
} nl_wifi_state_t;
|
||||||
|
|
||||||
typedef enum { NL_WIFI_ERR_NONE = 0, NL_WIFI_ERR_AUTH, NL_WIFI_ERR_NOT_FOUND, NL_WIFI_ERR_TIMEOUT } nl_wifi_error_t;
|
typedef enum { NL_WIFI_ERR_NONE = 0, NL_WIFI_ERR_AUTH, NL_WIFI_ERR_NOT_FOUND, NL_WIFI_ERR_TIMEOUT } nl_wifi_error_t;
|
||||||
@@ -33,6 +35,7 @@ typedef struct {
|
|||||||
bool disconnect; /* drop a stray/failed station attempt (before any connect in the same set) */
|
bool disconnect; /* drop a stray/failed station attempt (before any connect in the same set) */
|
||||||
bool pulse_on;
|
bool pulse_on;
|
||||||
bool pulse_off;
|
bool pulse_off;
|
||||||
|
bool clear_standalone; /* Onboarding gelungen: den Standalone-Modus dauerhaft loeschen */
|
||||||
} nl_wifi_actions_t;
|
} nl_wifi_actions_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -44,18 +47,26 @@ typedef struct {
|
|||||||
bool retry_armed;
|
bool retry_armed;
|
||||||
bool ap_stop_armed;
|
bool ap_stop_armed;
|
||||||
bool awaiting; /* waiting for response to a connect */
|
bool awaiting; /* waiting for response to a connect */
|
||||||
|
bool standalone; /* stays set across an onboarding test */
|
||||||
|
bool pulse_armed; /* start pulse still running */
|
||||||
uint32_t since; /* start of the current connecting phase */
|
uint32_t since; /* start of the current connecting phase */
|
||||||
uint32_t retry_at;
|
uint32_t retry_at;
|
||||||
uint32_t backoff;
|
uint32_t backoff;
|
||||||
uint32_t deadline; /* onboarding test */
|
uint32_t deadline; /* onboarding test */
|
||||||
uint32_t ap_stop_at;
|
uint32_t ap_stop_at;
|
||||||
uint32_t connect_at; /* when the pending connect was issued */
|
uint32_t connect_at; /* when the pending connect was issued */
|
||||||
|
uint32_t pulse_stop_at;
|
||||||
} nl_wifi_sm_t;
|
} nl_wifi_sm_t;
|
||||||
|
|
||||||
nl_wifi_actions_t nl_wifi_sm_start(nl_wifi_sm_t *sm, bool have_creds, uint32_t now);
|
nl_wifi_actions_t nl_wifi_sm_start(nl_wifi_sm_t *sm, bool have_creds, bool standalone, bool pulse, uint32_t now);
|
||||||
nl_wifi_actions_t nl_wifi_sm_event(nl_wifi_sm_t *sm, nl_wifi_event_t ev, nl_wifi_error_t err, uint32_t now);
|
nl_wifi_actions_t nl_wifi_sm_event(nl_wifi_sm_t *sm, nl_wifi_event_t ev, nl_wifi_error_t err, uint32_t now);
|
||||||
nl_wifi_actions_t nl_wifi_sm_tick(nl_wifi_sm_t *sm, uint32_t now);
|
nl_wifi_actions_t nl_wifi_sm_tick(nl_wifi_sm_t *sm, uint32_t now);
|
||||||
nl_wifi_actions_t nl_wifi_sm_onboard(nl_wifi_sm_t *sm, uint32_t now);
|
nl_wifi_actions_t nl_wifi_sm_onboard(nl_wifi_sm_t *sm, uint32_t now);
|
||||||
|
/* Schaltet zur Laufzeit in den Standalone-Modus (aus der API). */
|
||||||
|
nl_wifi_actions_t nl_wifi_sm_standalone(nl_wifi_sm_t *sm, bool pulse, uint32_t now);
|
||||||
|
/* Nimmt ihn zurueck: mit gespeicherten Zugangsdaten ins WLAN, sonst in den
|
||||||
|
offenen Onboarding-AP. */
|
||||||
|
nl_wifi_actions_t nl_wifi_sm_leave_standalone(nl_wifi_sm_t *sm, bool have_creds, uint32_t now);
|
||||||
bool nl_wifi_sm_open_access(const nl_wifi_sm_t *sm);
|
bool nl_wifi_sm_open_access(const nl_wifi_sm_t *sm);
|
||||||
const char *nl_wifi_sm_mode(const nl_wifi_sm_t *sm);
|
const char *nl_wifi_sm_mode(const nl_wifi_sm_t *sm);
|
||||||
const char *nl_wifi_error_name(nl_wifi_error_t e);
|
const char *nl_wifi_error_name(nl_wifi_error_t e);
|
||||||
|
|||||||
+306
-15
@@ -18,6 +18,10 @@
|
|||||||
#define NL_NET_QUEUE 8
|
#define NL_NET_QUEUE 8
|
||||||
#define MSG_NETIF 0x100u /* | netif status */
|
#define MSG_NETIF 0x100u /* | netif status */
|
||||||
#define MSG_ONBOARD 0x200u
|
#define MSG_ONBOARD 0x200u
|
||||||
|
/* 0x400, nicht 0x300: die netif-Meldungen werden weiter unten mit (m & MSG_NETIF) erkannt,
|
||||||
|
* und 0x300 traegt das 0x100-Bit mit - die Standalone-Meldung waere als netif-Ereignis
|
||||||
|
* protokolliert worden. */
|
||||||
|
#define MSG_STANDALONE 0x400u
|
||||||
|
|
||||||
static u32 s_stack[768];
|
static u32 s_stack[768];
|
||||||
static tls_os_queue_t *s_queue;
|
static tls_os_queue_t *s_queue;
|
||||||
@@ -29,6 +33,34 @@ static char s_hostname[24];
|
|||||||
static char s_ssid[NL_SSID_LEN], s_pass[NL_PASS_LEN];
|
static char s_ssid[NL_SSID_LEN], s_pass[NL_PASS_LEN];
|
||||||
static char s_test_ssid[NL_SSID_LEN], s_test_pass[NL_PASS_LEN];
|
static char s_test_ssid[NL_SSID_LEN], s_test_pass[NL_PASS_LEN];
|
||||||
static char s_ap_pass[NL_PASS_LEN];
|
static char s_ap_pass[NL_PASS_LEN];
|
||||||
|
static char s_ap_ssid[NL_SSID_LEN]; /* leer = Hostname */
|
||||||
|
static uint8_t s_ap_channel = 6;
|
||||||
|
/* Die Daten, mit denen der laufende AP tatsaechlich aufgebaut wurde. Nur so ist zu sehen,
|
||||||
|
* ob ein Wunsch aus der API den AP wirklich veraendert - die Zustandsmaschine liefert kein
|
||||||
|
* start_ap, wenn der AP schon steht, und die alten Daten blieben sonst still in Kraft. */
|
||||||
|
static char s_ap_live_ssid[NL_SSID_LEN], s_ap_live_pass[NL_PASS_LEN];
|
||||||
|
static uint8_t s_ap_live_channel;
|
||||||
|
static bool s_ap_live;
|
||||||
|
/* mDNS im eigenen AP haengt am ZUSTAND (Standalone + AP steht), nicht am Erzeugungsereignis:
|
||||||
|
* betritt die Lampe Standalone auf einem schon laufenden, unveraenderten AP, entsteht kein
|
||||||
|
* SOFTAP_SUCCESS mehr. Beides nur im Netz-Task angefasst. */
|
||||||
|
static bool s_ap_event_seen; /* zum aktuell stehenden AP kam SOFTAP_SUCCESS an */
|
||||||
|
static bool s_ap_mdns_done; /* fuer den aktuell stehenden AP steht der mDNS-Dienst wirklich */
|
||||||
|
/* I5: s_ap_mdns_done haengt am ERFOLG, nicht am Versuch. nl_mdns_on_ip_up() reicht die Arbeit
|
||||||
|
* nur an den tcpip-Thread weiter; findet announce() dort noch kein Netif mit Adresse, gibt es
|
||||||
|
* auf. Bis der Dienst steht, wird alle NL_AP_MDNS_RETRY_MS erneut angestossen. */
|
||||||
|
#define NL_AP_MDNS_RETRY_MS 5000u
|
||||||
|
static uint32_t s_ap_mdns_retry_at;
|
||||||
|
/* Drei Versuche, dann der Rueckfall auf Daten, die schon einmal getragen haben: einer koennte
|
||||||
|
* ein Zufall sein (das Funkteil raeumt gerade den alten AP ab), aber drei hintereinander
|
||||||
|
* heissen, dass das Funkteil genau diese Daten nicht annimmt. Und 3 x 5 s hoechstens
|
||||||
|
* 15 s ohne AP ist kuerzer als jede Fehlersuche vor Ort. */
|
||||||
|
#define NL_AP_CREATE_TRIES 3
|
||||||
|
static uint8_t s_ap_create_fails;
|
||||||
|
/* Vom HTTP-Task gefuellt, vom Netz-Task uebernommen; beides unter s_lock. */
|
||||||
|
static char s_req_ap_ssid[NL_SSID_LEN], s_req_ap_pass[NL_PASS_LEN];
|
||||||
|
static uint8_t s_req_ap_channel;
|
||||||
|
static bool s_req_enabled, s_req_pulse, s_req_pending;
|
||||||
static bool s_ap_create_pending; /* M3: tls_wifi_softap_create() failed, retry it */
|
static bool s_ap_create_pending; /* M3: tls_wifi_softap_create() failed, retry it */
|
||||||
static uint32_t s_ap_create_retry_at;
|
static uint32_t s_ap_create_retry_at;
|
||||||
|
|
||||||
@@ -88,18 +120,77 @@ void nl_wifi_disable_sdk_auto_connect(void)
|
|||||||
tls_wifi_disconnect();
|
tls_wifi_disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Setzt NUR Name und Kanal des naechsten AP auf die Vorgaben zurueck: "ninlight-<mac4>" auf
|
||||||
|
* Kanal 6. Das Passwort bleibt stehen - I1: ein Rueckfall darf das Passwort des Nutzers nicht
|
||||||
|
* durch das einkompilierte ersetzen, das in der README oeffentlich dokumentiert ist. Beruehrt
|
||||||
|
* einen LAUFENDEN AP nicht - der behaelt seine Daten, bis er abgebaut und neu aufgebaut wird. */
|
||||||
|
static void reset_ap_identity(void)
|
||||||
|
{
|
||||||
|
s_ap_ssid[0] = '\0';
|
||||||
|
s_ap_channel = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* I2: Vergisst, mit welchen Daten der zuletzt stehende AP aufgebaut wurde. Verlaesst die Lampe
|
||||||
|
* den Standalone-Modus, duerfen Name und Passwort des Nutzers nicht als "hat schon einmal
|
||||||
|
* getragen" liegenbleiben: der erste Rueckfallzweig in start_ap() holte sie sonst Minuten
|
||||||
|
* spaeter zurueck (etwa am Rueckfall-AP nach fuenf Minuten ohne Router), und die Lampe boete
|
||||||
|
* periodisch einen AP an, den der Nutzer laengst abgeschaltet hat. */
|
||||||
|
static void forget_ap_live(void)
|
||||||
|
{
|
||||||
|
s_ap_live_ssid[0] = s_ap_live_pass[0] = '\0';
|
||||||
|
s_ap_live_channel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vollstaendige Rueckstellung auf die Vorgaben, einschliesslich des einkompilierten Passworts.
|
||||||
|
* Nur fuer den Weg AUS dem Standalone-Modus heraus, wo der Wunsch des Nutzers endet. */
|
||||||
|
static void reset_ap_defaults(void)
|
||||||
|
{
|
||||||
|
reset_ap_identity();
|
||||||
|
copy(s_ap_pass, NINLIGHT_AP_PASSWORD, sizeof s_ap_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reisst den AP ab und raeumt dabei auch die Buchfuehrung auf. Laeuft nur im Netz-Task
|
||||||
|
* (oder vor dessen Start) und nie unter s_lock - clear_softap_param() fasst SDK-Zustand an. */
|
||||||
|
static void destroy_ap(void)
|
||||||
|
{
|
||||||
|
tls_wifi_softap_destroy();
|
||||||
|
s_ap_create_pending = false;
|
||||||
|
s_ap_live = false;
|
||||||
|
s_ap_event_seen = s_ap_mdns_done = false;
|
||||||
|
/* I3: Der Zaehler gehoert zu EINEM Datensatz. Ohne diese Nullung addieren sich die
|
||||||
|
* Fehlschlaege mit den alten Daten zu denen mit den neuen, und nach einer Korrektur
|
||||||
|
* (Kanal 13 -> Kanal 6) loeste schon ein einziger Fehlschlag den Rueckfall aus. */
|
||||||
|
s_ap_create_fails = 0;
|
||||||
|
clear_softap_param(); /* C2: every softap teardown clears the persisted SOFTAP bit */
|
||||||
|
}
|
||||||
|
|
||||||
static void start_ap(void)
|
static void start_ap(void)
|
||||||
{
|
{
|
||||||
struct tls_softap_info_t ap;
|
struct tls_softap_info_t ap;
|
||||||
struct tls_ip_info_t ip;
|
struct tls_ip_info_t ip;
|
||||||
|
const char *ssid = s_ap_ssid[0] ? s_ap_ssid : s_hostname;
|
||||||
size_t n = strlen(s_ap_pass);
|
size_t n = strlen(s_ap_pass);
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
|
/* Steht derselbe AP schon, bleibt er stehen: die Zustandsmaschine verlangt start_ap
|
||||||
|
* auch dann, wenn sie den AP gar nicht erst abgebaut hat (fehlgeschlagener
|
||||||
|
* Onboarding-Versuch aus dem Standalone-Modus), und ein Neuaufbau wuerfe dort alle
|
||||||
|
* Clients ohne Not heraus. */
|
||||||
|
if (s_ap_live && strcmp(s_ap_live_ssid, ssid) == 0 && strcmp(s_ap_live_pass, s_ap_pass) == 0 &&
|
||||||
|
s_ap_live_channel == s_ap_channel)
|
||||||
|
return;
|
||||||
|
/* Umgekehrt: Name, Passwort oder Kanal haben sich geaendert, waehrend der AP steht.
|
||||||
|
* Ohne eigenen Abriss liefe er mit den alten Daten weiter. */
|
||||||
|
if (s_ap_live) {
|
||||||
|
printf("wifi: AP data changed, rebuilding\r\n");
|
||||||
|
destroy_ap();
|
||||||
|
}
|
||||||
|
|
||||||
memset(&ap, 0, sizeof ap);
|
memset(&ap, 0, sizeof ap);
|
||||||
memset(&ip, 0, sizeof ip);
|
memset(&ip, 0, sizeof ip);
|
||||||
copy((char *)ap.ssid, s_hostname, sizeof ap.ssid);
|
copy((char *)ap.ssid, ssid, sizeof ap.ssid);
|
||||||
ap.encrypt = IEEE80211_ENCRYT_CCMP_WPA2;
|
ap.encrypt = IEEE80211_ENCRYT_CCMP_WPA2;
|
||||||
ap.channel = 6;
|
ap.channel = s_ap_channel;
|
||||||
ap.keyinfo.format = 1; /* ascii */
|
ap.keyinfo.format = 1; /* ascii */
|
||||||
ap.keyinfo.index = 1;
|
ap.keyinfo.index = 1;
|
||||||
ap.keyinfo.key_len = (u8)n;
|
ap.keyinfo.key_len = (u8)n;
|
||||||
@@ -108,10 +199,45 @@ static void start_ap(void)
|
|||||||
ip.netmask[0] = 255; ip.netmask[1] = 255; ip.netmask[2] = 255; ip.netmask[3] = 0;
|
ip.netmask[0] = 255; ip.netmask[1] = 255; ip.netmask[2] = 255; ip.netmask[3] = 0;
|
||||||
memcpy(ip.dnsname, "local", 6);
|
memcpy(ip.dnsname, "local", 6);
|
||||||
r = tls_wifi_softap_create(&ap, &ip);
|
r = tls_wifi_softap_create(&ap, &ip);
|
||||||
printf("wifi: AP %s %s\r\n", s_hostname, r == WM_SUCCESS ? "up" : "failed");
|
printf("wifi: AP %s (ch %u) %s\r\n", ssid, (unsigned)s_ap_channel,
|
||||||
|
r == WM_SUCCESS ? "up" : "failed");
|
||||||
/* M3: retry a failed AP create instead of leaving onboarding/fallback without an AP. */
|
/* M3: retry a failed AP create instead of leaving onboarding/fallback without an AP. */
|
||||||
s_ap_create_pending = r != WM_SUCCESS;
|
s_ap_create_pending = r != WM_SUCCESS;
|
||||||
s_ap_create_retry_at = nl_app_now_ms() + 5000u;
|
s_ap_create_retry_at = nl_app_now_ms() + 5000u;
|
||||||
|
s_ap_live = r == WM_SUCCESS;
|
||||||
|
if (s_ap_live) {
|
||||||
|
/* Neuer AP: erst das Ereignis abwarten, dann bekanntmachen. */
|
||||||
|
s_ap_event_seen = s_ap_mdns_done = false;
|
||||||
|
copy(s_ap_live_ssid, ssid, sizeof s_ap_live_ssid);
|
||||||
|
copy(s_ap_live_pass, s_ap_pass, sizeof s_ap_live_pass);
|
||||||
|
s_ap_live_channel = s_ap_channel;
|
||||||
|
s_ap_create_fails = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Nimmt das Funkteil diese Daten dauerhaft nicht an, steht die Lampe ohne AP da - und mit
|
||||||
|
* in den Einstellungen gespeicherten Wunschdaten auch nach einem Neustart wieder. Dann
|
||||||
|
* lieber ein AP unter unerwartetem Namen: der ist sichtbar und reparierbar. */
|
||||||
|
if (++s_ap_create_fails < NL_AP_CREATE_TRIES)
|
||||||
|
return;
|
||||||
|
s_ap_create_fails = 0;
|
||||||
|
if (s_ap_live_ssid[0] && (strcmp(s_ap_live_ssid, ssid) != 0 ||
|
||||||
|
strcmp(s_ap_live_pass, s_ap_pass) != 0 ||
|
||||||
|
s_ap_live_channel != s_ap_channel)) {
|
||||||
|
printf("wifi: AP create keeps failing, falling back to the last working data (%s, ch %u)\r\n",
|
||||||
|
s_ap_live_ssid, (unsigned)s_ap_live_channel);
|
||||||
|
copy(s_ap_ssid, s_ap_live_ssid, sizeof s_ap_ssid);
|
||||||
|
copy(s_ap_pass, s_ap_live_pass, sizeof s_ap_pass);
|
||||||
|
s_ap_channel = s_ap_live_channel;
|
||||||
|
} else if (s_ap_ssid[0] || s_ap_channel != 6) {
|
||||||
|
/* I1: nur Name und Kanal. Das Passwort ist der unwahrscheinlichste Grund fuer ein
|
||||||
|
* abgelehntes tls_wifi_softap_create() - die Laenge prueft die API selbst, Kanal und
|
||||||
|
* SSID sind die plausiblen Kandidaten. Ein Rueckfall auf NINLIGHT_AP_PASSWORD stellte
|
||||||
|
* eine Standalone-Lampe mit einem oeffentlich dokumentierten Passwort ins Funkfeld,
|
||||||
|
* und sACN hoert dort unauthentifiziert mit. */
|
||||||
|
printf("wifi: AP create keeps failing, falling back to %s on ch 6\r\n", s_hostname);
|
||||||
|
reset_ap_identity();
|
||||||
|
}
|
||||||
|
/* Sonst scheitern schon die Vorgaben - dann bleibt nur, es weiter zu versuchen. */
|
||||||
}
|
}
|
||||||
|
|
||||||
static void connect_station(bool test)
|
static void connect_station(bool test)
|
||||||
@@ -149,6 +275,24 @@ static void execute(nl_wifi_actions_t a)
|
|||||||
printf("wifi: onboarding ok, saving credentials for %s\r\n", ssid);
|
printf("wifi: onboarding ok, saving credentials for %s\r\n", ssid);
|
||||||
nl_app_set_wifi_credentials(ssid, pass);
|
nl_app_set_wifi_credentials(ssid, pass);
|
||||||
}
|
}
|
||||||
|
if (a.clear_standalone) {
|
||||||
|
/* Sonst stuende die Lampe nach dem naechsten Neustart wieder in ihrem eigenen AP,
|
||||||
|
* obwohl sie gerade erfolgreich ins WLAN gewechselt ist. */
|
||||||
|
printf("wifi: standalone mode cleared\r\n");
|
||||||
|
/* Dieselbe Rueckstellung wie beim Zurueckschalten ueber die API: sonst kaeme ein
|
||||||
|
* spaeterer Rueckfall-AP (5 min ohne Station) mit Namen und Passwort des Nutzers
|
||||||
|
* hoch, obwohl standalone in den Einstellungen schon 0 ist. forget_ap_live() raeumt
|
||||||
|
* dazu die Buchfuehrung ueber den zuletzt tragenden AP weg (I2) - sonst holte ihn
|
||||||
|
* der erste Rueckfallzweig in start_ap() nach drei abgelehnten Aufbauten zurueck.
|
||||||
|
* Der noch laufende Standalone-AP bleibt stehen, bis die Zustandsmaschine ihn
|
||||||
|
* abbaut: dieses Aktionsbuendel enthaelt kein start_ap. */
|
||||||
|
reset_ap_defaults();
|
||||||
|
forget_ap_live();
|
||||||
|
/* Schlaegt das Speichern fehl, steht die Lampe jetzt im WLAN, nach dem naechsten
|
||||||
|
* Neustart aber wieder in ihrem eigenen AP - ohne jede Spur. Wenigstens melden. */
|
||||||
|
if (nl_app_clear_standalone() != 0)
|
||||||
|
printf("wifi: could not persist the cleared standalone flag\r\n");
|
||||||
|
}
|
||||||
if (a.pulse_on)
|
if (a.pulse_on)
|
||||||
pulse(true);
|
pulse(true);
|
||||||
if (a.pulse_off)
|
if (a.pulse_off)
|
||||||
@@ -157,9 +301,7 @@ static void execute(nl_wifi_actions_t a)
|
|||||||
start_ap();
|
start_ap();
|
||||||
if (a.stop_ap) {
|
if (a.stop_ap) {
|
||||||
printf("wifi: AP down\r\n");
|
printf("wifi: AP down\r\n");
|
||||||
tls_wifi_softap_destroy();
|
destroy_ap();
|
||||||
s_ap_create_pending = false;
|
|
||||||
clear_softap_param(); /* C2: every softap teardown clears the persisted SOFTAP bit */
|
|
||||||
}
|
}
|
||||||
if (a.disconnect) {
|
if (a.disconnect) {
|
||||||
printf("wifi: dropping stray/failed station attempt\r\n");
|
printf("wifi: dropping stray/failed station attempt\r\n");
|
||||||
@@ -185,7 +327,9 @@ static void net_task(void *arg)
|
|||||||
void *msg = NULL;
|
void *msg = NULL;
|
||||||
nl_wifi_actions_t ev, tick;
|
nl_wifi_actions_t ev, tick;
|
||||||
uint32_t m = 0, now;
|
uint32_t m = 0, now;
|
||||||
bool got_ip_event = false, connected_now, ap_wanted, unexpected_softap;
|
bool got_ip_event = false, connected_now, ap_wanted, unexpected_softap,
|
||||||
|
standalone_now, open_now;
|
||||||
|
bool recheck_ap = false, rebuild_ap = false, ignored_off = false;
|
||||||
|
|
||||||
memset(&ev, 0, sizeof ev);
|
memset(&ev, 0, sizeof ev);
|
||||||
if (tls_os_queue_receive(s_queue, &msg, 0, HZ / 10) == TLS_OS_SUCCESS)
|
if (tls_os_queue_receive(s_queue, &msg, 0, HZ / 10) == TLS_OS_SUCCESS)
|
||||||
@@ -202,6 +346,38 @@ static void net_task(void *arg)
|
|||||||
ev = nl_wifi_sm_event(&s_sm, NL_WIFI_EV_DISCONNECTED, NL_WIFI_ERR_NONE, now);
|
ev = nl_wifi_sm_event(&s_sm, NL_WIFI_EV_DISCONNECTED, NL_WIFI_ERR_NONE, now);
|
||||||
} else if (m == MSG_ONBOARD) {
|
} else if (m == MSG_ONBOARD) {
|
||||||
ev = nl_wifi_sm_onboard(&s_sm, now);
|
ev = nl_wifi_sm_onboard(&s_sm, now);
|
||||||
|
} else if (m == MSG_STANDALONE) {
|
||||||
|
/* Die Wunschwerte erst hier uebernehmen: der HTTP-Task hat sie abgelegt, der
|
||||||
|
* Netz-Task ist der einzige, der s_ap_* schreibt und start_ap() aufruft. */
|
||||||
|
if (s_req_enabled) {
|
||||||
|
copy(s_ap_ssid, s_req_ap_ssid, sizeof s_ap_ssid);
|
||||||
|
copy(s_ap_pass, s_req_ap_pass[0] ? s_req_ap_pass : NINLIGHT_AP_PASSWORD,
|
||||||
|
sizeof s_ap_pass);
|
||||||
|
s_ap_channel = s_req_ap_channel ? s_req_ap_channel : 6;
|
||||||
|
ev = nl_wifi_sm_standalone(&s_sm, s_req_pulse, now);
|
||||||
|
/* Steht der AP schon, liefert die Zustandsmaschine kein start_ap. Ob die
|
||||||
|
* neuen Daten einen Neuaufbau verlangen, entscheidet start_ap() selbst. */
|
||||||
|
recheck_ap = true;
|
||||||
|
} else if (!s_sm.standalone) {
|
||||||
|
/* War die Lampe gar nicht standalone, gibt es nichts abzuschalten. Ohne diese
|
||||||
|
* Vorpruefung riefe leave_standalone() auf einer gewoehnlichen Stationslampe
|
||||||
|
* begin_connecting() und erzwaenge eine Neuverbindung ohne Anlass. Die Antwort
|
||||||
|
* bleibt 202: der gewuenschte Zustand gilt ja bereits. (standalone bleibt auch
|
||||||
|
* waehrend eines Onboarding-Tests gesetzt, der Test faellt also nicht hierher.) */
|
||||||
|
ignored_off = true;
|
||||||
|
} else {
|
||||||
|
/* Zurueck auf die Vorgaben: ein spaeterer Rueckfall- oder Onboarding-AP ist
|
||||||
|
* wieder der gewohnte "ninlight-<mac4>" mit dem einkompilierten Passwort,
|
||||||
|
* genau wie bei einer Lampe, die nie standalone war. */
|
||||||
|
reset_ap_defaults();
|
||||||
|
forget_ap_live();
|
||||||
|
ev = nl_wifi_sm_leave_standalone(&s_sm, s_ssid[0] != '\0', now);
|
||||||
|
rebuild_ap = true;
|
||||||
|
}
|
||||||
|
/* I3: Ein neuer Wunsch ist ein neuer Datensatz - die Fehlschlaege des alten
|
||||||
|
* duerfen nicht mitzaehlen. */
|
||||||
|
s_ap_create_fails = 0;
|
||||||
|
s_req_pending = false;
|
||||||
}
|
}
|
||||||
tick = nl_wifi_sm_tick(&s_sm, now);
|
tick = nl_wifi_sm_tick(&s_sm, now);
|
||||||
/* I1: a late/stray IP-up must not announce mDNS unless the SM actually settled on STA_CONNECTED. */
|
/* I1: a late/stray IP-up must not announce mDNS unless the SM actually settled on STA_CONNECTED. */
|
||||||
@@ -213,20 +389,77 @@ static void net_task(void *arg)
|
|||||||
* clear_softap_param() touch the SDK's own state and must not run while s_lock
|
* clear_softap_param() touch the SDK's own state and must not run while s_lock
|
||||||
* (which only guards s_sm and the credential buffers) is held. */
|
* (which only guards s_sm and the credential buffers) is held. */
|
||||||
unexpected_softap = m == (MSG_NETIF | NETIF_WIFI_SOFTAP_SUCCESS) && !ap_wanted;
|
unexpected_softap = m == (MSG_NETIF | NETIF_WIFI_SOFTAP_SUCCESS) && !ap_wanted;
|
||||||
|
/* Nur die Lage merken; der Aufruf nach aussen folgt hinter der Entsperrung. */
|
||||||
|
standalone_now = s_sm.state == NL_WIFI_ST_STANDALONE;
|
||||||
|
open_now = nl_wifi_sm_open_access(&s_sm);
|
||||||
unlock();
|
unlock();
|
||||||
|
|
||||||
if (unexpected_softap) {
|
if (unexpected_softap) {
|
||||||
printf("wifi: unexpected SDK softap while AP should be down, destroying it\r\n");
|
printf("wifi: unexpected SDK softap while AP should be down, destroying it\r\n");
|
||||||
tls_wifi_softap_destroy();
|
destroy_ap();
|
||||||
clear_softap_param();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m & MSG_NETIF)
|
if (m & MSG_NETIF)
|
||||||
printf("wifi: netif event %u\r\n", (unsigned)(m & 0xFFu));
|
printf("wifi: netif event %u\r\n", (unsigned)(m & 0xFFu));
|
||||||
|
/* I6: Die 202 auf den Standalone-Wunsch schreibt der HTTP-Task erst, nachdem
|
||||||
|
* nl_api_handle() zurueck ist - die Warteschlangennachricht geht aber schon darin
|
||||||
|
* raus, und dieser Task hat die hoehere Prioritaet (35 gegen 42, kleinere Zahl heisst
|
||||||
|
* hoeher). Er verdraengt also sofort und risse das Netz ab, bevor send_all() lief.
|
||||||
|
* Beim Wechsel AUS dem WLAN ging das bisher gut, weil tls_wifi_disconnect() asynchron
|
||||||
|
* arbeitet; ueber den eigenen AP ist tls_wifi_softap_destroy() der haertere Abriss.
|
||||||
|
* Die Pause ist billige Versicherung - gemessen ist der AP-Fall damit nicht, er steht
|
||||||
|
* als offener Punkt in der Abnahmeliste. */
|
||||||
|
if (m == MSG_STANDALONE)
|
||||||
|
tls_os_time_delay(HZ / 4); /* ~250 ms */
|
||||||
|
if (ignored_off)
|
||||||
|
printf("wifi: standalone was already off, nothing to do\r\n");
|
||||||
|
/* C1: Beim Verlassen des Standalone-Modus OHNE gespeicherte WLAN-Daten geht die
|
||||||
|
* Zustandsmaschine in den Onboarding-AP - dort liefert open_access() true, waehrend
|
||||||
|
* der AP noch unveraendert mit Name und Passwort des Nutzers steht. Wer das
|
||||||
|
* AP-Passwort kennt, koennte dann ohne Token system/settings und system/command
|
||||||
|
* benutzen. Also hier hart abreissen und mit den Vorgabedaten neu aufbauen; damit
|
||||||
|
* stimmt auch die README-Zusage wieder, der AP heisse danach "ninlight-<mac4>".
|
||||||
|
* Nicht ueber die Aktionsstruktur: execute() fuehrt start_ap VOR stop_ap aus, ein
|
||||||
|
* Buendel mit beiden Flags baute erst auf und risse dann ab. Und VOR execute(ev),
|
||||||
|
* damit dessen start_ap in die Gleichheitspruefung von start_ap() faellt, statt den
|
||||||
|
* eben gebauten AP ein zweites Mal abzureissen. */
|
||||||
|
if (rebuild_ap && ap_wanted && open_now) {
|
||||||
|
printf("wifi: left standalone without credentials, rebuilding the AP with the defaults\r\n");
|
||||||
|
destroy_ap();
|
||||||
|
start_ap();
|
||||||
|
}
|
||||||
execute(ev);
|
execute(ev);
|
||||||
|
/* Nach einem Wechsel in den Standalone-Modus: start_ap() baut den AP neu auf, wenn
|
||||||
|
* Name, Passwort oder Kanal sich geaendert haben, und tut sonst nichts. */
|
||||||
|
if (recheck_ap && ap_wanted)
|
||||||
|
start_ap();
|
||||||
execute(tick);
|
execute(tick);
|
||||||
if (got_ip_event && connected_now)
|
if (got_ip_event && connected_now)
|
||||||
nl_mdns_on_ip_up();
|
nl_mdns_on_ip_up();
|
||||||
|
/* Im eigenen AP gibt es kein Stationsereignis, an dem mDNS haengen koennte.
|
||||||
|
* Ohne diesen Aufruf findet die App ein Standalone-Geraet nicht - am
|
||||||
|
* 2026-10-01 am EVK gemessen: REST auf der AP-IP lief, ninlight-681f.local
|
||||||
|
* war weder aufloesbar noch sichtbar. */
|
||||||
|
if (m == (MSG_NETIF | NETIF_WIFI_SOFTAP_SUCCESS))
|
||||||
|
s_ap_event_seen = true;
|
||||||
|
/* Zustandsgebunden: auch ein Wechsel auf einen unveraendert stehenden AP loest es aus.
|
||||||
|
* Mehrfaches Rufen waere harmlos, s_ap_mdns_done spart es trotzdem. Faellt der Zustand
|
||||||
|
* weg, gilt die Bekanntmachung beim naechsten Eintritt neu. */
|
||||||
|
if (!standalone_now) {
|
||||||
|
s_ap_mdns_done = false;
|
||||||
|
s_ap_mdns_retry_at = now; /* beim Eintritt sofort anstossen */
|
||||||
|
} else if (s_ap_live && s_ap_event_seen && !s_ap_mdns_done) {
|
||||||
|
/* I5: Erst der Erfolg beendet die Sache. Wer den blossen Versuch als erledigt
|
||||||
|
* verbucht, laesst die Lampe fuer die ganze Standzeit des AP unauffindbar,
|
||||||
|
* sobald announce() einmal "no usable netif yet" gemeldet hat. Deshalb immer
|
||||||
|
* erst anstossen und dann den gemeldeten Zustand abfragen - nl_mdns_service_up()
|
||||||
|
* ist erst wahr, wenn die Bekanntmachung im tcpip-Thread auch gelaufen ist. */
|
||||||
|
if (due_now(now, s_ap_mdns_retry_at)) {
|
||||||
|
s_ap_mdns_retry_at = now + NL_AP_MDNS_RETRY_MS;
|
||||||
|
nl_mdns_on_ip_up();
|
||||||
|
} else
|
||||||
|
s_ap_mdns_done = nl_mdns_service_up();
|
||||||
|
}
|
||||||
|
|
||||||
/* M3: retry a softap create that failed, as long as the SM still wants the AP up. */
|
/* M3: retry a softap create that failed, as long as the SM still wants the AP up. */
|
||||||
if (s_ap_create_pending && ap_wanted && due_now(now, s_ap_create_retry_at)) {
|
if (s_ap_create_pending && ap_wanted && due_now(now, s_ap_create_retry_at)) {
|
||||||
@@ -237,7 +470,8 @@ static void net_task(void *arg)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void nl_wifi_start(const char *ssid, const char *pass, const char *ap_pass)
|
void nl_wifi_start(const char *ssid, const char *pass, const char *ap_pass,
|
||||||
|
const char *ap_ssid, uint8_t ap_channel, bool standalone, bool pulse)
|
||||||
{
|
{
|
||||||
nl_wifi_actions_t a;
|
nl_wifi_actions_t a;
|
||||||
struct netif *nif;
|
struct netif *nif;
|
||||||
@@ -248,7 +482,11 @@ void nl_wifi_start(const char *ssid, const char *pass, const char *ap_pass)
|
|||||||
nl_names_hostname(s_mac, s_hostname, sizeof s_hostname);
|
nl_names_hostname(s_mac, s_hostname, sizeof s_hostname);
|
||||||
copy(s_ssid, ssid, sizeof s_ssid);
|
copy(s_ssid, ssid, sizeof s_ssid);
|
||||||
copy(s_pass, pass, sizeof s_pass);
|
copy(s_pass, pass, sizeof s_pass);
|
||||||
copy(s_ap_pass, ap_pass, sizeof s_ap_pass);
|
/* Wie in nl_wifi_request_standalone(): NULL ist hier zwar von keinem Aufrufer zu
|
||||||
|
* erwarten, aber copy() wuerde darueber stolpern. */
|
||||||
|
copy(s_ap_pass, ap_pass && ap_pass[0] ? ap_pass : NINLIGHT_AP_PASSWORD, sizeof s_ap_pass);
|
||||||
|
copy(s_ap_ssid, ap_ssid ? ap_ssid : "", sizeof s_ap_ssid); /* leer = Hostname */
|
||||||
|
s_ap_channel = ap_channel ? ap_channel : 6;
|
||||||
|
|
||||||
/* The SDK would otherwise reconnect on its own with credentials from its parameter area. */
|
/* The SDK would otherwise reconnect on its own with credentials from its parameter area. */
|
||||||
nl_wifi_disable_sdk_auto_connect();
|
nl_wifi_disable_sdk_auto_connect();
|
||||||
@@ -256,8 +494,7 @@ void nl_wifi_start(const char *ssid, const char *pass, const char *ap_pass)
|
|||||||
/* C2: the SDK already brought up its own softap (e.g. from NETIF_IP_NET_UP before
|
/* C2: the SDK already brought up its own softap (e.g. from NETIF_IP_NET_UP before
|
||||||
* we got a chance to run) - tear it down, our state machine owns AP lifecycle. */
|
* we got a chance to run) - tear it down, our state machine owns AP lifecycle. */
|
||||||
printf("wifi: destroying SDK-created softap left over from boot\r\n");
|
printf("wifi: destroying SDK-created softap left over from boot\r\n");
|
||||||
tls_wifi_softap_destroy();
|
destroy_ap();
|
||||||
clear_softap_param();
|
|
||||||
}
|
}
|
||||||
nif = tls_get_netif();
|
nif = tls_get_netif();
|
||||||
if (nif)
|
if (nif)
|
||||||
@@ -268,10 +505,11 @@ void nl_wifi_start(const char *ssid, const char *pass, const char *ap_pass)
|
|||||||
tls_netif_add_status_event(netif_event);
|
tls_netif_add_status_event(netif_event);
|
||||||
|
|
||||||
lock();
|
lock();
|
||||||
a = nl_wifi_sm_start(&s_sm, s_ssid[0] != '\0', nl_app_now_ms());
|
a = nl_wifi_sm_start(&s_sm, s_ssid[0] != '\0', standalone, pulse, nl_app_now_ms());
|
||||||
unlock();
|
unlock();
|
||||||
s_started = true;
|
s_started = true;
|
||||||
printf("wifi: %s, hostname %s\r\n", s_ssid[0] ? "station" : "onboarding AP", s_hostname);
|
printf("wifi: %s, hostname %s\r\n",
|
||||||
|
standalone ? "standalone AP" : (s_ssid[0] ? "station" : "onboarding AP"), s_hostname);
|
||||||
execute(a);
|
execute(a);
|
||||||
tls_os_task_create(NULL, "nl_net", net_task, NULL, (u8 *)s_stack, sizeof(s_stack), NL_NET_PRIO, 0);
|
tls_os_task_create(NULL, "nl_net", net_task, NULL, (u8 *)s_stack, sizeof(s_stack), NL_NET_PRIO, 0);
|
||||||
}
|
}
|
||||||
@@ -294,6 +532,50 @@ int nl_wifi_request_onboard(const char *ssid, const char *pass)
|
|||||||
return tls_os_queue_send(s_queue, (void *)MSG_ONBOARD, 0) == TLS_OS_SUCCESS ? 0 : -1;
|
return tls_os_queue_send(s_queue, (void *)MSG_ONBOARD, 0) == TLS_OS_SUCCESS ? 0 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int nl_wifi_request_standalone(bool enabled, const char *ssid, const char *pass,
|
||||||
|
uint8_t channel, bool pulse)
|
||||||
|
{
|
||||||
|
bool busy;
|
||||||
|
|
||||||
|
if (!s_started)
|
||||||
|
return -1;
|
||||||
|
lock();
|
||||||
|
/* Wie beim Onboarding: nicht auf einen laufenden Test draufsatteln, und keinen zweiten
|
||||||
|
* Wunsch annehmen, solange der erste noch in der Warteschlange steht - er ueberschriebe
|
||||||
|
* dessen Werte, die Antwort haette aber schon 202 gesagt. */
|
||||||
|
busy = s_sm.state == NL_WIFI_ST_ONBOARD_TEST || s_req_pending;
|
||||||
|
if (!busy) {
|
||||||
|
copy(s_req_ap_ssid, ssid ? ssid : "", sizeof s_req_ap_ssid);
|
||||||
|
copy(s_req_ap_pass, pass ? pass : "", sizeof s_req_ap_pass);
|
||||||
|
s_req_ap_channel = channel;
|
||||||
|
s_req_enabled = enabled;
|
||||||
|
s_req_pulse = pulse;
|
||||||
|
s_req_pending = true;
|
||||||
|
}
|
||||||
|
unlock();
|
||||||
|
if (busy)
|
||||||
|
return -1;
|
||||||
|
if (tls_os_queue_send(s_queue, (void *)MSG_STANDALONE, 0) != TLS_OS_SUCCESS) {
|
||||||
|
lock();
|
||||||
|
s_req_pending = false;
|
||||||
|
unlock();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nl_wifi_is_standalone(void)
|
||||||
|
{
|
||||||
|
bool r;
|
||||||
|
|
||||||
|
if (!s_started)
|
||||||
|
return false;
|
||||||
|
lock();
|
||||||
|
r = s_sm.state == NL_WIFI_ST_STANDALONE;
|
||||||
|
unlock();
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
bool nl_wifi_open_access(void)
|
bool nl_wifi_open_access(void)
|
||||||
{
|
{
|
||||||
bool r;
|
bool r;
|
||||||
@@ -316,6 +598,15 @@ void nl_wifi_status(nl_wifi_status_t *out)
|
|||||||
lock();
|
lock();
|
||||||
out->mode = nl_wifi_sm_mode(&s_sm);
|
out->mode = nl_wifi_sm_mode(&s_sm);
|
||||||
out->error = nl_wifi_error_name(s_sm.error);
|
out->error = nl_wifi_error_name(s_sm.error);
|
||||||
|
/* I4: Im eigenen AP (Standalone oder Onboarding) ist die Stations-SSID nicht der Name
|
||||||
|
* des Netzes, auf dem die Lampe antwortet - und ein Heimnetzname hat in einer
|
||||||
|
* tokenfrei lesbaren Antwort ohnehin nichts verloren. Gemeldet wird der WIRKSAME
|
||||||
|
* AP-Name; damit wird auch ein Rueckfall auf andere AP-Daten ueber die API sichtbar,
|
||||||
|
* ohne ein neues Feld. "apsta" bleibt bei der Station: dort ist sie die Hauptsache. */
|
||||||
|
if (strcmp(nl_wifi_sm_mode(&s_sm), "ap") == 0)
|
||||||
|
copy(out->ssid, s_ap_live_ssid[0] ? s_ap_live_ssid : (s_ap_ssid[0] ? s_ap_ssid : s_hostname),
|
||||||
|
sizeof out->ssid);
|
||||||
|
else
|
||||||
copy(out->ssid, s_ssid, sizeof out->ssid);
|
copy(out->ssid, s_ssid, sizeof out->ssid);
|
||||||
ap_up = s_sm.ap_up || s_sm.state == NL_WIFI_ST_AP_ONBOARDING;
|
ap_up = s_sm.ap_up || s_sm.state == NL_WIFI_ST_AP_ONBOARDING;
|
||||||
unlock();
|
unlock();
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ void nl_app_request_factory_reset(void);
|
|||||||
/* Stores new station credentials (from a successful onboarding test) and persists them. */
|
/* Stores new station credentials (from a successful onboarding test) and persists them. */
|
||||||
int nl_app_set_wifi_credentials(const char *ssid, const char *pass);
|
int nl_app_set_wifi_credentials(const char *ssid, const char *pass);
|
||||||
|
|
||||||
|
/* Loescht den Standalone-Modus dauerhaft (gelungenes Onboarding aus dem eigenen AP heraus).
|
||||||
|
* Name, Passwort und Kanal des AP bleiben stehen - sie zaehlen wieder, sobald der Modus
|
||||||
|
* erneut eingeschaltet wird. */
|
||||||
|
int nl_app_clear_standalone(void);
|
||||||
|
|
||||||
/* Clears the power-cycle counter ahead of the regular 5 s deadline (spec 5.5), idempotent.
|
/* Clears the power-cycle counter ahead of the regular 5 s deadline (spec 5.5), idempotent.
|
||||||
* Used by the boot recovery path once it knows this boot is a recovery boot: a successful
|
* Used by the boot recovery path once it knows this boot is a recovery boot: a successful
|
||||||
* recovery never returns to the app task, so without this the counter would keep growing
|
* recovery never returns to the app task, so without this the counter would keep growing
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ static int save_settings(const nl_settings_t *s)
|
|||||||
static int wifi_configure(const char *ssid, const char *pass) { return nl_wifi_request_onboard(ssid, pass); }
|
static int wifi_configure(const char *ssid, const char *pass) { return nl_wifi_request_onboard(ssid, pass); }
|
||||||
static bool open_access(void) { return nl_wifi_open_access(); }
|
static bool open_access(void) { return nl_wifi_open_access(); }
|
||||||
|
|
||||||
|
/* Der Treiber nimmt den Wunsch nur entgegen; wirksam wird er im Netz-Task, nachdem die
|
||||||
|
* Antwort raus ist. 0 = uebernommen, sonst 503. */
|
||||||
|
static int standalone_configure(bool enabled, const char *ssid, const char *pass, uint8_t channel,
|
||||||
|
bool pulse)
|
||||||
|
{
|
||||||
|
return nl_wifi_request_standalone(enabled, ssid, pass, channel, pulse);
|
||||||
|
}
|
||||||
|
|
||||||
static void get_info(nl_sys_info_t *out)
|
static void get_info(nl_sys_info_t *out)
|
||||||
{
|
{
|
||||||
nl_wifi_status_t w;
|
nl_wifi_status_t w;
|
||||||
@@ -97,6 +105,7 @@ static void get_info(nl_sys_info_t *out)
|
|||||||
nl_app_unlock();
|
nl_app_unlock();
|
||||||
/* Ausserhalb der Sperre: nl_sacn haelt den Wert selbst, nl_dmx ist nicht beteiligt. */
|
/* Ausserhalb der Sperre: nl_sacn haelt den Wert selbst, nl_dmx ist nicht beteiligt. */
|
||||||
out->sacn_joined = nl_sacn_joined();
|
out->sacn_joined = nl_sacn_joined();
|
||||||
|
out->sacn_running = nl_sacn_running();
|
||||||
nl_wifi_status(&w);
|
nl_wifi_status(&w);
|
||||||
out->wifi_mode = w.mode;
|
out->wifi_mode = w.mode;
|
||||||
memcpy(out->ssid, w.ssid, sizeof out->ssid);
|
memcpy(out->ssid, w.ssid, sizeof out->ssid);
|
||||||
@@ -107,6 +116,7 @@ static void get_info(nl_sys_info_t *out)
|
|||||||
out->uptime_s = nl_app_now_ms() / 1000u;
|
out->uptime_s = nl_app_now_ms() / 1000u;
|
||||||
out->free_heap = tls_mem_get_avail_heapsize();
|
out->free_heap = tls_mem_get_avail_heapsize();
|
||||||
out->last_ota_error = nl_ota_last_error();
|
out->last_ota_error = nl_ota_last_error();
|
||||||
|
out->wifi_standalone = nl_wifi_is_standalone();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void factory_reset(void) { nl_app_request_factory_reset(); }
|
static void factory_reset(void) { nl_app_request_factory_reset(); }
|
||||||
@@ -130,12 +140,17 @@ const nl_api_ctx_t *nl_app_api_ctx(void)
|
|||||||
cJSON_InitHooks(&hooks);
|
cJSON_InitHooks(&hooks);
|
||||||
ctx.board = nl_board_get();
|
ctx.board = nl_board_get();
|
||||||
ctx.fw_version = NINLIGHT_FW_VERSION;
|
ctx.fw_version = NINLIGHT_FW_VERSION;
|
||||||
|
/* Zeigerziel ist ein statischer Puffer im Treiber und gilt ab nl_wifi_start(), das im
|
||||||
|
* Startpfad vor dem HTTP-Server laeuft. Ohne diese Zeile dereferenziert der
|
||||||
|
* Einstellungs-Handler NULL, wenn kein eigener AP-Name gesetzt ist. */
|
||||||
|
ctx.hostname = nl_wifi_hostname();
|
||||||
ctx.build_date = NINLIGHT_BUILD_DATE;
|
ctx.build_date = NINLIGHT_BUILD_DATE;
|
||||||
ctx.get_state = get_state;
|
ctx.get_state = get_state;
|
||||||
ctx.apply = apply;
|
ctx.apply = apply;
|
||||||
ctx.get_settings = get_settings;
|
ctx.get_settings = get_settings;
|
||||||
ctx.save_settings = save_settings;
|
ctx.save_settings = save_settings;
|
||||||
ctx.wifi_configure = wifi_configure;
|
ctx.wifi_configure = wifi_configure;
|
||||||
|
ctx.standalone_configure = standalone_configure;
|
||||||
ctx.open_access = open_access;
|
ctx.open_access = open_access;
|
||||||
ctx.get_info = get_info;
|
ctx.get_info = get_info;
|
||||||
ctx.factory_reset = factory_reset;
|
ctx.factory_reset = factory_reset;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "fake_api_ctx.h"
|
#include "fake_api_ctx.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "light/nl_boot_state.h"
|
#include "light/nl_boot_state.h"
|
||||||
@@ -12,6 +13,10 @@ nl_ctrl_err_t fake_apply_result;
|
|||||||
int fake_apply_calls, fake_save_calls, fake_save_result;
|
int fake_apply_calls, fake_save_calls, fake_save_result;
|
||||||
int fake_wifi_calls, fake_wifi_result;
|
int fake_wifi_calls, fake_wifi_result;
|
||||||
char fake_wifi_ssid[NL_SSID_LEN], fake_wifi_pass[NL_PASS_LEN];
|
char fake_wifi_ssid[NL_SSID_LEN], fake_wifi_pass[NL_PASS_LEN];
|
||||||
|
int fake_standalone_calls, fake_standalone_result;
|
||||||
|
bool fake_standalone_enabled, fake_standalone_pulse;
|
||||||
|
char fake_standalone_ssid[NL_SSID_LEN], fake_standalone_pass[NL_PASS_LEN];
|
||||||
|
uint8_t fake_standalone_channel;
|
||||||
int fake_factory_calls, fake_ping_calls, fake_fw_result;
|
int fake_factory_calls, fake_ping_calls, fake_fw_result;
|
||||||
char fake_fw_url[256];
|
char fake_fw_url[256];
|
||||||
bool fake_open;
|
bool fake_open;
|
||||||
@@ -49,6 +54,18 @@ static int wifi_configure(const char *ssid, const char *pass)
|
|||||||
return fake_wifi_result;
|
return fake_wifi_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int fake_standalone_configure(bool enabled, const char *ssid, const char *pass,
|
||||||
|
uint8_t channel, bool pulse)
|
||||||
|
{
|
||||||
|
fake_standalone_calls++;
|
||||||
|
fake_standalone_enabled = enabled;
|
||||||
|
fake_standalone_pulse = pulse;
|
||||||
|
fake_standalone_channel = channel;
|
||||||
|
snprintf(fake_standalone_ssid, sizeof fake_standalone_ssid, "%s", ssid ? ssid : "");
|
||||||
|
snprintf(fake_standalone_pass, sizeof fake_standalone_pass, "%s", pass ? pass : "");
|
||||||
|
return fake_standalone_result;
|
||||||
|
}
|
||||||
|
|
||||||
static bool open_access(void) { return fake_open; }
|
static bool open_access(void) { return fake_open; }
|
||||||
static void get_info(nl_sys_info_t *out) { *out = fake_info; }
|
static void get_info(nl_sys_info_t *out) { *out = fake_info; }
|
||||||
static void factory_reset(void) { fake_factory_calls++; }
|
static void factory_reset(void) { fake_factory_calls++; }
|
||||||
@@ -83,6 +100,10 @@ void fake_api_reset(const char *board_id)
|
|||||||
fake_apply_calls = fake_save_calls = fake_save_result = 0;
|
fake_apply_calls = fake_save_calls = fake_save_result = 0;
|
||||||
fake_wifi_calls = fake_wifi_result = 0;
|
fake_wifi_calls = fake_wifi_result = 0;
|
||||||
fake_wifi_ssid[0] = fake_wifi_pass[0] = '\0';
|
fake_wifi_ssid[0] = fake_wifi_pass[0] = '\0';
|
||||||
|
fake_standalone_calls = fake_standalone_result = 0;
|
||||||
|
fake_standalone_enabled = fake_standalone_pulse = false;
|
||||||
|
fake_standalone_ssid[0] = fake_standalone_pass[0] = '\0';
|
||||||
|
fake_standalone_channel = 0;
|
||||||
fake_factory_calls = fake_ping_calls = 0;
|
fake_factory_calls = fake_ping_calls = 0;
|
||||||
fake_fw_result = -1;
|
fake_fw_result = -1;
|
||||||
fake_fw_url[0] = '\0';
|
fake_fw_url[0] = '\0';
|
||||||
@@ -96,6 +117,8 @@ void fake_api_reset(const char *board_id)
|
|||||||
fake_ctx.get_settings = get_settings;
|
fake_ctx.get_settings = get_settings;
|
||||||
fake_ctx.save_settings = save_settings;
|
fake_ctx.save_settings = save_settings;
|
||||||
fake_ctx.wifi_configure = wifi_configure;
|
fake_ctx.wifi_configure = wifi_configure;
|
||||||
|
fake_ctx.standalone_configure = fake_standalone_configure;
|
||||||
|
fake_ctx.hostname = "ninlight-681f";
|
||||||
fake_ctx.open_access = open_access;
|
fake_ctx.open_access = open_access;
|
||||||
fake_ctx.get_info = get_info;
|
fake_ctx.get_info = get_info;
|
||||||
fake_ctx.factory_reset = factory_reset;
|
fake_ctx.factory_reset = factory_reset;
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ extern nl_ctrl_err_t fake_apply_result;
|
|||||||
extern int fake_apply_calls, fake_save_calls, fake_save_result;
|
extern int fake_apply_calls, fake_save_calls, fake_save_result;
|
||||||
extern int fake_wifi_calls, fake_wifi_result;
|
extern int fake_wifi_calls, fake_wifi_result;
|
||||||
extern char fake_wifi_ssid[NL_SSID_LEN], fake_wifi_pass[NL_PASS_LEN];
|
extern char fake_wifi_ssid[NL_SSID_LEN], fake_wifi_pass[NL_PASS_LEN];
|
||||||
|
extern int fake_standalone_calls, fake_standalone_result;
|
||||||
|
extern bool fake_standalone_enabled, fake_standalone_pulse;
|
||||||
|
extern char fake_standalone_ssid[NL_SSID_LEN], fake_standalone_pass[NL_PASS_LEN];
|
||||||
|
extern uint8_t fake_standalone_channel;
|
||||||
extern int fake_factory_calls, fake_ping_calls, fake_fw_result;
|
extern int fake_factory_calls, fake_ping_calls, fake_fw_result;
|
||||||
extern char fake_fw_url[256];
|
extern char fake_fw_url[256];
|
||||||
extern bool fake_open;
|
extern bool fake_open;
|
||||||
|
|||||||
+286
@@ -466,12 +466,27 @@ static void test_info_exposes_sacn_state(void)
|
|||||||
{
|
{
|
||||||
fake_info.sacn_active = true;
|
fake_info.sacn_active = true;
|
||||||
fake_info.sacn_joined = true;
|
fake_info.sacn_joined = true;
|
||||||
|
fake_info.sacn_running = true;
|
||||||
fake_settings.sacn_universe = 5;
|
fake_settings.sacn_universe = 5;
|
||||||
call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL);
|
call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL);
|
||||||
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
||||||
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("active")));
|
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("active")));
|
||||||
TEST_ASSERT_EQUAL_INT(5, (int)cJSON_GetNumberValue(sacn_field("universe")));
|
TEST_ASSERT_EQUAL_INT(5, (int)cJSON_GetNumberValue(sacn_field("universe")));
|
||||||
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("multicast")));
|
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("multicast")));
|
||||||
|
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("running")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ein toter Empfangs-Task sah von aussen genauso aus wie "es sendet gerade niemand":
|
||||||
|
* beides meldete nur active=false. Beim Montana-Black-Shield (2026-09-26) hat das eine
|
||||||
|
* halbe Stunde Suche gekostet - "multicast" half nicht weiter, weil es den gemerkten
|
||||||
|
* Erfolg des LETZTEN Beitritts zeigt und auch dann true bleibt, wenn der Socket laengst
|
||||||
|
* zu ist. */
|
||||||
|
static void test_info_reports_a_dead_sacn_task(void)
|
||||||
|
{
|
||||||
|
fake_info.sacn_running = false;
|
||||||
|
call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL);
|
||||||
|
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
||||||
|
TEST_ASSERT_FALSE(cJSON_IsTrue(sacn_field("running")));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- system/command --- */
|
/* --- system/command --- */
|
||||||
@@ -574,6 +589,260 @@ static void test_put_sacn_layout_and_start_slot_together(void)
|
|||||||
TEST_ASSERT_EQUAL_UINT16(510, fake_settings.sacn_start_slot);
|
TEST_ASSERT_EQUAL_UINT16(510, fake_settings.sacn_start_slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- standalone --- */
|
||||||
|
|
||||||
|
static void test_standalone_requires_a_password(void)
|
||||||
|
{
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"standalone\",\"enabled\":true}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(400, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("password required", error_text());
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, fake_standalone_calls);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, fake_save_calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ein gespeichertes Passwort genuegt - man soll den Kanal aendern koennen, ohne das
|
||||||
|
* Passwort erneut zu schicken. */
|
||||||
|
static void test_standalone_keeps_a_stored_password(void)
|
||||||
|
{
|
||||||
|
strcpy(fake_settings.ap_pass, "geheim123");
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"channel\":11}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(202, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_INT(11, fake_standalone_channel);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("geheim123", fake_standalone_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_rejects_a_short_password(void)
|
||||||
|
{
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"password\":\"1234567\"}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(400, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("invalid password", error_text());
|
||||||
|
/* Die Fehlermeldung nennt das Feld, nie den Wert. */
|
||||||
|
TEST_ASSERT_NULL(strstr(resp.body, "1234567"));
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, fake_standalone_calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_rejects_a_channel_outside_the_band(void)
|
||||||
|
{
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"password\":\"geheim123\",\"channel\":14}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(400, resp.status);
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"password\":\"geheim123\",\"channel\":0}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(400, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, fake_standalone_calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_accepts_a_full_request(void)
|
||||||
|
{
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"password\":\"geheim123\","
|
||||||
|
"\"ssid\":\"Wohnzimmerlampe\",\"channel\":6,\"pulse\":false}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(202, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("standalone", cJSON_GetStringValue(field("status")));
|
||||||
|
TEST_ASSERT_NULL(strstr(resp.body, "geheim123"));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, fake_standalone_calls);
|
||||||
|
TEST_ASSERT_TRUE(fake_standalone_enabled);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Wohnzimmerlampe", fake_standalone_ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("geheim123", fake_standalone_pass);
|
||||||
|
TEST_ASSERT_EQUAL_INT(6, fake_standalone_channel);
|
||||||
|
TEST_ASSERT_FALSE(fake_standalone_pulse);
|
||||||
|
/* Gespeichert wird auch: sonst ist der Modus nach dem Neustart weg. */
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, fake_save_calls);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1, fake_settings.standalone);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("geheim123", fake_settings.ap_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Abschalten braucht kein Passwort. */
|
||||||
|
static void test_standalone_can_be_switched_off(void)
|
||||||
|
{
|
||||||
|
fake_settings.standalone = 1;
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"standalone\",\"enabled\":false}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(202, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, fake_standalone_calls);
|
||||||
|
TEST_ASSERT_FALSE(fake_standalone_enabled);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, fake_settings.standalone);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_busy_radio_answers_503_and_rolls_back(void)
|
||||||
|
{
|
||||||
|
strcpy(fake_settings.ap_pass, "geheim123");
|
||||||
|
fake_settings.standalone = 0;
|
||||||
|
fake_standalone_result = -1;
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"standalone\",\"enabled\":true}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(503, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("wifi busy", error_text());
|
||||||
|
TEST_ASSERT_NULL(strstr(resp.body, "geheim123"));
|
||||||
|
/* "nicht uebernommen" gilt auch im Flash. */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, fake_settings.standalone);
|
||||||
|
TEST_ASSERT_EQUAL_INT(2, fake_save_calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_rollback_restores_every_changed_field(void)
|
||||||
|
{
|
||||||
|
strcpy(fake_settings.ap_pass, "altpass123");
|
||||||
|
strcpy(fake_settings.ap_ssid, "alt");
|
||||||
|
fake_settings.ap_channel = 3;
|
||||||
|
fake_settings.standalone_pulse = 1;
|
||||||
|
fake_standalone_result = -1;
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"password\":\"neupass123\","
|
||||||
|
"\"ssid\":\"neu\",\"channel\":11,\"pulse\":false}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(503, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("altpass123", fake_settings.ap_pass);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("alt", fake_settings.ap_ssid);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(3, fake_settings.ap_channel);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1, fake_settings.standalone_pulse);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_failed_save_answers_500_without_switching(void)
|
||||||
|
{
|
||||||
|
strcpy(fake_settings.ap_pass, "geheim123");
|
||||||
|
fake_save_result = -1;
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"standalone\",\"enabled\":true}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(500, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, fake_standalone_calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nicht angegebene Felder behalten ihren gespeicherten Wert. */
|
||||||
|
static void test_standalone_unspecified_fields_keep_their_stored_values(void)
|
||||||
|
{
|
||||||
|
strcpy(fake_settings.ap_pass, "geheim123");
|
||||||
|
strcpy(fake_settings.ap_ssid, "Gespeichert");
|
||||||
|
fake_settings.ap_channel = 11;
|
||||||
|
fake_settings.standalone_pulse = 0;
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"standalone\",\"enabled\":true}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(202, resp.status);
|
||||||
|
TEST_ASSERT_NULL(strstr(resp.body, "geheim123"));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Gespeichert", fake_standalone_ssid);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(11, fake_standalone_channel);
|
||||||
|
TEST_ASSERT_FALSE(fake_standalone_pulse);
|
||||||
|
|
||||||
|
fake_settings.standalone_pulse = 1;
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"standalone\",\"enabled\":true}");
|
||||||
|
TEST_ASSERT_TRUE(fake_standalone_pulse);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_length_limits(void)
|
||||||
|
{
|
||||||
|
char body[256], pw[80], ss[48];
|
||||||
|
|
||||||
|
/* 63 ist die Obergrenze, nicht 64: eine WPA2-ASCII-Passphrase ist hoechstens 63 Zeichen
|
||||||
|
* lang, und ein vom Funkteil abgelehnter AP-Aufbau waere hier eine Sackgasse. */
|
||||||
|
memset(pw, 'p', 63); pw[63] = '\0';
|
||||||
|
snprintf(body, sizeof body, "{\"type\":\"standalone\",\"enabled\":true,\"password\":\"%s\"}", pw);
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, body);
|
||||||
|
TEST_ASSERT_EQUAL_INT(202, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_STRING(pw, fake_standalone_pass);
|
||||||
|
memset(pw, 'p', 64); pw[64] = '\0';
|
||||||
|
snprintf(body, sizeof body, "{\"type\":\"standalone\",\"enabled\":true,\"password\":\"%s\"}", pw);
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, body);
|
||||||
|
TEST_ASSERT_EQUAL_INT(400, resp.status);
|
||||||
|
TEST_ASSERT_NULL(strstr(resp.body, pw));
|
||||||
|
memset(pw, 'p', 65); pw[65] = '\0';
|
||||||
|
snprintf(body, sizeof body, "{\"type\":\"standalone\",\"enabled\":true,\"password\":\"%s\"}", pw);
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, body);
|
||||||
|
TEST_ASSERT_EQUAL_INT(400, resp.status);
|
||||||
|
TEST_ASSERT_NULL(strstr(resp.body, pw));
|
||||||
|
|
||||||
|
memset(ss, 's', 32); ss[32] = '\0';
|
||||||
|
snprintf(body, sizeof body,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"password\":\"geheim123\",\"ssid\":\"%s\"}", ss);
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, body);
|
||||||
|
TEST_ASSERT_EQUAL_INT(202, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_STRING(ss, fake_standalone_ssid);
|
||||||
|
memset(ss, 's', 33); ss[33] = '\0';
|
||||||
|
snprintf(body, sizeof body,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"password\":\"geheim123\",\"ssid\":\"%s\"}", ss);
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, body);
|
||||||
|
TEST_ASSERT_EQUAL_INT(400, resp.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_accepts_the_band_edges(void)
|
||||||
|
{
|
||||||
|
strcpy(fake_settings.ap_pass, "geheim123");
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"channel\":13}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(202, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(13, fake_standalone_channel);
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
|
||||||
|
"{\"type\":\"standalone\",\"enabled\":true,\"channel\":1}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(202, resp.status);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1, fake_standalone_channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_rejects_bad_types_of_the_other_fields(void)
|
||||||
|
{
|
||||||
|
strcpy(fake_settings.ap_pass, "geheim123");
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"standalone\",\"enabled\":\"yes\"}");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("invalid enabled", error_text());
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"standalone\",\"enabled\":true,\"pulse\":1}");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("invalid pulse", error_text());
|
||||||
|
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"standalone\",\"enabled\":true,\"ssid\":\"\"}");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("invalid ssid", error_text());
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, fake_standalone_calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Das AP-Passwort darf nirgends in der Antwort auftauchen. */
|
||||||
|
static void test_settings_show_standalone_without_the_password(void)
|
||||||
|
{
|
||||||
|
cJSON *o, *sa;
|
||||||
|
fake_settings.standalone = 1;
|
||||||
|
fake_settings.ap_channel = 11;
|
||||||
|
fake_settings.standalone_pulse = 0;
|
||||||
|
strcpy(fake_settings.ap_ssid, "Wohnzimmerlampe");
|
||||||
|
strcpy(fake_settings.ap_pass, "geheim123");
|
||||||
|
call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL);
|
||||||
|
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
||||||
|
TEST_ASSERT_NULL(strstr(resp.body, "geheim123"));
|
||||||
|
o = cJSON_Parse(resp.body);
|
||||||
|
sa = cJSON_GetObjectItemCaseSensitive(o, "standalone");
|
||||||
|
TEST_ASSERT_TRUE(cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(sa, "enabled")));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Wohnzimmerlampe",
|
||||||
|
cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(sa, "ssid")));
|
||||||
|
TEST_ASSERT_EQUAL_INT(11, (int)cJSON_GetNumberValue(cJSON_GetObjectItemCaseSensitive(sa, "channel")));
|
||||||
|
TEST_ASSERT_FALSE(cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(sa, "pulse")));
|
||||||
|
cJSON_Delete(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ohne eigenen Namen zeigt die Antwort den WIRKSAMEN, nicht einen leeren String -
|
||||||
|
* sonst muesste jeder Aufrufer die Vorgabe selbst nachbilden. */
|
||||||
|
static void test_settings_show_the_effective_ap_name(void)
|
||||||
|
{
|
||||||
|
cJSON *o, *sa;
|
||||||
|
fake_settings.ap_ssid[0] = '\0';
|
||||||
|
call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL);
|
||||||
|
o = cJSON_Parse(resp.body);
|
||||||
|
sa = cJSON_GetObjectItemCaseSensitive(o, "standalone");
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ninlight-681f",
|
||||||
|
cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(sa, "ssid")));
|
||||||
|
/* Kanal 0 im Datensatz heisst 6. */
|
||||||
|
TEST_ASSERT_EQUAL_INT(6, (int)cJSON_GetNumberValue(cJSON_GetObjectItemCaseSensitive(sa, "channel")));
|
||||||
|
cJSON_Delete(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "ap" allein reicht nicht: denselben Wert meldet der Onboarding-AP. Ohne dieses Feld
|
||||||
|
* boete die App einer fertig eingerichteten Lampe ein Onboarding an. */
|
||||||
|
static void test_info_tells_standalone_from_onboarding(void)
|
||||||
|
{
|
||||||
|
cJSON *o, *w;
|
||||||
|
fake_info.wifi_mode = "ap";
|
||||||
|
fake_info.wifi_standalone = true;
|
||||||
|
call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL);
|
||||||
|
o = cJSON_Parse(resp.body);
|
||||||
|
w = cJSON_GetObjectItemCaseSensitive(o, "wifi");
|
||||||
|
TEST_ASSERT_TRUE(cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(w, "standalone")));
|
||||||
|
cJSON_Delete(o);
|
||||||
|
|
||||||
|
fake_info.wifi_standalone = false;
|
||||||
|
call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL);
|
||||||
|
o = cJSON_Parse(resp.body);
|
||||||
|
w = cJSON_GetObjectItemCaseSensitive(o, "wifi");
|
||||||
|
TEST_ASSERT_TRUE(cJSON_IsFalse(cJSON_GetObjectItemCaseSensitive(w, "standalone")));
|
||||||
|
cJSON_Delete(o);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
@@ -610,6 +879,23 @@ int main(void)
|
|||||||
RUN_TEST(test_put_sacn_start_slot_must_leave_room_for_all_slots);
|
RUN_TEST(test_put_sacn_start_slot_must_leave_room_for_all_slots);
|
||||||
RUN_TEST(test_put_sacn_start_slot_limit_follows_the_board);
|
RUN_TEST(test_put_sacn_start_slot_limit_follows_the_board);
|
||||||
RUN_TEST(test_info_exposes_sacn_state);
|
RUN_TEST(test_info_exposes_sacn_state);
|
||||||
|
RUN_TEST(test_info_reports_a_dead_sacn_task);
|
||||||
RUN_TEST(test_commands);
|
RUN_TEST(test_commands);
|
||||||
|
RUN_TEST(test_standalone_requires_a_password);
|
||||||
|
RUN_TEST(test_standalone_keeps_a_stored_password);
|
||||||
|
RUN_TEST(test_standalone_rejects_a_short_password);
|
||||||
|
RUN_TEST(test_standalone_rejects_a_channel_outside_the_band);
|
||||||
|
RUN_TEST(test_standalone_accepts_a_full_request);
|
||||||
|
RUN_TEST(test_standalone_can_be_switched_off);
|
||||||
|
RUN_TEST(test_standalone_busy_radio_answers_503_and_rolls_back);
|
||||||
|
RUN_TEST(test_standalone_rollback_restores_every_changed_field);
|
||||||
|
RUN_TEST(test_standalone_failed_save_answers_500_without_switching);
|
||||||
|
RUN_TEST(test_standalone_unspecified_fields_keep_their_stored_values);
|
||||||
|
RUN_TEST(test_standalone_length_limits);
|
||||||
|
RUN_TEST(test_standalone_accepts_the_band_edges);
|
||||||
|
RUN_TEST(test_standalone_rejects_bad_types_of_the_other_fields);
|
||||||
|
RUN_TEST(test_settings_show_standalone_without_the_password);
|
||||||
|
RUN_TEST(test_settings_show_the_effective_ap_name);
|
||||||
|
RUN_TEST(test_info_tells_standalone_from_onboarding);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,6 +275,55 @@ static void test_record_without_layout_gets_the_default(void)
|
|||||||
TEST_ASSERT_EQUAL_UINT8(1, out.sacn_layout);
|
TEST_ASSERT_EQUAL_UINT8(1, out.sacn_layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_standalone_defaults(void)
|
||||||
|
{
|
||||||
|
nl_settings_t s;
|
||||||
|
TEST_ASSERT_FALSE(nl_settings_load(&s));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, s.standalone);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, s.ap_channel); /* 0 heisst "Vorgabe 6" */
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1, s.standalone_pulse);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", s.ap_ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("", s.ap_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_roundtrip(void)
|
||||||
|
{
|
||||||
|
nl_settings_t in = sample("ap"), out;
|
||||||
|
in.standalone = 1;
|
||||||
|
in.ap_channel = 11;
|
||||||
|
in.standalone_pulse = 0;
|
||||||
|
strcpy(in.ap_ssid, "Wohnzimmerlampe");
|
||||||
|
strcpy(in.ap_pass, "geheim123");
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in));
|
||||||
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1, out.standalone);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(11, out.ap_channel);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, out.standalone_pulse);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("Wohnzimmerlampe", out.ap_ssid);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("geheim123", out.ap_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ein Datensatz aus einer Firmware VOR dem Standalone-Modus: die neuen Felder muessen
|
||||||
|
* als echte Vorgaben zurueckkommen, nicht als Null. Sonst stuende standalone_pulse nach
|
||||||
|
* dem Update auf jeder Lampe auf 0 - der Startpuls waere still abgeschaltet. */
|
||||||
|
static void test_record_without_standalone_gets_defaults(void)
|
||||||
|
{
|
||||||
|
nl_settings_t in = sample("alt"), out;
|
||||||
|
uint32_t crc;
|
||||||
|
nl_settings_save(&in);
|
||||||
|
{
|
||||||
|
uint16_t shorter = 220u; /* sizeof(nl_settings_t) vor diesem Zweig */
|
||||||
|
memset(&fake_flash_mem[NL_FL_SETTINGS_A + 12 + 217], 0, 3); /* Fuellbytes */
|
||||||
|
memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &shorter, 2);
|
||||||
|
crc = nl_crc32(0, &fake_flash_mem[NL_FL_SETTINGS_A], 12 + shorter);
|
||||||
|
memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 12 + shorter], &crc, 4);
|
||||||
|
}
|
||||||
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("alt", out.name);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(0, out.standalone);
|
||||||
|
TEST_ASSERT_EQUAL_UINT8(1, out.standalone_pulse);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_record_size_stays_aligned(void)
|
static void test_record_size_stays_aligned(void)
|
||||||
{
|
{
|
||||||
TEST_ASSERT_EQUAL_UINT(0, sizeof(nl_settings_t) % 4u);
|
TEST_ASSERT_EQUAL_UINT(0, sizeof(nl_settings_t) % 4u);
|
||||||
@@ -301,5 +350,8 @@ int main(void)
|
|||||||
RUN_TEST(test_sacn_layout_roundtrip);
|
RUN_TEST(test_sacn_layout_roundtrip);
|
||||||
RUN_TEST(test_record_without_layout_gets_the_default);
|
RUN_TEST(test_record_without_layout_gets_the_default);
|
||||||
RUN_TEST(test_record_size_stays_aligned);
|
RUN_TEST(test_record_size_stays_aligned);
|
||||||
|
RUN_TEST(test_standalone_defaults);
|
||||||
|
RUN_TEST(test_standalone_roundtrip);
|
||||||
|
RUN_TEST(test_record_without_standalone_gets_defaults);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
+284
-12
@@ -10,7 +10,7 @@ void tearDown(void) {}
|
|||||||
|
|
||||||
static void test_start_without_credentials_opens_ap(void)
|
static void test_start_without_credentials_opens_ap(void)
|
||||||
{
|
{
|
||||||
a = nl_wifi_sm_start(&sm, false, 0);
|
a = nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
TEST_ASSERT_TRUE(a.start_ap);
|
TEST_ASSERT_TRUE(a.start_ap);
|
||||||
TEST_ASSERT_TRUE(a.pulse_on);
|
TEST_ASSERT_TRUE(a.pulse_on);
|
||||||
TEST_ASSERT_FALSE(a.connect);
|
TEST_ASSERT_FALSE(a.connect);
|
||||||
@@ -21,7 +21,7 @@ static void test_start_without_credentials_opens_ap(void)
|
|||||||
|
|
||||||
static void test_start_with_credentials_connects(void)
|
static void test_start_with_credentials_connects(void)
|
||||||
{
|
{
|
||||||
a = nl_wifi_sm_start(&sm, true, 0);
|
a = nl_wifi_sm_start(&sm, true, false, false, 0);
|
||||||
TEST_ASSERT_TRUE(a.connect);
|
TEST_ASSERT_TRUE(a.connect);
|
||||||
TEST_ASSERT_FALSE(a.use_test_creds);
|
TEST_ASSERT_FALSE(a.use_test_creds);
|
||||||
TEST_ASSERT_FALSE(a.start_ap);
|
TEST_ASSERT_FALSE(a.start_ap);
|
||||||
@@ -33,7 +33,7 @@ static void test_start_with_credentials_connects(void)
|
|||||||
|
|
||||||
static void test_retry_with_backoff(void)
|
static void test_retry_with_backoff(void)
|
||||||
{
|
{
|
||||||
nl_wifi_sm_start(&sm, true, 0);
|
nl_wifi_sm_start(&sm, true, false, false, 0);
|
||||||
nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 1000);
|
nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 1000);
|
||||||
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 1999).connect);
|
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 1999).connect);
|
||||||
TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 2000).connect);
|
TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 2000).connect);
|
||||||
@@ -50,7 +50,7 @@ static void test_retry_with_backoff(void)
|
|||||||
|
|
||||||
static void test_fallback_after_five_minutes(void)
|
static void test_fallback_after_five_minutes(void)
|
||||||
{
|
{
|
||||||
nl_wifi_sm_start(&sm, true, 0);
|
nl_wifi_sm_start(&sm, true, false, false, 0);
|
||||||
nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_NOT_FOUND, 1000);
|
nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_NOT_FOUND, 1000);
|
||||||
a = nl_wifi_sm_tick(&sm, NL_WIFI_FALLBACK_MS - 1);
|
a = nl_wifi_sm_tick(&sm, NL_WIFI_FALLBACK_MS - 1);
|
||||||
TEST_ASSERT_FALSE(a.start_ap);
|
TEST_ASSERT_FALSE(a.start_ap);
|
||||||
@@ -74,7 +74,7 @@ static void test_fallback_after_five_minutes(void)
|
|||||||
|
|
||||||
static void test_disconnect_while_connected(void)
|
static void test_disconnect_while_connected(void)
|
||||||
{
|
{
|
||||||
nl_wifi_sm_start(&sm, true, 0);
|
nl_wifi_sm_start(&sm, true, false, false, 0);
|
||||||
nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 1000);
|
nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 1000);
|
||||||
nl_wifi_sm_event(&sm, NL_WIFI_EV_DISCONNECTED, NL_WIFI_ERR_NONE, 100000);
|
nl_wifi_sm_event(&sm, NL_WIFI_EV_DISCONNECTED, NL_WIFI_ERR_NONE, 100000);
|
||||||
TEST_ASSERT_EQUAL(NL_WIFI_ST_STA_CONNECTING, sm.state);
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STA_CONNECTING, sm.state);
|
||||||
@@ -86,7 +86,7 @@ static void test_disconnect_while_connected(void)
|
|||||||
|
|
||||||
static void test_onboarding_success(void)
|
static void test_onboarding_success(void)
|
||||||
{
|
{
|
||||||
nl_wifi_sm_start(&sm, false, 0);
|
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
a = nl_wifi_sm_onboard(&sm, 5000);
|
a = nl_wifi_sm_onboard(&sm, 5000);
|
||||||
TEST_ASSERT_FALSE(a.start_ap); /* already up */
|
TEST_ASSERT_FALSE(a.start_ap); /* already up */
|
||||||
TEST_ASSERT_TRUE(a.connect);
|
TEST_ASSERT_TRUE(a.connect);
|
||||||
@@ -106,7 +106,7 @@ static void test_onboarding_success(void)
|
|||||||
|
|
||||||
static void test_onboarding_failure_without_credentials(void)
|
static void test_onboarding_failure_without_credentials(void)
|
||||||
{
|
{
|
||||||
nl_wifi_sm_start(&sm, false, 0);
|
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
nl_wifi_sm_onboard(&sm, 5000);
|
nl_wifi_sm_onboard(&sm, 5000);
|
||||||
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 8000);
|
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 8000);
|
||||||
TEST_ASSERT_FALSE(a.pulse_off);
|
TEST_ASSERT_FALSE(a.pulse_off);
|
||||||
@@ -121,7 +121,7 @@ static void test_onboarding_failure_without_credentials(void)
|
|||||||
|
|
||||||
static void test_onboarding_timeout(void)
|
static void test_onboarding_timeout(void)
|
||||||
{
|
{
|
||||||
nl_wifi_sm_start(&sm, false, 0);
|
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
nl_wifi_sm_onboard(&sm, 5000);
|
nl_wifi_sm_onboard(&sm, 5000);
|
||||||
TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state);
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state);
|
||||||
nl_wifi_sm_tick(&sm, 5000 + NL_WIFI_ONBOARD_TIMEOUT_MS - 1);
|
nl_wifi_sm_tick(&sm, 5000 + NL_WIFI_ONBOARD_TIMEOUT_MS - 1);
|
||||||
@@ -136,7 +136,7 @@ static void test_onboarding_connected_event_while_ap_onboarding_is_stray(void)
|
|||||||
{
|
{
|
||||||
/* I1: fail_test() returns to AP_ONBOARDING; a late CONNECTED for the timed-out
|
/* I1: fail_test() returns to AP_ONBOARDING; a late CONNECTED for the timed-out
|
||||||
* attempt must be treated as stray and disconnected, not accepted. */
|
* attempt must be treated as stray and disconnected, not accepted. */
|
||||||
nl_wifi_sm_start(&sm, false, 0);
|
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
nl_wifi_sm_onboard(&sm, 5000);
|
nl_wifi_sm_onboard(&sm, 5000);
|
||||||
nl_wifi_sm_tick(&sm, 5000 + NL_WIFI_ONBOARD_TIMEOUT_MS); /* -> AP_ONBOARDING */
|
nl_wifi_sm_tick(&sm, 5000 + NL_WIFI_ONBOARD_TIMEOUT_MS); /* -> AP_ONBOARDING */
|
||||||
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 40000);
|
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 40000);
|
||||||
@@ -146,7 +146,7 @@ static void test_onboarding_connected_event_while_ap_onboarding_is_stray(void)
|
|||||||
|
|
||||||
static void test_onboarding_while_connected_falls_back_to_old_credentials(void)
|
static void test_onboarding_while_connected_falls_back_to_old_credentials(void)
|
||||||
{
|
{
|
||||||
nl_wifi_sm_start(&sm, true, 0);
|
nl_wifi_sm_start(&sm, true, false, false, 0);
|
||||||
nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 1000);
|
nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 1000);
|
||||||
a = nl_wifi_sm_onboard(&sm, 2000);
|
a = nl_wifi_sm_onboard(&sm, 2000);
|
||||||
TEST_ASSERT_TRUE(a.start_ap);
|
TEST_ASSERT_TRUE(a.start_ap);
|
||||||
@@ -167,7 +167,7 @@ static void test_onboard_while_test_running_disconnects_stale_attempt(void)
|
|||||||
{
|
{
|
||||||
/* M1: a repeated onboarding request while a test connect is already in flight
|
/* M1: a repeated onboarding request while a test connect is already in flight
|
||||||
* must disconnect first so stale events from the previous attempt don't count. */
|
* must disconnect first so stale events from the previous attempt don't count. */
|
||||||
nl_wifi_sm_start(&sm, false, 0);
|
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
nl_wifi_sm_onboard(&sm, 5000);
|
nl_wifi_sm_onboard(&sm, 5000);
|
||||||
TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state);
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state);
|
||||||
a = nl_wifi_sm_onboard(&sm, 6000);
|
a = nl_wifi_sm_onboard(&sm, 6000);
|
||||||
@@ -185,7 +185,7 @@ static void test_error_names(void)
|
|||||||
|
|
||||||
static void test_connect_without_any_event_retries(void)
|
static void test_connect_without_any_event_retries(void)
|
||||||
{
|
{
|
||||||
nl_wifi_sm_start(&sm, true, 0);
|
nl_wifi_sm_start(&sm, true, false, false, 0);
|
||||||
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 29999).connect);
|
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 29999).connect);
|
||||||
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 30000).connect); /* retry scheduled at 31000 */
|
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 30000).connect); /* retry scheduled at 31000 */
|
||||||
TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 31000).connect);
|
TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 31000).connect);
|
||||||
@@ -202,6 +202,257 @@ static void test_connect_without_any_event_retries(void)
|
|||||||
TEST_ASSERT_TRUE(a.connect);
|
TEST_ASSERT_TRUE(a.connect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_standalone_opens_ap_without_connecting(void)
|
||||||
|
{
|
||||||
|
a = nl_wifi_sm_start(&sm, false, true, false, 0);
|
||||||
|
TEST_ASSERT_TRUE(a.start_ap);
|
||||||
|
TEST_ASSERT_FALSE(a.connect);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("ap", nl_wifi_sm_mode(&sm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der Standalone-AP ist KEIN offener Onboarding-AP. Haengt diese Zusicherung nicht,
|
||||||
|
* koennte jeder in Funkreichweite die Lampe ohne Token flashen oder zuruecksetzen. */
|
||||||
|
static void test_standalone_is_not_open_access(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, true, false, 0);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Das Fenster, das der Ruhezustand nicht abdeckt: Onboarding-Test aus dem Standalone-Modus.
|
||||||
|
* Dann ist state == ONBOARD_TEST und have_creds == false - ohne !standalone in open_access()
|
||||||
|
* fiele der Tokenschutz bis zu 30 s lang, waehrend der passwortgeschuetzte AP oben steht. */
|
||||||
|
static void test_standalone_onboard_test_is_not_open_access(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, true, false, 0);
|
||||||
|
nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state);
|
||||||
|
TEST_ASSERT_FALSE(sm.have_creds);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gespeicherte WLAN-Daten aendern nichts: wer standalone gewaehlt hat, will keinen
|
||||||
|
* Verbindungsversuch, auch wenn von frueher noch Zugangsdaten herumliegen. */
|
||||||
|
static void test_standalone_wins_over_stored_credentials(void)
|
||||||
|
{
|
||||||
|
a = nl_wifi_sm_start(&sm, true, true, false, 0);
|
||||||
|
TEST_ASSERT_FALSE(a.connect);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kein Rueckfall und kein Wiederholungsversuch: die Maschine darf im Standalone-Modus
|
||||||
|
* nie von selbst anfangen, ein WLAN zu suchen. */
|
||||||
|
static void test_standalone_never_falls_back(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, true, true, false, 0);
|
||||||
|
a = nl_wifi_sm_tick(&sm, NL_WIFI_FALLBACK_MS + 60000);
|
||||||
|
TEST_ASSERT_FALSE(a.connect);
|
||||||
|
TEST_ASSERT_FALSE(a.start_ap);
|
||||||
|
TEST_ASSERT_FALSE(a.stop_ap);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ein verirrter Erfolg eines alten Versuchs darf die Lampe nicht aus ihrem AP holen. */
|
||||||
|
static void test_standalone_drops_a_stray_connect(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, true, true, false, 0);
|
||||||
|
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 1000);
|
||||||
|
TEST_ASSERT_TRUE(a.disconnect);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der Puls ist zeitlich begrenzt, und das ist keine Kosmetik: nl_engine_pulse() legt sich
|
||||||
|
* ueber die normale Ausgabe und uebermalt auch sACN. Am 2026-10-01 am EVK beobachtet -
|
||||||
|
* ein gueltiger sACN-Strom kam an, waehrend die Lampe weiter den Puls zeigte. Ein
|
||||||
|
* dauerhafter Puls machte sACN im Standalone-Modus unbrauchbar. */
|
||||||
|
static void test_standalone_pulse_stops_by_itself(void)
|
||||||
|
{
|
||||||
|
a = nl_wifi_sm_start(&sm, false, true, true, 0);
|
||||||
|
TEST_ASSERT_TRUE(a.pulse_on);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, NL_WIFI_STANDALONE_PULSE_MS - 1).pulse_off);
|
||||||
|
TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, NL_WIFI_STANDALONE_PULSE_MS).pulse_off);
|
||||||
|
/* und danach nicht noch einmal */
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, NL_WIFI_STANDALONE_PULSE_MS + 5000).pulse_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_standalone_without_pulse_stays_dark(void)
|
||||||
|
{
|
||||||
|
a = nl_wifi_sm_start(&sm, false, true, false, 0);
|
||||||
|
TEST_ASSERT_FALSE(a.pulse_on);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, NL_WIFI_STANDALONE_PULSE_MS).pulse_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Erfolg: die Lampe wechselt ins WLAN UND verlaesst den Standalone-Modus dauerhaft.
|
||||||
|
* Ohne clear_standalone staende sie nach dem naechsten Neustart wieder im eigenen AP. */
|
||||||
|
static void test_onboarding_from_standalone_clears_the_mode(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, true, false, 0);
|
||||||
|
nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 2000);
|
||||||
|
TEST_ASSERT_TRUE(a.save_test_creds);
|
||||||
|
TEST_ASSERT_TRUE(a.clear_standalone);
|
||||||
|
TEST_ASSERT_FALSE(sm.standalone);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STA_CONNECTED, sm.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fehlschlag: zurueck in den EIGENEN AP, nicht in den offenen Onboarding-AP. Sonst
|
||||||
|
* verloere ein Tippfehler im WLAN-Passwort den vom Nutzer gesetzten AP-Namen samt
|
||||||
|
* Passwort und entsicherte die Lampe, weil open_access() dann wieder true waere. */
|
||||||
|
static void test_failed_onboarding_returns_to_standalone(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, true, false, 0);
|
||||||
|
nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 2000);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
|
||||||
|
TEST_ASSERT_TRUE(sm.standalone);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm));
|
||||||
|
TEST_ASSERT_FALSE(a.clear_standalone);
|
||||||
|
TEST_ASSERT_FALSE(a.connect);
|
||||||
|
TEST_ASSERT_TRUE(a.disconnect);
|
||||||
|
TEST_ASSERT_TRUE(a.start_ap);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ERR_AUTH, sm.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auch der Zeitablauf des Tests fuehrt zurueck in den eigenen AP. */
|
||||||
|
static void test_onboarding_timeout_returns_to_standalone(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, true, false, 0);
|
||||||
|
nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
a = nl_wifi_sm_tick(&sm, 1000 + NL_WIFI_ONBOARD_TIMEOUT_MS);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ERR_TIMEOUT, sm.error);
|
||||||
|
TEST_ASSERT_FALSE(a.connect);
|
||||||
|
TEST_ASSERT_TRUE(a.disconnect);
|
||||||
|
TEST_ASSERT_TRUE(a.start_ap);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der bisherige Weg bleibt unberuehrt: ohne Standalone und ohne gespeicherte Daten
|
||||||
|
* landet ein gescheiterter Test weiterhin im offenen Onboarding-AP. */
|
||||||
|
static void test_failed_onboarding_without_standalone_opens_ap(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
|
nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 2000);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_AP_ONBOARDING, sm.state);
|
||||||
|
TEST_ASSERT_TRUE(nl_wifi_sm_open_access(&sm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aus dem laufenden Stationsbetrieb in den eigenen AP. */
|
||||||
|
static void test_switching_to_standalone_at_runtime(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, true, false, false, 0);
|
||||||
|
nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 1000);
|
||||||
|
a = nl_wifi_sm_standalone(&sm, true, 2000);
|
||||||
|
TEST_ASSERT_TRUE(a.disconnect);
|
||||||
|
TEST_ASSERT_TRUE(a.start_ap);
|
||||||
|
TEST_ASSERT_TRUE(a.pulse_on);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
|
||||||
|
TEST_ASSERT_TRUE(sm.standalone);
|
||||||
|
TEST_ASSERT_TRUE(nl_wifi_sm_tick(&sm, 2000 + NL_WIFI_STANDALONE_PULSE_MS).pulse_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zuruecknehmen MIT gespeicherten Zugangsdaten: ab ins WLAN, nicht in den offenen AP. */
|
||||||
|
static void test_leaving_standalone_with_credentials_connects(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, true, true, false, 0);
|
||||||
|
a = nl_wifi_sm_leave_standalone(&sm, true, 1000);
|
||||||
|
TEST_ASSERT_TRUE(a.connect);
|
||||||
|
TEST_ASSERT_FALSE(a.start_ap);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm));
|
||||||
|
TEST_ASSERT_FALSE(sm.standalone);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STA_CONNECTING, sm.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zuruecknehmen OHNE Zugangsdaten: offener Onboarding-AP, damit man wieder herankommt. */
|
||||||
|
static void test_leaving_standalone_without_credentials_opens_onboarding(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, true, false, 0);
|
||||||
|
a = nl_wifi_sm_leave_standalone(&sm, false, 1000);
|
||||||
|
TEST_ASSERT_TRUE(a.start_ap);
|
||||||
|
TEST_ASSERT_TRUE(a.pulse_on);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_AP_ONBOARDING, sm.state);
|
||||||
|
TEST_ASSERT_TRUE(nl_wifi_sm_open_access(&sm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Laeuft beim Start eines Onboarding-Tests noch der Startpuls, raeumt onboard() ihn ab:
|
||||||
|
* pulse_off sofort, danach kein verwaister pulse_off mehr -- weder nach einem
|
||||||
|
* gescheiterten noch nach einem gelungenen Test. */
|
||||||
|
static void test_onboard_cancels_running_start_pulse(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, true, true, 0);
|
||||||
|
a = nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
TEST_ASSERT_TRUE(a.pulse_off);
|
||||||
|
TEST_ASSERT_FALSE(sm.pulse_armed);
|
||||||
|
TEST_ASSERT_FALSE(sm.pulsing);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, NL_WIFI_STANDALONE_PULSE_MS).pulse_off);
|
||||||
|
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 6000);
|
||||||
|
TEST_ASSERT_FALSE(a.pulse_off);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 20000).pulse_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Der eigentliche Spec-Fall: gespeicherte Zugangsdaten UND Standalone, der Test scheitert.
|
||||||
|
* Die Lampe darf nicht in den Rueckfall (APSTA_FALLBACK) mit den alten Daten gehen und
|
||||||
|
* damit ihren eigenen AP verlassen, sondern bleibt im Standalone-AP. */
|
||||||
|
static void test_failed_onboarding_with_credentials_stays_standalone(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, true, true, false, 0);
|
||||||
|
nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 2000);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
|
||||||
|
TEST_ASSERT_FALSE(a.connect);
|
||||||
|
TEST_ASSERT_TRUE(a.disconnect);
|
||||||
|
TEST_ASSERT_TRUE(a.start_ap);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_onboarding_timeout_with_credentials_stays_standalone(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, true, true, false, 0);
|
||||||
|
nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
a = nl_wifi_sm_tick(&sm, 1000 + NL_WIFI_ONBOARD_TIMEOUT_MS);
|
||||||
|
TEST_ASSERT_EQUAL(NL_WIFI_ST_STANDALONE, sm.state);
|
||||||
|
TEST_ASSERT_FALSE(a.connect);
|
||||||
|
TEST_ASSERT_TRUE(a.disconnect);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_open_access(&sm));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nach gelungenem Onboarding laeuft noch der AP-Stopptimer. Schaltet man jetzt auf
|
||||||
|
* Standalone, muss er weg sein, sonst schaltet tick() den AP ab und die Lampe ist
|
||||||
|
* ueber nichts mehr erreichbar. */
|
||||||
|
static void test_standalone_cancels_pending_ap_stop(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
|
nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
nl_wifi_sm_event(&sm, NL_WIFI_EV_CONNECTED, NL_WIFI_ERR_NONE, 2000);
|
||||||
|
nl_wifi_sm_standalone(&sm, false, 3000);
|
||||||
|
a = nl_wifi_sm_tick(&sm, 2000 + NL_WIFI_AP_CLOSE_ONBOARD_MS);
|
||||||
|
TEST_ASSERT_FALSE(a.stop_ap);
|
||||||
|
TEST_ASSERT_TRUE(sm.ap_up);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ein unbefristeter Puls aus dem Onboarding-AP muss beim Wechsel ohne Puls enden, denn
|
||||||
|
* im Standalone-Modus gibt es keinen Timer mehr, der ihn beendete. */
|
||||||
|
static void test_standalone_without_pulse_ends_running_pulse(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
|
TEST_ASSERT_TRUE(sm.pulsing);
|
||||||
|
a = nl_wifi_sm_standalone(&sm, false, 1000);
|
||||||
|
TEST_ASSERT_TRUE(a.pulse_off);
|
||||||
|
TEST_ASSERT_FALSE(a.pulse_on);
|
||||||
|
TEST_ASSERT_FALSE(sm.pulsing);
|
||||||
|
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 1000 + NL_WIFI_STANDALONE_PULSE_MS).pulse_off);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* onboard() aus dem Onboarding-AP laesst den unbefristeten Puls in Ruhe. */
|
||||||
|
static void test_onboard_from_ap_onboarding_keeps_pulse(void)
|
||||||
|
{
|
||||||
|
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||||
|
a = nl_wifi_sm_onboard(&sm, 1000);
|
||||||
|
TEST_ASSERT_FALSE(a.pulse_off);
|
||||||
|
TEST_ASSERT_TRUE(sm.pulsing);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
@@ -218,5 +469,26 @@ int main(void)
|
|||||||
RUN_TEST(test_onboard_while_test_running_disconnects_stale_attempt);
|
RUN_TEST(test_onboard_while_test_running_disconnects_stale_attempt);
|
||||||
RUN_TEST(test_error_names);
|
RUN_TEST(test_error_names);
|
||||||
RUN_TEST(test_connect_without_any_event_retries);
|
RUN_TEST(test_connect_without_any_event_retries);
|
||||||
|
RUN_TEST(test_standalone_opens_ap_without_connecting);
|
||||||
|
RUN_TEST(test_standalone_is_not_open_access);
|
||||||
|
RUN_TEST(test_standalone_onboard_test_is_not_open_access);
|
||||||
|
RUN_TEST(test_standalone_wins_over_stored_credentials);
|
||||||
|
RUN_TEST(test_standalone_never_falls_back);
|
||||||
|
RUN_TEST(test_standalone_drops_a_stray_connect);
|
||||||
|
RUN_TEST(test_standalone_pulse_stops_by_itself);
|
||||||
|
RUN_TEST(test_standalone_without_pulse_stays_dark);
|
||||||
|
RUN_TEST(test_onboarding_from_standalone_clears_the_mode);
|
||||||
|
RUN_TEST(test_failed_onboarding_returns_to_standalone);
|
||||||
|
RUN_TEST(test_onboarding_timeout_returns_to_standalone);
|
||||||
|
RUN_TEST(test_failed_onboarding_without_standalone_opens_ap);
|
||||||
|
RUN_TEST(test_switching_to_standalone_at_runtime);
|
||||||
|
RUN_TEST(test_leaving_standalone_with_credentials_connects);
|
||||||
|
RUN_TEST(test_leaving_standalone_without_credentials_opens_onboarding);
|
||||||
|
RUN_TEST(test_onboard_cancels_running_start_pulse);
|
||||||
|
RUN_TEST(test_failed_onboarding_with_credentials_stays_standalone);
|
||||||
|
RUN_TEST(test_onboarding_timeout_with_credentials_stays_standalone);
|
||||||
|
RUN_TEST(test_standalone_cancels_pending_ap_stop);
|
||||||
|
RUN_TEST(test_standalone_without_pulse_ends_running_pulse);
|
||||||
|
RUN_TEST(test_onboard_from_ap_onboarding_keeps_pulse);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,6 +319,77 @@ def _read_imageset(imageset_path: Path, conf: dict[str, str], mode: str) -> tupl
|
|||||||
return name, False
|
return name, False
|
||||||
|
|
||||||
|
|
||||||
|
def _trailer_version(path: Path) -> str:
|
||||||
|
"""Firmware-Version aus dem NINL-Anhaenger eines ninlight-Abzugs, sonst "".
|
||||||
|
|
||||||
|
Nur die letzten 512 Byte werden gelesen: die Rettungsabzuege liegen als
|
||||||
|
_ota.img vor und tragen ihre Version im Anhaenger (Geraetetyp bei Offset 5,
|
||||||
|
Version bei 21) - das Entpacken des gzip-Rumpfs waere dafuer unnoetige Arbeit auf
|
||||||
|
einem Pi, der alle paar Sekunden neu zeichnet.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with path.open("rb") as f:
|
||||||
|
f.seek(-512, 2)
|
||||||
|
anhaenger = f.read(512)
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
if len(anhaenger) != 512 or anhaenger[0:4] != b"NINL":
|
||||||
|
return ""
|
||||||
|
return anhaenger[21:37].split(b"\x00", 1)[0].decode("ascii", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def delivered_version(conf: dict, imageset: str, mode: str) -> str:
|
||||||
|
"""Die Firmware-Version, die die Station gerade ausliefern wuerde, sonst "".
|
||||||
|
|
||||||
|
Quelle ist der GEWAEHLTE Bildsatz, nicht irgendein Verzeichnis: wer am Taster
|
||||||
|
zwischen "mit WLAN" und "ohne WLAN" umschaltet, bekommt sonst eine Zahl angezeigt,
|
||||||
|
die zum anderen Satz gehoert.
|
||||||
|
|
||||||
|
Im Migrationsmodus steht die Version in `manifest.json` - die Migrationsabzuege
|
||||||
|
selbst tragen keinen ninlight-Anhaenger (siehe server.py:image_version). Weichen
|
||||||
|
die Eintraege voneinander ab, ist das Ergebnis "gemischt": ein zusammengefuehrter
|
||||||
|
Deploy kann Alt-Eintraege mit anderer Version enthalten, und eine davon
|
||||||
|
herauszugreifen waere geraten. Im Rettungsmodus liegen gewoehnliche _ota.img, deren
|
||||||
|
Anhaenger die Version tragen.
|
||||||
|
|
||||||
|
Fehlt die Quelle oder ist sie unlesbar, bleibt das Ergebnis leer - dann zeigt die
|
||||||
|
Anzeige gar nichts, statt eine Version zu erfinden.
|
||||||
|
"""
|
||||||
|
image_dir = conf.get("IMAGE_DIR")
|
||||||
|
if not image_dir:
|
||||||
|
return ""
|
||||||
|
if mode == "recovery":
|
||||||
|
verzeichnis = Path(image_dir) / RESCUE_IMAGESET_DIRS.get(imageset, "")
|
||||||
|
try:
|
||||||
|
abzuege = sorted(p for p in verzeichnis.iterdir() if p.name.endswith(".img"))
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
versionen = {v for v in (_trailer_version(p) for p in abzuege) if v}
|
||||||
|
else:
|
||||||
|
pfad = Path(image_dir) / IMAGESET_DIRS.get(imageset, "") / "manifest.json"
|
||||||
|
try:
|
||||||
|
daten = json.loads(pfad.read_text())
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return ""
|
||||||
|
if not isinstance(daten, dict):
|
||||||
|
return ""
|
||||||
|
versionen = set()
|
||||||
|
eintraege = daten.get("images")
|
||||||
|
if isinstance(eintraege, list):
|
||||||
|
versionen = {
|
||||||
|
e["ninlight_version"]
|
||||||
|
for e in eintraege
|
||||||
|
if isinstance(e, dict) and isinstance(e.get("ninlight_version"), str)
|
||||||
|
and e["ninlight_version"]
|
||||||
|
}
|
||||||
|
if not versionen:
|
||||||
|
oben = daten.get("ninlight_version")
|
||||||
|
versionen = {oben} if isinstance(oben, str) and oben else set()
|
||||||
|
if not versionen:
|
||||||
|
return ""
|
||||||
|
return versionen.pop() if len(versionen) == 1 else "gemischt"
|
||||||
|
|
||||||
|
|
||||||
def _route_lookup() -> str:
|
def _route_lookup() -> str:
|
||||||
"""Die Rohauskunft von "ip -4 route get" zur Standardroute, oder "" wenn es keine gibt.
|
"""Die Rohauskunft von "ip -4 route get" zur Standardroute, oder "" wenn es keine gibt.
|
||||||
|
|
||||||
@@ -416,13 +487,17 @@ def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
|||||||
# (wlan0 traegt dann 192.168.8.1, das steht ohnehin auf jedem Statusbild) und
|
# (wlan0 traegt dann 192.168.8.1, das steht ohnehin auf jedem Statusbild) und
|
||||||
# wuerde den knappen Platz fuer Betriebsauskuenfte wegnehmen.
|
# wuerde den knappen Platz fuer Betriebsauskuenfte wegnehmen.
|
||||||
geraet, adresse = local_address()
|
geraet, adresse = local_address()
|
||||||
|
# Auch im Aus-Zustand: welche Version die Station ausliefern WUERDE, ist genau
|
||||||
|
# dann interessant, wenn man sie noch nicht eingeschaltet hat.
|
||||||
return {"mode": "off", "imageset": imageset, "imageset_ok": imageset_ok,
|
return {"mode": "off", "imageset": imageset, "imageset_ok": imageset_ok,
|
||||||
"ip": adresse, "iface": geraet}
|
"ip": adresse, "iface": geraet,
|
||||||
|
"image_version": delivered_version(conf, imageset, "migration")}
|
||||||
|
|
||||||
status = _read_json(run_dir / "status.json")
|
status = _read_json(run_dir / "status.json")
|
||||||
state: dict = {"mode": mode, "served": status.get("served"), "recent": status.get("recent"),
|
state: dict = {"mode": mode, "served": status.get("served"), "recent": status.get("recent"),
|
||||||
"imageset": imageset, "imageset_ok": imageset_ok,
|
"imageset": imageset, "imageset_ok": imageset_ok,
|
||||||
"activity": _recent_request(status)}
|
"activity": _recent_request(status),
|
||||||
|
"image_version": delivered_version(conf, imageset, mode)}
|
||||||
|
|
||||||
if mode == "recovery":
|
if mode == "recovery":
|
||||||
state["ssid"] = conf.get("SSID")
|
state["ssid"] = conf.get("SSID")
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ TEXT_MARGIN_PX = 2
|
|||||||
# Migration.
|
# Migration.
|
||||||
ROLE_HEADER = "header"
|
ROLE_HEADER = "header"
|
||||||
ROLE_HEADER_PHASE = "header_phase"
|
ROLE_HEADER_PHASE = "header_phase"
|
||||||
|
# Rechtsbuendig in DERSELBEN Zeile wie die zuletzt gezeichnete Detailzeile.
|
||||||
|
# Genau ein Block benutzt sie: die ausgelieferte Firmware-Version neben der
|
||||||
|
# Adresse im Aus-Zustand. Eigene Rolle statt einer zweiten Zeile, weil der
|
||||||
|
# Platz auf 250x122 knapp ist und beide Angaben zusammengehoeren.
|
||||||
|
ROLE_DETAIL_RIGHT = "detail_right"
|
||||||
ROLE_HERO = "hero"
|
ROLE_HERO = "hero"
|
||||||
ROLE_DETAIL = "detail"
|
ROLE_DETAIL = "detail"
|
||||||
ROLE_BIG = "big"
|
ROLE_BIG = "big"
|
||||||
@@ -187,6 +192,9 @@ PHASE_LABELS = {"auto": "AUTO", "listen": "LISTEN"}
|
|||||||
# verloren geht - der Zustand "freigegeben" ist der einzige mit diesem Label) und der
|
# verloren geht - der Zustand "freigegeben" ist der einzige mit diesem Label) und der
|
||||||
# Zielname als eigene Beiwerkzeile ueber die volle Panelbreite.
|
# Zielname als eigene Beiwerkzeile ueber die volle Panelbreite.
|
||||||
ARMED_LABEL = "ARMED"
|
ARMED_LABEL = "ARMED"
|
||||||
|
# Praefix der Zeile mit der ausgelieferten Version - grenzt sie von "FW:" ab, das die
|
||||||
|
# Version der erkannten Lampe traegt.
|
||||||
|
DELIVERS_PREFIX = "liefert: "
|
||||||
ARMED_DETAIL_PREFIX = "Armed: "
|
ARMED_DETAIL_PREFIX = "Armed: "
|
||||||
|
|
||||||
# Der Bildsatz-Umschalter in der Fusszeile. Bewusst als Aussage ueber die LAMPE formuliert
|
# Der Bildsatz-Umschalter in der Fusszeile. Bewusst als Aussage ueber die LAMPE formuliert
|
||||||
@@ -334,6 +342,7 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
|||||||
out.append((f"{geraet}: {ip}", ROLE_DETAIL))
|
out.append((f"{geraet}: {ip}", ROLE_DETAIL))
|
||||||
else:
|
else:
|
||||||
out.append((f"IP: {ip}", ROLE_DETAIL))
|
out.append((f"IP: {ip}", ROLE_DETAIL))
|
||||||
|
out += _delivered_version_blocks(state, ROLE_DETAIL_RIGHT)
|
||||||
return out + footer
|
return out + footer
|
||||||
|
|
||||||
if mode not in ("recovery", "migration"):
|
if mode not in ("recovery", "migration"):
|
||||||
@@ -348,6 +357,7 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
|||||||
ssid = state.get("ssid")
|
ssid = state.get("ssid")
|
||||||
|
|
||||||
if mode == "recovery":
|
if mode == "recovery":
|
||||||
|
out += _delivered_version_blocks(state, ROLE_HEADER_PHASE)
|
||||||
# Im Rettungsmodus gibt es keine Phase - kein ROLE_HEADER_PHASE-Block, die
|
# Im Rettungsmodus gibt es keine Phase - kein ROLE_HEADER_PHASE-Block, die
|
||||||
# Kopfzeile bleibt also nur der Modus (compose() zentriert sie dann weiterhin
|
# Kopfzeile bleibt also nur der Modus (compose() zentriert sie dann weiterhin
|
||||||
# wie vor der zweiten Hardware-Abnahme, siehe dort).
|
# wie vor der zweiten Hardware-Abnahme, siehe dort).
|
||||||
@@ -409,6 +419,13 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
|||||||
lamp_version = last_seen.get("lamp_version")
|
lamp_version = last_seen.get("lamp_version")
|
||||||
if lamp_version:
|
if lamp_version:
|
||||||
out.append((f"FW: {lamp_version}", ROLE_DETAIL))
|
out.append((f"FW: {lamp_version}", ROLE_DETAIL))
|
||||||
|
# Beschriftet und unmittelbar hinter "FW:", weil beides Versionsnummern sind
|
||||||
|
# und nebeneinander sonst niemand weiss, welche wem gehoert: "FW:" ist die
|
||||||
|
# der erkannten LAMPE, "liefert:" die des Bildsatzes. Nicht in die Kopfzeile:
|
||||||
|
# dort steht im Migrationsmodus die Phase, und neben dem Modus bleiben nur
|
||||||
|
# rund 112 px - daran ist schon "FREIGEGEBEN: <ziel>" gescheitert.
|
||||||
|
for text, _ in _delivered_version_blocks(state, ROLE_DETAIL):
|
||||||
|
out.append((f"{DELIVERS_PREFIX}{text}", ROLE_DETAIL))
|
||||||
if ssid and armed_target is None:
|
if ssid and armed_target is None:
|
||||||
# Nur hier, wo die grosse Zeile schon vergeben ist - sonst stuende die
|
# Nur hier, wo die grosse Zeile schon vergeben ist - sonst stuende die
|
||||||
# SSID zweimal auf dem Panel.
|
# SSID zweimal auf dem Panel.
|
||||||
@@ -462,6 +479,19 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
|||||||
return out + footer
|
return out + footer
|
||||||
|
|
||||||
|
|
||||||
|
def _delivered_version_blocks(state: dict, role: str) -> list[tuple[str, str]]:
|
||||||
|
"""Die Version, die die Station ausliefert - oder [], wenn sie unbekannt ist.
|
||||||
|
|
||||||
|
Bewusst keine Ersatzanzeige: eine erfundene oder veraltete Versionsnummer ist
|
||||||
|
schlimmer als keine, denn sie beantwortet genau die Frage falsch, fuer die sie da
|
||||||
|
ist. display.py liefert "" , wenn manifest.json bzw. die Abzuege fehlen.
|
||||||
|
"""
|
||||||
|
version = state.get("image_version")
|
||||||
|
if not isinstance(version, str) or not version:
|
||||||
|
return []
|
||||||
|
return [(version, role)]
|
||||||
|
|
||||||
|
|
||||||
def _imageset_blocks(state: dict) -> list[tuple[str, str]]:
|
def _imageset_blocks(state: dict) -> list[tuple[str, str]]:
|
||||||
"""Die Fusszeile mit dem Bildsatz-Umschalter, oder [] wenn der Zustand keinen traegt
|
"""Die Fusszeile mit dem Bildsatz-Umschalter, oder [] wenn der Zustand keinen traegt
|
||||||
(aeltere display.py-Fassung, von Hand zusammengesetzter Zustand in einem Test).
|
(aeltere display.py-Fassung, von Hand zusammengesetzter Zustand in einem Test).
|
||||||
@@ -636,6 +666,7 @@ def compose(state: dict):
|
|||||||
ROLE_HEADER_PHASE: (HEADER_PHASE_FONT_PX, True),
|
ROLE_HEADER_PHASE: (HEADER_PHASE_FONT_PX, True),
|
||||||
ROLE_HERO: (HERO_FONT_PX, True),
|
ROLE_HERO: (HERO_FONT_PX, True),
|
||||||
ROLE_DETAIL: (DETAIL_FONT_PX, False),
|
ROLE_DETAIL: (DETAIL_FONT_PX, False),
|
||||||
|
ROLE_DETAIL_RIGHT: (DETAIL_FONT_PX, False),
|
||||||
ROLE_BIG: (BIG_FONT_PX, True),
|
ROLE_BIG: (BIG_FONT_PX, True),
|
||||||
ROLE_FOOTER: (FOOTER_FONT_PX, True),
|
ROLE_FOOTER: (FOOTER_FONT_PX, True),
|
||||||
}
|
}
|
||||||
@@ -700,6 +731,10 @@ def compose(state: dict):
|
|||||||
fill=0, align="center")
|
fill=0, align="center")
|
||||||
|
|
||||||
y = TEXT_MARGIN_PX
|
y = TEXT_MARGIN_PX
|
||||||
|
# Oberkante und rechter Rand der zuletzt gezeichneten Flusszeile - ROLE_DETAIL_RIGHT
|
||||||
|
# setzt seinen Text daneben.
|
||||||
|
letzte_zeile_oben: int | None = None
|
||||||
|
letzte_zeile_rechts = 0.0
|
||||||
for raw_text, role in block_list:
|
for raw_text, role in block_list:
|
||||||
font = font_for(role)
|
font = font_for(role)
|
||||||
|
|
||||||
@@ -740,6 +775,22 @@ def compose(state: dict):
|
|||||||
# Ueberlappen mit dem Modus ist nicht erlaubt.
|
# Ueberlappen mit dem Modus ist nicht erlaubt.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if role == ROLE_DETAIL_RIGHT:
|
||||||
|
# Rechtsbuendig neben der zuletzt gezeichneten Zeile, gemessen ab deren
|
||||||
|
# tatsaechlichem rechten Rand - wie die Phase neben dem Modus. Ohne
|
||||||
|
# vorangehende Zeile (von Hand zusammengesetzter Zustand im Test) oder bei
|
||||||
|
# zu wenig Platz entfaellt die Angabe: ein Ueberlappen waere schlimmer als
|
||||||
|
# eine fehlende Zusatzauskunft.
|
||||||
|
if letzte_zeile_oben is None:
|
||||||
|
continue
|
||||||
|
frei_px = (PANEL_WIDTH_PX - TEXT_MARGIN_PX - letzte_zeile_rechts
|
||||||
|
- HEADER_PHASE_GAP_PX)
|
||||||
|
if frei_px >= HEADER_PHASE_MIN_WIDTH_PX:
|
||||||
|
text = fit(raw_text, font, width_px=frei_px)
|
||||||
|
_draw_text(draw, text, font, top=letzte_zeile_oben,
|
||||||
|
height=DETAIL_LINE_HEIGHT_PX, fill=0, align="right")
|
||||||
|
continue
|
||||||
|
|
||||||
text = fit(raw_text, font)
|
text = fit(raw_text, font)
|
||||||
line_height = HERO_LINE_HEIGHT_PX if role == ROLE_HERO else DETAIL_LINE_HEIGHT_PX
|
line_height = HERO_LINE_HEIGHT_PX if role == ROLE_HERO else DETAIL_LINE_HEIGHT_PX
|
||||||
if y + line_height > flow_limit_px:
|
if y + line_height > flow_limit_px:
|
||||||
@@ -748,6 +799,8 @@ def compose(state: dict):
|
|||||||
# Reihenfolge in blocks() ist deshalb nach Wichtigkeit sortiert.
|
# Reihenfolge in blocks() ist deshalb nach Wichtigkeit sortiert.
|
||||||
break
|
break
|
||||||
draw.text((TEXT_MARGIN_PX, y), text, font=font, fill=0)
|
draw.text((TEXT_MARGIN_PX, y), text, font=font, fill=0)
|
||||||
|
letzte_zeile_oben = y
|
||||||
|
letzte_zeile_rechts = TEXT_MARGIN_PX + draw.textlength(text, font=font)
|
||||||
y += line_height
|
y += line_height
|
||||||
|
|
||||||
return image
|
return image
|
||||||
|
|||||||
@@ -957,9 +957,11 @@ def test_build_state_off_when_service_not_active(tmp_path, monkeypatch):
|
|||||||
last_mode_path=tmp_path / "last-mode")
|
last_mode_path=tmp_path / "last-mode")
|
||||||
|
|
||||||
# "off" und sonst NICHTS aus status.json - nur der Bildsatz, der auch im
|
# "off" und sonst NICHTS aus status.json - nur der Bildsatz, der auch im
|
||||||
# ausgeschalteten Zustand gilt (und gerade dann gesehen werden soll).
|
# ausgeschalteten Zustand gilt (und gerade dann gesehen werden soll), die Adresse
|
||||||
|
# fuer SSH und die Version, die die Station ausliefern WUERDE.
|
||||||
assert state["mode"] == "off"
|
assert state["mode"] == "off"
|
||||||
assert set(state) == {"mode", "imageset", "imageset_ok", "ip", "iface"}
|
assert set(state) == {"mode", "imageset", "imageset_ok", "ip", "iface",
|
||||||
|
"image_version"}
|
||||||
assert isinstance(state["ip"], str)
|
assert isinstance(state["ip"], str)
|
||||||
|
|
||||||
|
|
||||||
@@ -2462,3 +2464,59 @@ def test_fixed_labels_fit_the_panel_with_room_for_a_wider_font(source, text, rol
|
|||||||
"{}: {!r} misst {:.1f} px; mit {:.0f} % Reserve fuer eine breitere Schrift "
|
"{}: {!r} misst {:.1f} px; mit {:.0f} % Reserve fuer eine breitere Schrift "
|
||||||
"bleiben nur {:.1f} px Platz".format(
|
"bleiben nur {:.1f} px Platz".format(
|
||||||
source, text, width_px, (FOOTER_WIDTH_SAFETY - 1) * 100, max_width_px))
|
source, text, width_px, (FOOTER_WIDTH_SAFETY - 1) * 100, max_width_px))
|
||||||
|
|
||||||
|
|
||||||
|
# --- Ausgelieferte Firmware-Version ---------------------------------------------------
|
||||||
|
#
|
||||||
|
# "Welche Version liefert die Station gerade aus?" war am Geraet nur zu beantworten,
|
||||||
|
# indem man sich per SSH die Abzuege ansah. Die Anzeige sagt es jetzt selbst.
|
||||||
|
|
||||||
|
|
||||||
|
def test_off_screen_shows_delivered_version_next_to_the_address():
|
||||||
|
bloecke = render.blocks({"mode": "off", "ip": "192.168.2.96", "iface": "wlan0",
|
||||||
|
"image_version": "0.4.0"})
|
||||||
|
assert ("wlan0: 192.168.2.96", render.ROLE_DETAIL) in bloecke
|
||||||
|
assert ("0.4.0", render.ROLE_DETAIL_RIGHT) in bloecke
|
||||||
|
# Rechts NEBEN der Adresse, also unmittelbar dahinter - sonst landete sie neben
|
||||||
|
# einer anderen Zeile.
|
||||||
|
assert bloecke.index(("0.4.0", render.ROLE_DETAIL_RIGHT)) == (
|
||||||
|
bloecke.index(("wlan0: 192.168.2.96", render.ROLE_DETAIL)) + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rescue_shows_delivered_version_in_the_header():
|
||||||
|
bloecke = render.blocks({"mode": "recovery", "ssid": "ninlight-rescue",
|
||||||
|
"image_version": "0.4.0"})
|
||||||
|
assert ("0.4.0", render.ROLE_HEADER_PHASE) in bloecke
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_labels_the_delivered_version():
|
||||||
|
"""Im Migrationsmodus beschriftet, weil daneben schon die Lampenversion steht."""
|
||||||
|
bloecke = render.blocks({
|
||||||
|
"mode": "migration", "phase": "auto", "image_version": "0.4.0",
|
||||||
|
"last_seen": {"product_name": "Klyqa E27 RGB", "lamp_version": "1.1.1.2358"},
|
||||||
|
})
|
||||||
|
texte = [text for text, _ in bloecke]
|
||||||
|
assert "FW: 1.1.1.2358" in texte
|
||||||
|
assert "liefert: 0.4.0" in texte
|
||||||
|
# NICHT in die Kopfzeile: dort steht die Phase.
|
||||||
|
assert ("0.4.0", render.ROLE_HEADER_PHASE) not in bloecke
|
||||||
|
assert ("AUTO", render.ROLE_HEADER_PHASE) in bloecke
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_version_shows_nothing():
|
||||||
|
"""Lieber keine Angabe als eine erfundene - sie beantwortet sonst genau die Frage
|
||||||
|
falsch, fuer die sie da ist."""
|
||||||
|
for zustand in ({"mode": "off", "ip": "192.168.2.96", "iface": "wlan0"},
|
||||||
|
{"mode": "off", "ip": "192.168.2.96", "iface": "wlan0",
|
||||||
|
"image_version": ""},
|
||||||
|
{"mode": "recovery", "ssid": "x", "image_version": None}):
|
||||||
|
bloecke = render.blocks(zustand)
|
||||||
|
assert not [b for b in bloecke if b[1] == render.ROLE_DETAIL_RIGHT]
|
||||||
|
assert not [b for b in bloecke if b[1] == render.ROLE_HEADER_PHASE]
|
||||||
|
|
||||||
|
|
||||||
|
def test_mixed_sets_are_called_out():
|
||||||
|
bloecke = render.blocks({"mode": "off", "ip": "10.0.0.5", "iface": "eth0",
|
||||||
|
"image_version": "gemischt"})
|
||||||
|
assert ("gemischt", render.ROLE_DETAIL_RIGHT) in bloecke
|
||||||
|
|||||||
Reference in New Issue
Block a user