Compare commits
21
Commits
7fd279721f
...
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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>
|
||||
|
||||
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):
|
||||
|
||||
@@ -65,7 +65,7 @@ Verfügbare Befehle in der Konsole: `help`.
|
||||
gespeicherten Zugangsdaten). Im Fallback-AP (Router war > 5 min weg, Lampe hat aber bereits
|
||||
Zugangsdaten) ist der Token-Header wie bei jedem anderen Endpunkt erforderlich.
|
||||
Nach Erfolg schließt der AP nach 60 s. Ohne Verbindung für 5 min öffnet die Lampe den AP zusätzlich (Fallback).
|
||||
- REST auf Port 3333, Präfix `/api/v1`: `GET|POST device/state`, `GET system/info`, `GET|PUT system/settings` (kennt u.a. den Typ `sacn`, siehe unten) und `PUT system/command` (beide mit Header `Authorization: <token>`, Default-Token `aabbccddeeff0011223344`).
|
||||
- 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}'`
|
||||
- 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.
|
||||
@@ -107,6 +107,54 @@ 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`.
|
||||
|
||||
### 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"}`. Anders als der unkonfigurierte Onboarding-AP ist der
|
||||
Standalone-AP nicht token-frei: der Tokenschutz gilt dort wie im Normalbetrieb (am Gerät noch nicht
|
||||
nachgemessen, siehe Abnahmeliste). 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
|
||||
|
||||
Der Build erzeugt pro Gerät zwei Images:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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)
|
||||
- [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
|
||||
- [ ] 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
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
printf("factory reset\r\n");
|
||||
@@ -228,10 +241,20 @@ static void app_task(void *arg)
|
||||
boot = s_settings;
|
||||
nl_app_unlock();
|
||||
nl_mdns_set_name(boot.name);
|
||||
if (boot.wifi_ssid[0])
|
||||
nl_wifi_start(boot.wifi_ssid, boot.wifi_pass, NINLIGHT_AP_PASSWORD);
|
||||
if (boot.standalone)
|
||||
/* 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
|
||||
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());
|
||||
|
||||
@@ -62,6 +62,12 @@ typedef char nl_settings_legacy_check[offsetof(nl_settings_t, sacn_enabled) == 2
|
||||
* Vorgabe. */
|
||||
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];
|
||||
|
||||
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_hold = 0;
|
||||
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)
|
||||
|
||||
@@ -41,6 +41,17 @@ typedef struct {
|
||||
* dem vorherigen sizeof (216), sonst ueberschreibt das Fuellbyte
|
||||
* eines aelteren Satzes sie. */
|
||||
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;
|
||||
|
||||
void nl_settings_defaults(nl_settings_t *s);
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#define P_SETTINGS "/api/v1/system/settings"
|
||||
#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 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);
|
||||
else
|
||||
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, "free_heap", in.free_heap);
|
||||
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
|
||||
* Gruppe nicht weiterreicht - Unicast laeuft in beiden Faellen. */
|
||||
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);
|
||||
}
|
||||
@@ -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_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);
|
||||
}
|
||||
|
||||
@@ -339,6 +362,67 @@ static void handle_put_settings(const nl_api_ctx_t *ctx, const nl_http_request_t
|
||||
cJSON_Delete(j);
|
||||
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) {
|
||||
if (get_string(j, "name", 1, NL_NAME_LEN - 1, &a) != 1)
|
||||
err = "invalid name";
|
||||
|
||||
@@ -26,18 +26,25 @@ typedef struct {
|
||||
const char *last_ota_error; /* NULL until OTA exists */
|
||||
bool sacn_active; /* true, solange die Lampe ferngesteuert wird */
|
||||
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;
|
||||
|
||||
/* Everything the API needs from the application; implemented by src/sys/nl_app_api.c. */
|
||||
typedef struct {
|
||||
const nl_board_t *board;
|
||||
const char *fw_version;
|
||||
const char *hostname; /* "ninlight-<mac4>", Vorgabe fuer den AP-Namen */
|
||||
const char *build_date;
|
||||
void (*get_state)(nl_light_state_t *out);
|
||||
nl_ctrl_err_t (*apply)(const nl_light_change_t *c);
|
||||
void (*get_settings)(nl_settings_t *out);
|
||||
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 */
|
||||
/* 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 */
|
||||
void (*get_info)(nl_sys_info_t *out);
|
||||
void (*factory_reset)(void); /* request only */
|
||||
|
||||
+65
-6
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "wm_include.h"
|
||||
#include "lwip/apps/mdns.h"
|
||||
#include "lwip/netif.h"
|
||||
#include "lwip/tcpip.h"
|
||||
|
||||
#include "config/nl_settings.h"
|
||||
@@ -16,8 +17,13 @@
|
||||
#define NL_MDNS_TTL 120
|
||||
|
||||
static bool s_inited;
|
||||
static struct netif *s_nif; /* Netif, an dem Antwortdienst und Dienst haengen */
|
||||
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_mac[18];
|
||||
|
||||
@@ -45,15 +51,53 @@ static void txt_cb(struct mdns_service *service, void *data)
|
||||
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 */
|
||||
static void announce(void *arg)
|
||||
{
|
||||
struct netif *nif = tls_get_netif();
|
||||
struct netif *nif = pick_netif();
|
||||
bool attempted = false;
|
||||
|
||||
(void)arg;
|
||||
if (!nif)
|
||||
if (!nif) {
|
||||
printf("mdns: no usable netif yet\r\n");
|
||||
s_announce_pending = false;
|
||||
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) {
|
||||
mdns_resp_init();
|
||||
@@ -69,10 +113,19 @@ static void announce(void *arg)
|
||||
NL_MDNS_TTL, txt_cb, NULL) == ERR_OK;
|
||||
}
|
||||
|
||||
if (attempted)
|
||||
printf("mdns: %s.local _ninlight._tcp %s\r\n", nl_wifi_hostname(), s_service_added ? "up" : "failed");
|
||||
else
|
||||
if (attempted) {
|
||||
/* Welches Netif benutzt wurde, steht in der Zeile selbst: Namenskuerzel, Nummer, IPv4.
|
||||
* 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 */
|
||||
s_announce_pending = false;
|
||||
}
|
||||
|
||||
void nl_mdns_on_ip_up(void)
|
||||
@@ -80,9 +133,15 @@ void nl_mdns_on_ip_up(void)
|
||||
uint8_t mac[6];
|
||||
nl_wifi_mac(mac);
|
||||
nl_names_mac_str(mac, s_mac);
|
||||
s_announce_pending = true;
|
||||
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)
|
||||
{
|
||||
u32 sr = tls_os_set_critical();
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
#ifndef NL_MDNS_H
|
||||
#define NL_MDNS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Called by the network task once the station got an IP (again). */
|
||||
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. */
|
||||
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. */
|
||||
extern volatile uint32_t nl_net_beat;
|
||||
|
||||
/* 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);
|
||||
/* Starts the network task. An empty ssid starts the onboarding AP. Strings are copied.
|
||||
* 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
|
||||
* 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
|
||||
@@ -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);
|
||||
/* Starts an onboarding test with new credentials (from the HTTP task). 0 = queued. */
|
||||
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). */
|
||||
bool nl_wifi_open_access(void);
|
||||
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)
|
||||
{
|
||||
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->since = now;
|
||||
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();
|
||||
|
||||
memset(sm, 0, sizeof *sm);
|
||||
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);
|
||||
a.connect = 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:
|
||||
if (ev == NL_WIFI_EV_CONNECTED) {
|
||||
a.save_test_creds = true;
|
||||
if (sm->standalone) {
|
||||
sm->standalone = false;
|
||||
a.clear_standalone = true;
|
||||
}
|
||||
sm->have_creds = true;
|
||||
sm->error = NL_WIFI_ERR_NONE;
|
||||
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)
|
||||
a.disconnect = true; /* I1: stray success of a timed-out/failed attempt */
|
||||
break;
|
||||
case NL_WIFI_ST_STANDALONE:
|
||||
if (ev == NL_WIFI_EV_CONNECTED)
|
||||
a.disconnect = true; /* stray success of an old attempt */
|
||||
break;
|
||||
default:
|
||||
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();
|
||||
|
||||
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)) {
|
||||
sm->ap_stop_armed = 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->retry_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.connect = true;
|
||||
a.use_test_creds = true;
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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 sm->ap_up ? "apsta" : "sta";
|
||||
}
|
||||
|
||||
+13
-2
@@ -11,13 +11,15 @@
|
||||
#define NL_WIFI_BACKOFF_MIN_MS 1000u
|
||||
#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_STANDALONE_PULSE_MS 5000u /* short sign-of-life pulse, then normal light */
|
||||
|
||||
typedef enum {
|
||||
NL_WIFI_ST_AP_ONBOARDING = 0,
|
||||
NL_WIFI_ST_STA_CONNECTING,
|
||||
NL_WIFI_ST_STA_CONNECTED,
|
||||
NL_WIFI_ST_APSTA_FALLBACK,
|
||||
NL_WIFI_ST_ONBOARD_TEST
|
||||
NL_WIFI_ST_ONBOARD_TEST,
|
||||
NL_WIFI_ST_STANDALONE
|
||||
} 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;
|
||||
@@ -33,6 +35,7 @@ typedef struct {
|
||||
bool disconnect; /* drop a stray/failed station attempt (before any connect in the same set) */
|
||||
bool pulse_on;
|
||||
bool pulse_off;
|
||||
bool clear_standalone; /* Onboarding gelungen: den Standalone-Modus dauerhaft loeschen */
|
||||
} nl_wifi_actions_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -44,18 +47,26 @@ typedef struct {
|
||||
bool retry_armed;
|
||||
bool ap_stop_armed;
|
||||
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 retry_at;
|
||||
uint32_t backoff;
|
||||
uint32_t deadline; /* onboarding test */
|
||||
uint32_t ap_stop_at;
|
||||
uint32_t connect_at; /* when the pending connect was issued */
|
||||
uint32_t pulse_stop_at;
|
||||
} 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_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);
|
||||
/* 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);
|
||||
const char *nl_wifi_sm_mode(const nl_wifi_sm_t *sm);
|
||||
const char *nl_wifi_error_name(nl_wifi_error_t e);
|
||||
|
||||
+306
-15
@@ -18,6 +18,10 @@
|
||||
#define NL_NET_QUEUE 8
|
||||
#define MSG_NETIF 0x100u /* | netif status */
|
||||
#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 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_test_ssid[NL_SSID_LEN], s_test_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 uint32_t s_ap_create_retry_at;
|
||||
|
||||
@@ -88,18 +120,77 @@ void nl_wifi_disable_sdk_auto_connect(void)
|
||||
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)
|
||||
{
|
||||
struct tls_softap_info_t ap;
|
||||
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);
|
||||
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(&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.channel = 6;
|
||||
ap.channel = s_ap_channel;
|
||||
ap.keyinfo.format = 1; /* ascii */
|
||||
ap.keyinfo.index = 1;
|
||||
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;
|
||||
memcpy(ip.dnsname, "local", 6);
|
||||
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. */
|
||||
s_ap_create_pending = r != WM_SUCCESS;
|
||||
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)
|
||||
@@ -149,6 +275,24 @@ static void execute(nl_wifi_actions_t a)
|
||||
printf("wifi: onboarding ok, saving credentials for %s\r\n", ssid);
|
||||
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)
|
||||
pulse(true);
|
||||
if (a.pulse_off)
|
||||
@@ -157,9 +301,7 @@ static void execute(nl_wifi_actions_t a)
|
||||
start_ap();
|
||||
if (a.stop_ap) {
|
||||
printf("wifi: AP down\r\n");
|
||||
tls_wifi_softap_destroy();
|
||||
s_ap_create_pending = false;
|
||||
clear_softap_param(); /* C2: every softap teardown clears the persisted SOFTAP bit */
|
||||
destroy_ap();
|
||||
}
|
||||
if (a.disconnect) {
|
||||
printf("wifi: dropping stray/failed station attempt\r\n");
|
||||
@@ -185,7 +327,9 @@ static void net_task(void *arg)
|
||||
void *msg = NULL;
|
||||
nl_wifi_actions_t ev, tick;
|
||||
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);
|
||||
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);
|
||||
} else if (m == MSG_ONBOARD) {
|
||||
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);
|
||||
/* 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
|
||||
* (which only guards s_sm and the credential buffers) is held. */
|
||||
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();
|
||||
|
||||
if (unexpected_softap) {
|
||||
printf("wifi: unexpected SDK softap while AP should be down, destroying it\r\n");
|
||||
tls_wifi_softap_destroy();
|
||||
clear_softap_param();
|
||||
destroy_ap();
|
||||
}
|
||||
|
||||
if (m & MSG_NETIF)
|
||||
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);
|
||||
/* 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);
|
||||
if (got_ip_event && connected_now)
|
||||
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. */
|
||||
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;
|
||||
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);
|
||||
copy(s_ssid, ssid, sizeof s_ssid);
|
||||
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. */
|
||||
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
|
||||
* 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");
|
||||
tls_wifi_softap_destroy();
|
||||
clear_softap_param();
|
||||
destroy_ap();
|
||||
}
|
||||
nif = tls_get_netif();
|
||||
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);
|
||||
|
||||
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();
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
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 r;
|
||||
@@ -316,6 +598,15 @@ void nl_wifi_status(nl_wifi_status_t *out)
|
||||
lock();
|
||||
out->mode = nl_wifi_sm_mode(&s_sm);
|
||||
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);
|
||||
ap_up = s_sm.ap_up || s_sm.state == NL_WIFI_ST_AP_ONBOARDING;
|
||||
unlock();
|
||||
|
||||
@@ -28,6 +28,11 @@ void nl_app_request_factory_reset(void);
|
||||
/* Stores new station credentials (from a successful onboarding test) and persists them. */
|
||||
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.
|
||||
* 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
|
||||
|
||||
@@ -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 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)
|
||||
{
|
||||
nl_wifi_status_t w;
|
||||
@@ -97,6 +105,7 @@ static void get_info(nl_sys_info_t *out)
|
||||
nl_app_unlock();
|
||||
/* Ausserhalb der Sperre: nl_sacn haelt den Wert selbst, nl_dmx ist nicht beteiligt. */
|
||||
out->sacn_joined = nl_sacn_joined();
|
||||
out->sacn_running = nl_sacn_running();
|
||||
nl_wifi_status(&w);
|
||||
out->wifi_mode = w.mode;
|
||||
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->free_heap = tls_mem_get_avail_heapsize();
|
||||
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(); }
|
||||
@@ -130,12 +140,17 @@ const nl_api_ctx_t *nl_app_api_ctx(void)
|
||||
cJSON_InitHooks(&hooks);
|
||||
ctx.board = nl_board_get();
|
||||
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.get_state = get_state;
|
||||
ctx.apply = apply;
|
||||
ctx.get_settings = get_settings;
|
||||
ctx.save_settings = save_settings;
|
||||
ctx.wifi_configure = wifi_configure;
|
||||
ctx.standalone_configure = standalone_configure;
|
||||
ctx.open_access = open_access;
|
||||
ctx.get_info = get_info;
|
||||
ctx.factory_reset = factory_reset;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "fake_api_ctx.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.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_wifi_calls, fake_wifi_result;
|
||||
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;
|
||||
char fake_fw_url[256];
|
||||
bool fake_open;
|
||||
@@ -49,6 +54,18 @@ static int wifi_configure(const char *ssid, const char *pass)
|
||||
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 void get_info(nl_sys_info_t *out) { *out = fake_info; }
|
||||
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_wifi_calls = fake_wifi_result = 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_fw_result = -1;
|
||||
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.save_settings = save_settings;
|
||||
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.get_info = get_info;
|
||||
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_wifi_calls, fake_wifi_result;
|
||||
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 char fake_fw_url[256];
|
||||
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_joined = true;
|
||||
fake_info.sacn_running = true;
|
||||
fake_settings.sacn_universe = 5;
|
||||
call(NL_HTTP_GET, "/api/v1/system/info", NULL, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(200, resp.status);
|
||||
TEST_ASSERT_TRUE(cJSON_IsTrue(sacn_field("active")));
|
||||
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("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 --- */
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/* --- 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)
|
||||
{
|
||||
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_limit_follows_the_board);
|
||||
RUN_TEST(test_info_exposes_sacn_state);
|
||||
RUN_TEST(test_info_reports_a_dead_sacn_task);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -275,6 +275,55 @@ static void test_record_without_layout_gets_the_default(void)
|
||||
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)
|
||||
{
|
||||
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_record_without_layout_gets_the_default);
|
||||
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();
|
||||
}
|
||||
|
||||
+284
-12
@@ -10,7 +10,7 @@ void tearDown(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.pulse_on);
|
||||
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)
|
||||
{
|
||||
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_FALSE(a.use_test_creds);
|
||||
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)
|
||||
{
|
||||
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);
|
||||
TEST_ASSERT_FALSE(nl_wifi_sm_tick(&sm, 1999).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)
|
||||
{
|
||||
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);
|
||||
a = nl_wifi_sm_tick(&sm, NL_WIFI_FALLBACK_MS - 1);
|
||||
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)
|
||||
{
|
||||
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_DISCONNECTED, NL_WIFI_ERR_NONE, 100000);
|
||||
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)
|
||||
{
|
||||
nl_wifi_sm_start(&sm, false, 0);
|
||||
nl_wifi_sm_start(&sm, false, false, false, 0);
|
||||
a = nl_wifi_sm_onboard(&sm, 5000);
|
||||
TEST_ASSERT_FALSE(a.start_ap); /* already up */
|
||||
TEST_ASSERT_TRUE(a.connect);
|
||||
@@ -106,7 +106,7 @@ static void test_onboarding_success(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);
|
||||
a = nl_wifi_sm_event(&sm, NL_WIFI_EV_JOIN_FAILED, NL_WIFI_ERR_AUTH, 8000);
|
||||
TEST_ASSERT_FALSE(a.pulse_off);
|
||||
@@ -121,7 +121,7 @@ static void test_onboarding_failure_without_credentials(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);
|
||||
TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state);
|
||||
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
|
||||
* 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_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);
|
||||
@@ -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)
|
||||
{
|
||||
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);
|
||||
a = nl_wifi_sm_onboard(&sm, 2000);
|
||||
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
|
||||
* 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);
|
||||
TEST_ASSERT_EQUAL(NL_WIFI_ST_ONBOARD_TEST, sm.state);
|
||||
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)
|
||||
{
|
||||
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, 30000).connect); /* retry scheduled at 31000 */
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
@@ -218,5 +469,26 @@ int main(void)
|
||||
RUN_TEST(test_onboard_while_test_running_disconnects_stale_attempt);
|
||||
RUN_TEST(test_error_names);
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user