From 18eb7b5c013bdafc629fb7f1d433861061304314 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Fri, 2 Oct 2026 00:52:52 +0200 Subject: [PATCH] feat(wifi): Standalone-AP im Treiber und im Startpfad --- src/app_main.c | 29 +++++++- src/net/nl_wifi.h | 15 +++- src/net/nl_wifi_w600.c | 155 +++++++++++++++++++++++++++++++++++++---- src/sys/nl_app.h | 5 ++ src/sys/nl_app_api.c | 14 ++++ 5 files changed, 198 insertions(+), 20 deletions(-) diff --git a/src/app_main.c b/src/app_main.c index 121cbbc..fa6ba1a 100644 --- a/src/app_main.c +++ b/src/app_main.c @@ -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()); diff --git a/src/net/nl_wifi.h b/src/net/nl_wifi.h index 7704815..c25f4f9 100644 --- a/src/net/nl_wifi.h +++ b/src/net/nl_wifi.h @@ -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); diff --git a/src/net/nl_wifi_w600.c b/src/net/nl_wifi_w600.c index 48c6776..0107203 100644 --- a/src/net/nl_wifi_w600.c +++ b/src/net/nl_wifi_w600.c @@ -16,8 +16,12 @@ #define NL_NET_PRIO 35 #define NL_NET_QUEUE 8 -#define MSG_NETIF 0x100u /* | netif status */ -#define MSG_ONBOARD 0x200u +#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,18 @@ 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; +/* 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 +104,43 @@ void nl_wifi_disable_sdk_auto_connect(void) tls_wifi_disconnect(); } +/* 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; + 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 +149,17 @@ 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) { + 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; + } } static void connect_station(bool test) @@ -149,6 +197,12 @@ 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"); + nl_app_clear_standalone(); + } if (a.pulse_on) pulse(true); if (a.pulse_off) @@ -157,9 +211,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"); @@ -186,6 +238,7 @@ static void net_task(void *arg) nl_wifi_actions_t ev, tick; uint32_t m = 0, now; bool got_ip_event = false, connected_now, ap_wanted, unexpected_softap; + bool recheck_ap = false; memset(&ev, 0, sizeof ev); if (tls_os_queue_receive(s_queue, &msg, 0, HZ / 10) == TLS_OS_SUCCESS) @@ -202,6 +255,28 @@ 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 { + /* Zurueck auf die Vorgaben: ein spaeterer Rueckfall- oder Onboarding-AP ist + * wieder der gewohnte "ninlight-" mit dem einkompilierten Passwort, + * genau wie bei einer Lampe, die nie standalone war. */ + s_ap_ssid[0] = '\0'; + copy(s_ap_pass, NINLIGHT_AP_PASSWORD, sizeof s_ap_pass); + s_ap_channel = 6; + ev = nl_wifi_sm_leave_standalone(&s_sm, s_ssid[0] != '\0', now); + } + 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. */ @@ -217,13 +292,16 @@ static void net_task(void *arg) 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)); 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(); @@ -237,7 +315,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; @@ -249,6 +328,8 @@ void nl_wifi_start(const char *ssid, const char *pass, const char *ap_pass) copy(s_ssid, ssid, sizeof s_ssid); copy(s_pass, pass, sizeof s_pass); copy(s_ap_pass, ap_pass, sizeof s_ap_pass); + copy(s_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 +337,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 +348,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', false, false, 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 +375,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; diff --git a/src/sys/nl_app.h b/src/sys/nl_app.h index df355e7..ee119ad 100644 --- a/src/sys/nl_app.h +++ b/src/sys/nl_app.h @@ -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 diff --git a/src/sys/nl_app_api.c b/src/sys/nl_app_api.c index be04c4e..1f66782 100644 --- a/src/sys/nl_app_api.c +++ b/src/sys/nl_app_api.c @@ -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; @@ -108,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(); } @@ -131,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;