From 222888bf1fe9b334bbee8a62f9e1d3f4a9a4871e Mon Sep 17 00:00:00 2001 From: Juli Date: Thu, 17 Sep 2026 20:12:13 +0200 Subject: [PATCH] Multi-SSID Wi-Fi with a saved-network list; fix could-not-save Wi-Fi credentials now live in the widget's own NVS (up to 8 networks) instead of the driver's single slot. The save handler just appends to the list and reboots, so it no longer calls esp_wifi_set_config in APSTA mode -- the source of the intermittent could-not-save. A connect task cycles the saved networks newest-first and holds the first that connects, re-scanning on drop, so the widget follows you between locations. The portal lists saved networks with a Forget button and an add form. An existing driver-stored credential is migrated on first boot. Verified end to end over the LAN: save persists across reboot, forget removes. --- README.md | 6 +- main/net.c | 247 +++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 197 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 8ea1e77..ae478ed 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,11 @@ and stores its choices in NVS. With no saved network the widget opens the open hotspot **insecam-setup**; join it with a phone and the captive portal (or `http://192.168.4.1`) asks for -your Wi-Fi. Holding the knob down while the widget starts forces the portal. +your Wi-Fi. It remembers up to 8 networks and tries them newest-first at boot, +so the widget follows you between locations; the portal lists the saved ones +with a **Forget** button and a form to add another. Holding the knob down while +the widget starts, or choosing **Wi-Fi setup** in the menu, reopens the portal +even when already connected. ## Controls diff --git a/main/net.c b/main/net.c index a6868df..5aa7026 100644 --- a/main/net.c +++ b/main/net.c @@ -4,35 +4,93 @@ #include "esp_http_server.h" #include "esp_log.h" #include "esp_netif.h" -#include "esp_timer.h" #include "esp_wifi.h" +#include "nvs.h" #include "nvs_flash.h" #include "lwip/sockets.h" #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" #include "freertos/task.h" #include +#include #include static const char *TAG = "net"; #define BIT_CONNECTED 1 +#define MAX_NETS 8 +#define ATTEMPT_MS 9000 + +typedef struct { char ssid[33]; char pass[65]; } cred_t; static EventGroupHandle_t events; -static esp_timer_handle_t reconnect_timer; static char ip_str[16] = "0.0.0.0"; static bool portal_started; -static void reconnect_cb(void *arg) { esp_wifi_connect(); } +static cred_t nets[MAX_NETS]; +static int nets_count; +static nvs_handle_t nvs; + +// ---------------------------------------------------------------- saved networks + +static void nets_load(void) +{ + uint8_t n = 0; + nvs_get_u8(nvs, "cnt", &n); + if (n > MAX_NETS) n = MAX_NETS; + nets_count = 0; + for (int i = 0; i < n; i++) { + char key[8]; + snprintf(key, sizeof key, "n%d", i); + size_t len = sizeof(cred_t); + if (nvs_get_blob(nvs, key, &nets[nets_count], &len) == ESP_OK && len == sizeof(cred_t) && nets[nets_count].ssid[0]) + nets_count++; + } +} + +static void nets_save(void) +{ + for (int i = 0; i < MAX_NETS; i++) { + char key[8]; + snprintf(key, sizeof key, "n%d", i); + if (i < nets_count) nvs_set_blob(nvs, key, &nets[i], sizeof(cred_t)); + else nvs_erase_key(nvs, key); + } + nvs_set_u8(nvs, "cnt", nets_count); + nvs_commit(nvs); +} + +// Newest network goes to the front (tried first); a repeat SSID just updates its password. +static void nets_add(const char *ssid, const char *pass) +{ + int existing = -1; + for (int i = 0; i < nets_count; i++) if (!strcmp(nets[i].ssid, ssid)) existing = i; + cred_t c = {0}; + strlcpy(c.ssid, ssid, sizeof c.ssid); + strlcpy(c.pass, pass, sizeof c.pass); + if (existing >= 0) { + memmove(&nets[1], &nets[0], existing * sizeof(cred_t)); + } else { + if (nets_count < MAX_NETS) nets_count++; + memmove(&nets[1], &nets[0], (nets_count - 1) * sizeof(cred_t)); + } + nets[0] = c; + nets_save(); +} + +static void nets_forget(int idx) +{ + if (idx < 0 || idx >= nets_count) return; + memmove(&nets[idx], &nets[idx + 1], (nets_count - idx - 1) * sizeof(cred_t)); + nets_count--; + nets_save(); +} + +// ---------------------------------------------------------------- station static void wifi_event(void *arg, esp_event_base_t base, int32_t id, void *data) { - if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) { - esp_wifi_connect(); - } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { - wifi_event_sta_disconnected_t *d = data; - xEventGroupClearBits(events, BIT_CONNECTED); - ESP_LOGW(TAG, "disconnected (reason %d), retrying", d->reason); - esp_timer_start_once(reconnect_timer, 2000 * 1000); + if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { + xEventGroupClearBits(events, BIT_CONNECTED); // the connect task drives retries } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *e = data; snprintf(ip_str, sizeof ip_str, IPSTR, IP2STR(&e->ip_info.ip)); @@ -41,6 +99,33 @@ static void wifi_event(void *arg, esp_event_base_t base, int32_t id, void *data) } } +// Cycle the saved networks, newest first, until one connects; hold it until it drops. +static void wifi_task(void *arg) +{ + while (1) { + if (nets_count == 0) { vTaskDelay(pdMS_TO_TICKS(1000)); continue; } + bool connected = false; + for (int i = 0; i < nets_count && !connected; i++) { + wifi_config_t cfg = {0}; + strlcpy((char *)cfg.sta.ssid, nets[i].ssid, sizeof cfg.sta.ssid); + strlcpy((char *)cfg.sta.password, nets[i].pass, sizeof cfg.sta.password); + cfg.sta.threshold.authmode = nets[i].pass[0] ? WIFI_AUTH_WPA_PSK : WIFI_AUTH_OPEN; + esp_wifi_disconnect(); + xEventGroupClearBits(events, BIT_CONNECTED); + if (esp_wifi_set_config(WIFI_IF_STA, &cfg) != ESP_OK) continue; + ESP_LOGI(TAG, "trying \"%s\"", nets[i].ssid); + if (esp_wifi_connect() != ESP_OK) { vTaskDelay(pdMS_TO_TICKS(300)); continue; } + if (xEventGroupWaitBits(events, BIT_CONNECTED, pdFALSE, pdFALSE, pdMS_TO_TICKS(ATTEMPT_MS)) & BIT_CONNECTED) { + ESP_LOGI(TAG, "connected to \"%s\"", nets[i].ssid); + connected = true; + while (xEventGroupGetBits(events) & BIT_CONNECTED) vTaskDelay(pdMS_TO_TICKS(1000)); + ESP_LOGW(TAG, "lost \"%s\", rescanning", nets[i].ssid); + } + } + if (!connected) vTaskDelay(pdMS_TO_TICKS(3000)); // all failed; pause before another sweep + } +} + void net_init(void) { esp_err_t err = nvs_flash_init(); @@ -49,6 +134,8 @@ void net_init(void) err = nvs_flash_init(); } ESP_ERROR_CHECK(err); + ESP_ERROR_CHECK(nvs_open("wifinets", NVS_READWRITE, &nvs)); + nets_load(); events = xEventGroupCreate(); ESP_ERROR_CHECK(esp_netif_init()); @@ -58,27 +145,28 @@ void net_init(void) wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); - ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_FLASH)); - esp_timer_create_args_t targs = {.callback = reconnect_cb, .name = "wifi_retry"}; - ESP_ERROR_CHECK(esp_timer_create(&targs, &reconnect_timer)); ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event, NULL)); + + // One-time migration: pull a credential saved by an older firmware (driver NVS) into the list. + if (nets_count == 0) { + wifi_config_t old = {0}; + if (esp_wifi_get_config(WIFI_IF_STA, &old) == ESP_OK && old.sta.ssid[0]) { + nets_add((char *)old.sta.ssid, (char *)old.sta.password); + ESP_LOGI(TAG, "migrated stored network \"%s\"", (char *)old.sta.ssid); + } + } + + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); // the list above is the source of truth ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); + xTaskCreate(wifi_task, "wifi", 4096, NULL, 6, NULL); - wifi_config_t stored = {0}; - if (esp_wifi_get_config(WIFI_IF_STA, &stored) == ESP_OK && stored.sta.ssid[0]) - ESP_LOGI(TAG, "stored credentials for \"%s\"", (char *)stored.sta.ssid); - else - ESP_LOGW(TAG, "no stored Wi-Fi credentials"); + ESP_LOGI(TAG, "%d saved network%s", nets_count, nets_count == 1 ? "" : "s"); } -bool net_has_credentials(void) -{ - wifi_config_t stored = {0}; - return esp_wifi_get_config(WIFI_IF_STA, &stored) == ESP_OK && stored.sta.ssid[0]; -} +bool net_has_credentials(void) { return nets_count > 0; } bool net_wait_connected(int timeout_ms) { @@ -91,15 +179,6 @@ const char *net_ip_str(void) { return ip_str; } // ---------------------------------------------------------------- captive portal -static const char *PAGE = - "insecam widget setup" - "" - "

