#include "net.h" #include "sdkconfig.h" #include "esp_event.h" #include "esp_http_server.h" #include "esp_log.h" #include "esp_netif.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 char ip_str[16] = "0.0.0.0"; static bool portal_started; 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_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)); ESP_LOGI(TAG, "got IP %s", ip_str); xEventGroupSetBits(events, BIT_CONNECTED); } } // 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(); if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); 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()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_t *sta = esp_netif_create_default_wifi_sta(); esp_netif_set_hostname(sta, "insecam-widget"); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); 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); ESP_LOGI(TAG, "%d saved network%s", nets_count, nets_count == 1 ? "" : "s"); } bool net_has_credentials(void) { return nets_count > 0; } bool net_wait_connected(int timeout_ms) { TickType_t ticks = timeout_ms < 0 ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms); return (xEventGroupWaitBits(events, BIT_CONNECTED, pdFALSE, pdFALSE, ticks) & BIT_CONNECTED) != 0; } bool net_is_connected(void) { return (xEventGroupGetBits(events) & BIT_CONNECTED) != 0; } const char *net_ip_str(void) { return ip_str; } // ---------------------------------------------------------------- captive portal static bool form_value(const char *body, const char *key, char *out, size_t cap) { size_t klen = strlen(key); for (const char *p = body; *p;) { const char *end = strchr(p, '&'); if (!end) end = p + strlen(p); if ((size_t)(end - p) > klen && !strncmp(p, key, klen) && p[klen] == '=') { size_t n = 0; for (p += klen + 1; p < end; p++) { unsigned char ch = *p; if (ch == '+') ch = ' '; else if (ch == '%') { if (end - p < 3 || !isxdigit((unsigned char)p[1]) || !isxdigit((unsigned char)p[2])) return false; char hex[3] = {p[1], p[2], 0}; ch = strtoul(hex, NULL, 16); p += 2; } if (!ch || n + 1 >= cap) return false; out[n++] = ch; } out[n] = 0; return true; } p = *end ? end + 1 : end; } if (cap) out[0] = 0; 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"); 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; } static esp_err_t redirect_handler(httpd_req_t *req) { httpd_resp_set_status(req, "302 Found"); httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/"); return httpd_resp_sendstr(req, ""); } static void restart_task(void *arg) { vTaskDelay(pdMS_TO_TICKS(1500)); esp_restart(); } static bool read_body(httpd_req_t *req, char *body, size_t cap) { 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 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); nets_add(ssid, password); memset(body, 0, sizeof body); memset(password, 0, sizeof password); httpd_resp_set_type(req, "text/html"); httpd_resp_sendstr(req, "" "

Saved. The widget is restarting.

"); xTaskCreate(restart_task, "restart", 2048, NULL, 2, NULL); return ESP_OK; } 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); struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(53), .sin_addr.s_addr = htonl(INADDR_ANY)}; if (sock < 0 || bind(sock, (struct sockaddr *)&addr, sizeof addr) < 0) { ESP_LOGE(TAG, "dns socket failed"); vTaskDelete(NULL); return; } uint8_t buf[512]; while (1) { 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; int qd = (buf[4] << 8) | buf[5]; if (qd < 1) continue; 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; 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) { static const uint8_t ans[] = {0xC0, 0x0C, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4, 192, 168, 4, 1}; memcpy(buf + len, ans, sizeof ans); len += sizeof ans; } sendto(sock, buf, len, 0, (struct sockaddr *)&from, flen); } } void net_start_portal(void) { if (portal_started) return; portal_started = true; esp_netif_create_default_wifi_ap(); wifi_config_t ap = {.ap = {.channel = 1, .max_connection = 4, .authmode = WIFI_AUTH_OPEN}}; strlcpy((char *)ap.ap.ssid, CONFIG_WIDGET_AP_SSID, sizeof ap.ap.ssid); ap.ap.ssid_len = strlen((char *)ap.ap.ssid); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap)); 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); }