Finish captive portal: NVS config, POST handler, STA connect, AP identity

Splits provisioning (softAP + captive portal + NVS-backed config) into
wifi_provisioning.c and home-network connection into frame_client.c.
/save_config now parses the form body and persists it to NVS; on boot the
device goes straight to STA mode if a config exists, retrying a few times
before falling back to provisioning if the home network is unreachable.

The provisioning AP is now always named ESPRESSO with a random per-device
password (generated once, persisted in NVS) instead of a fixed Kconfig
value, drawn from a charset that avoids visually ambiguous characters since
it'll be read off the e-ink panel and possibly typed by hand.
This commit is contained in:
2026-07-18 10:48:52 -04:00
parent cb50c99c98
commit de449d5973
7 changed files with 610 additions and 189 deletions
+25 -176
View File
@@ -1,195 +1,44 @@
/* Captive Portal Example
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <sys/param.h>
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "lwip/inet.h"
#include "esp_http_server.h"
#include "dns_server.h"
#include "wifi_provisioning.h"
#include "frame_client.h"
#define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
#define EXAMPLE_MAX_STA_CONN CONFIG_ESP_MAX_STA_CONN
extern const char root_start[] asm("_binary_root_html_start");
extern const char root_end[] asm("_binary_root_html_end");
static const char *TAG = "example";
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *)event_data;
ESP_LOGI(TAG, "station " MACSTR " join, AID=%d",
MAC2STR(event->mac), event->aid);
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *)event_data;
ESP_LOGI(TAG, "station " MACSTR " leave, AID=%d, reason=%d",
MAC2STR(event->mac), event->aid, event->reason);
}
}
static void wifi_init_softap(void)
{
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_handler, NULL));
wifi_config_t wifi_config = {
.ap = {
.ssid = EXAMPLE_ESP_WIFI_SSID,
.ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
.password = EXAMPLE_ESP_WIFI_PASS,
.max_connection = EXAMPLE_MAX_STA_CONN,
.authmode = WIFI_AUTH_WPA_WPA2_PSK
},
};
if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
}
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ip_info);
char ip_addr[16];
inet_ntoa_r(ip_info.ip.addr, ip_addr, 16);
ESP_LOGI(TAG, "Set up softAP with IP: %s", ip_addr);
ESP_LOGI(TAG, "wifi_init_softap finished. SSID:'%s' password:'%s'",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
}
#ifdef CONFIG_ESP_ENABLE_DHCP_CAPTIVEPORTAL
static void dhcp_set_captiveportal_url(void) {
// get the IP of the access point to redirect to
esp_netif_ip_info_t ip_info;
esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ip_info);
char ip_addr[16];
inet_ntoa_r(ip_info.ip.addr, ip_addr, 16);
ESP_LOGI(TAG, "Set up softAP with IP: %s", ip_addr);
// turn the IP into a URI
char* captiveportal_uri = (char*) malloc(32 * sizeof(char));
assert(captiveportal_uri && "Failed to allocate captiveportal_uri");
strcpy(captiveportal_uri, "http://");
strcat(captiveportal_uri, ip_addr);
// get a handle to configure DHCP with
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
// set the DHCP option 114
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_stop(netif));
ESP_ERROR_CHECK(esp_netif_dhcps_option(netif, ESP_NETIF_OP_SET, ESP_NETIF_CAPTIVEPORTAL_URI, captiveportal_uri, strlen(captiveportal_uri)));
ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_start(netif));
}
#endif // CONFIG_ESP_ENABLE_DHCP_CAPTIVEPORTAL
// HTTP GET Handler
static esp_err_t root_get_handler(httpd_req_t *req)
{
const uint32_t root_len = root_end - root_start;
ESP_LOGI(TAG, "Serve root");
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, root_start, root_len);
return ESP_OK;
}
static const httpd_uri_t root = {
.uri = "/",
.method = HTTP_GET,
.handler = root_get_handler
};
// HTTP Error (404) Handler - Redirects all requests to the root page
esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
{
// Set status
httpd_resp_set_status(req, "303 See Other");
// Redirect to the "/" root directory
httpd_resp_set_hdr(req, "Location", "/");
// iOS requires content in the response to detect a captive portal, simply redirecting is not sufficient.
httpd_resp_send(req, "Redirect to the captive portal", HTTPD_RESP_USE_STRLEN);
ESP_LOGI(TAG, "Redirecting to root");
return ESP_OK;
}
static httpd_handle_t start_webserver(void)
{
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_open_sockets = 13;
config.lru_purge_enable = true;
// Start the httpd server
ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
if (httpd_start(&server, &config) == ESP_OK) {
// Set URI handlers
ESP_LOGI(TAG, "Registering URI handlers");
httpd_register_uri_handler(server, &root);
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
}
return server;
}
static const char *TAG = "main";
void app_main(void)
{
/*
Turn of warnings from HTTP server as redirecting traffic will yield
lots of invalid requests
*/
/* Redirected/invalid captive-portal traffic generates a lot of noise
* at the default log level. */
esp_log_level_set("httpd_uri", ESP_LOG_ERROR);
esp_log_level_set("httpd_txrx", ESP_LOG_ERROR);
esp_log_level_set("httpd_parse", ESP_LOG_ERROR);
// Initialize networking stack
ESP_ERROR_CHECK(esp_netif_init());
// Create default event loop needed by the main app
ESP_ERROR_CHECK(esp_event_loop_create_default());
// Initialize NVS needed by Wi-Fi
ESP_ERROR_CHECK(nvs_flash_init());
esp_err_t nvs_err = nvs_flash_init();
if (nvs_err == ESP_ERR_NVS_NO_FREE_PAGES || nvs_err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
nvs_err = nvs_flash_init();
}
ESP_ERROR_CHECK(nvs_err);
// Initialize Wi-Fi including netif with default config
esp_netif_create_default_wifi_ap();
frame_config_t cfg;
esp_err_t cfg_err = frame_config_load(&cfg);
if (cfg_err == ESP_OK) {
ESP_LOGI(TAG, "Found stored config for '%s', connecting to home WiFi", cfg.sta_ssid);
if (frame_wifi_connect_sta(&cfg) == ESP_OK) {
frame_client_run(&cfg);
return; /* frame_client_run currently never returns */
}
ESP_LOGW(TAG, "Could not connect to stored WiFi after %d attempts, falling back to provisioning",
CONFIG_FRAME_STA_CONNECT_MAX_RETRIES);
} else {
ESP_LOGI(TAG, "No stored config found (%s), starting provisioning", esp_err_to_name(cfg_err));
}
// Initialise ESP32 in SoftAP mode
wifi_init_softap();
// Configure DNS-based captive portal, if configured
#ifdef CONFIG_ESP_ENABLE_DHCP_CAPTIVEPORTAL
dhcp_set_captiveportal_url();
#endif
// Start the server for the first time
start_webserver();
// Start the DNS server that will redirect all queries to the softAP IP
dns_server_config_t config = DNS_SERVER_CONFIG_SINGLE("*" /* all A queries */, "WIFI_AP_DEF" /* softAP netif ID */);
start_dns_server(&config);
wifi_provisioning_start();
}