insecam desk widget

Give the widget your Wi-Fi so it can reach the cameras.

" - "
" - "
"; - static bool form_value(const char *body, const char *key, char *out, size_t cap) { size_t klen = strlen(key); @@ -129,14 +208,60 @@ static bool form_value(const char *body, const char *key, char *out, size_t cap) return false; } +static void send_escaped(httpd_req_t *req, const char *s) +{ + char buf[128]; + size_t n = 0; + for (; *s; s++) { + const char *rep = *s == '&' ? "&" : *s == '<' ? "<" : *s == '>' ? ">" : + *s == '"' ? """ : NULL; + const char one[2] = {*s, 0}; + const char *piece = rep ? rep : one; + size_t pl = strlen(piece); + if (n + pl >= sizeof buf) { httpd_resp_send_chunk(req, buf, n); n = 0; } + memcpy(buf + n, piece, pl); + n += pl; + } + if (n) httpd_resp_send_chunk(req, buf, n); +} + static esp_err_t page_handler(httpd_req_t *req) { httpd_resp_set_type(req, "text/html"); - return httpd_resp_sendstr(req, PAGE); + httpd_resp_sendstr_chunk(req, + "insecam widget setup" + "" + "

insecam desk widget

"); + + if (nets_count) { + httpd_resp_sendstr_chunk(req, "

