#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_timer.h" #include "esp_wifi.h" #include "nvs_flash.h" #include "lwip/sockets.h" #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" #include "freertos/task.h" #include #include static const char *TAG = "net"; #define BIT_CONNECTED 1 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 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); } 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); } } 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); 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_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)); 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)); 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"); } 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_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 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); 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 esp_err_t page_handler(httpd_req_t *req) { httpd_resp_set_type(req, "text/html"); return httpd_resp_sendstr(req, PAGE); } // 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"); 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 esp_err_t save_handler(httpd_req_t *req) { 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"); 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; used += n; } body[used] = 0; 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; 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.

"); 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 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; // not a query int qd = (buf[4] << 8) | buf[5]; if (qd < 1) continue; int p = 12; // walk the first QNAME 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[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; 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 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, &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); }