Saved networks (tried newest first):

    "); + for (int i = 0; i < nets_count; i++) { + httpd_resp_sendstr_chunk(req, "
  • "); + send_escaped(req, nets[i].ssid); + char btn[128]; + snprintf(btn, sizeof btn, "
    " + "
  • ", i); + httpd_resp_sendstr_chunk(req, btn); + } + httpd_resp_sendstr_chunk(req, "
"); + } else { + httpd_resp_sendstr_chunk(req, "

Give the widget your Wi-Fi so it can reach the cameras.

"); + } + + httpd_resp_sendstr_chunk(req, + "
" + "" + "
"); + httpd_resp_sendstr_chunk(req, NULL); + return ESP_OK; } -// Anything else (Android's /generate_204, Apple's /hotspot-detect.html, ...) bounces to the form, -// which is what makes phones pop the "sign in to network" sheet. static esp_err_t redirect_handler(httpd_req_t *req) { httpd_resp_set_status(req, "302 Found"); @@ -150,38 +275,47 @@ static void restart_task(void *arg) esp_restart(); } -static esp_err_t save_handler(httpd_req_t *req) +static bool read_body(httpd_req_t *req, char *body, size_t cap) { - char body[512], ssid[33], password[65]; - if (req->content_len <= 0 || req->content_len >= sizeof body) - return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "bad form"); + if (req->content_len <= 0 || req->content_len >= cap) return false; size_t used = 0; while (used < req->content_len) { int n = httpd_req_recv(req, body + used, req->content_len - used); - if (n <= 0) return ESP_FAIL; + if (n <= 0) return false; used += n; } body[used] = 0; + return true; +} + +static esp_err_t save_handler(httpd_req_t *req) +{ + char body[512], ssid[33], password[65]; + if (!read_body(req, body, sizeof body)) + return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "bad form"); if (!form_value(body, "ssid", ssid, sizeof ssid) || !ssid[0]) return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Wi-Fi name missing"); form_value(body, "password", password, sizeof password); - - wifi_config_t cfg = {0}; - memcpy(cfg.sta.ssid, ssid, strlen(ssid)); - memcpy(cfg.sta.password, password, strlen(password)); - cfg.sta.threshold.authmode = password[0] ? WIFI_AUTH_WPA_PSK : WIFI_AUTH_OPEN; + nets_add(ssid, password); memset(body, 0, sizeof body); memset(password, 0, sizeof password); - if (esp_wifi_set_config(WIFI_IF_STA, &cfg) != ESP_OK) - return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "could not save"); - memset(&cfg, 0, sizeof cfg); httpd_resp_set_type(req, "text/html"); - httpd_resp_sendstr(req, "

Saved. The widget is restarting.

"); + httpd_resp_sendstr(req, "" + "

Saved. The widget is restarting.

"); xTaskCreate(restart_task, "restart", 2048, NULL, 2, NULL); return ESP_OK; } -// Minimal DNS: answer every A query with 192.168.4.1 so captive-portal probes land on us. +static esp_err_t forget_handler(httpd_req_t *req) +{ + char body[64], idx[8]; + if (read_body(req, body, sizeof body) && form_value(body, "i", idx, sizeof idx)) + nets_forget(atoi(idx)); + httpd_resp_set_status(req, "303 See Other"); + httpd_resp_set_hdr(req, "Location", "/"); + return httpd_resp_sendstr(req, ""); +} + static void dns_task(void *arg) { int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); @@ -196,17 +330,17 @@ static void dns_task(void *arg) struct sockaddr_in from; socklen_t flen = sizeof from; int n = recvfrom(sock, buf, sizeof buf - 16, 0, (struct sockaddr *)&from, &flen); - if (n < 12 || (buf[2] & 0x80)) continue; // not a query + if (n < 12 || (buf[2] & 0x80)) continue; int qd = (buf[4] << 8) | buf[5]; if (qd < 1) continue; - int p = 12; // walk the first QNAME + int p = 12; while (p < n && buf[p]) p += buf[p] + 1; if (p + 5 > n) continue; int qtype = (buf[p + 1] << 8) | buf[p + 2]; int qend = p + 5; - buf[2] = 0x84; buf[3] = 0x00; // response, authoritative, no error - buf[4] = 0; buf[5] = 1; // one question - buf[6] = 0; buf[7] = qtype == 1 ? 1 : 0; // one answer for A queries + buf[2] = 0x84; buf[3] = 0x00; + buf[4] = 0; buf[5] = 1; + buf[6] = 0; buf[7] = qtype == 1 ? 1 : 0; buf[8] = buf[9] = buf[10] = buf[11] = 0; int len = qend; if (qtype == 1) { @@ -233,12 +367,15 @@ void net_start_portal(void) httpd_handle_t server; httpd_config_t hc = HTTPD_DEFAULT_CONFIG(); hc.uri_match_fn = httpd_uri_match_wildcard; + hc.max_uri_handlers = 12; ESP_ERROR_CHECK(httpd_start(&server, &hc)); httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = page_handler}; httpd_uri_t save = {.uri = "/save", .method = HTTP_POST, .handler = save_handler}; + httpd_uri_t forget = {.uri = "/forget", .method = HTTP_POST, .handler = forget_handler}; httpd_uri_t any = {.uri = "/*", .method = HTTP_GET, .handler = redirect_handler}; httpd_register_uri_handler(server, &root); httpd_register_uri_handler(server, &save); + httpd_register_uri_handler(server, &forget); httpd_register_uri_handler(server, &any); xTaskCreate(dns_task, "dns", 3072, NULL, 3, NULL); ESP_LOGI(TAG, "setup hotspot \"%s\" at http://192.168.4.1", CONFIG_WIDGET_AP_SSID);