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
+2 -2
View File
@@ -1,3 +1,3 @@
idf_component_register(SRCS main.c
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_http_server dns_server
idf_component_register(SRCS main.c wifi_provisioning.c frame_client.c
PRIV_REQUIRES esp_event nvs_flash esp_wifi esp_netif esp_http_server dns_server
EMBED_FILES root.html)
+24 -11
View File
@@ -1,16 +1,14 @@
menu "Example Configuration"
menu "ESPresso Frame Configuration"
config ESP_WIFI_SSID
string "SoftAP SSID"
default "esp32_ssid"
config ESP_AP_SSID
string "Provisioning softAP SSID"
default "ESPRESSO"
help
SSID (network name) to set up the softAP with.
config ESP_WIFI_PASSWORD
string "SoftAP Password"
default "esp32_pwd"
help
WiFi password (WPA or WPA2) for the example to use for the softAP.
SSID of the softAP the device brings up when it needs provisioning.
The password is generated randomly per device on first boot and
shown on the e-ink panel both as a WiFi-join QR code and as
plaintext underneath it, so it can be typed in by hand on a
desktop/laptop that can't scan the QR.
config ESP_MAX_STA_CONN
int "Maximal STA connections"
@@ -29,4 +27,19 @@ menu "Example Configuration"
default y
help
Enables more modern DHCP-based Option 114 to provide clients with the captive portal URI
config FRAME_STA_CONNECT_MAX_RETRIES
int "Home WiFi connect max retries before falling back to provisioning"
default 3
help
Number of failed connection attempts to the stored home WiFi network
before the device gives up and re-enters provisioning (softAP) mode.
config FRAME_STA_CONNECT_TIMEOUT_MS
int "Home WiFi connect timeout per attempt (ms)"
default 15000
help
How long to wait for an IP address on each connection attempt to the
stored home WiFi network before counting it as a failed retry.
endmenu
+114
View File
@@ -0,0 +1,114 @@
#include <string.h>
#include "esp_event.h"
#include "esp_log.h"
#include "esp_wifi.h"
#include "esp_netif.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "frame_client.h"
static const char *TAG = "frame_client";
#define STA_CONNECTED_BIT BIT0
#define STA_FAILED_BIT BIT1
static EventGroupHandle_t s_sta_event_group;
/* wifi_sta_config_t's ssid/password fields are fixed-size byte arrays, not
* necessarily null-terminated (a full 32-char SSID fills the field exactly).
* snprintf() flags that as a possible truncation at -Werror, so copy by
* hand instead. */
static void copy_wifi_field(uint8_t *dst, size_t dst_size, const char *src)
{
size_t len = strnlen(src, dst_size);
memcpy(dst, src, len);
if (len < dst_size) {
dst[len] = '\0';
}
}
static void sta_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
ESP_LOGW(TAG, "Disconnected from home WiFi");
xEventGroupSetBits(s_sta_event_group, STA_FAILED_BIT);
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
xEventGroupSetBits(s_sta_event_group, STA_CONNECTED_BIT);
}
}
esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg)
{
s_sta_event_group = xEventGroupCreate();
esp_netif_create_default_wifi_sta();
wifi_init_config_t init_cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&init_cfg));
esp_event_handler_instance_t wifi_handler;
esp_event_handler_instance_t ip_handler;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &sta_event_handler, NULL, &wifi_handler));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &sta_event_handler, NULL, &ip_handler));
wifi_config_t wifi_config = {0};
copy_wifi_field(wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid), cfg->sta_ssid);
copy_wifi_field(wifi_config.sta.password, sizeof(wifi_config.sta.password), cfg->sta_password);
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
esp_err_t result = ESP_FAIL;
for (int attempt = 1; attempt <= CONFIG_FRAME_STA_CONNECT_MAX_RETRIES; attempt++) {
ESP_LOGI(TAG, "Connecting to '%s' (attempt %d/%d)", cfg->sta_ssid, attempt,
CONFIG_FRAME_STA_CONNECT_MAX_RETRIES);
xEventGroupClearBits(s_sta_event_group, STA_CONNECTED_BIT | STA_FAILED_BIT);
esp_wifi_connect();
EventBits_t bits = xEventGroupWaitBits(s_sta_event_group, STA_CONNECTED_BIT | STA_FAILED_BIT,
pdTRUE, pdFALSE,
pdMS_TO_TICKS(CONFIG_FRAME_STA_CONNECT_TIMEOUT_MS));
if (bits & STA_CONNECTED_BIT) {
result = ESP_OK;
break;
}
ESP_LOGW(TAG, "Attempt %d/%d failed", attempt, CONFIG_FRAME_STA_CONNECT_MAX_RETRIES);
}
esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_handler);
esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, ip_handler);
vEventGroupDelete(s_sta_event_group);
s_sta_event_group = NULL;
if (result != ESP_OK) {
esp_wifi_stop();
}
return result;
}
void frame_client_run(const frame_config_t *cfg)
{
/* TODO(step 6): HTTP fetch from cfg->toolsserver -> epd_write_stream ->
* epd_refresh -> esp_deep_sleep. Idling here for now so the device
* stays observable over serial while STA connectivity is verified. */
ESP_LOGI(TAG, "Connected to home WiFi. toolsserver='%s' (fetch/display cycle not yet implemented)",
cfg->toolsserver);
while (1) {
ESP_LOGI(TAG, "heartbeat: still connected");
vTaskDelay(pdMS_TO_TICKS(60000));
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "esp_err.h"
#include "wifi_provisioning.h"
/**
* Connects to the home WiFi network described by cfg, retrying up to
* CONFIG_FRAME_STA_CONNECT_MAX_RETRIES times with a per-attempt timeout of
* CONFIG_FRAME_STA_CONNECT_TIMEOUT_MS. Returns ESP_OK once an IP address
* has been obtained, ESP_FAIL if all retries are exhausted.
*/
esp_err_t frame_wifi_connect_sta(const frame_config_t *cfg);
/**
* Runs the frame's normal-operation cycle: fetch the current image from
* cfg->toolsserver, display it, and deep-sleep until the next refresh.
*
* TODO(step 6): implement the HTTP fetch + EPD display + esp_deep_sleep
* cycle once the display driver (step 4) and server (step 3) exist. For
* now this just confirms STA connectivity survives a reboot.
*/
void frame_client_run(const frame_config_t *cfg);
+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();
}
+384
View File
@@ -0,0 +1,384 @@
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <sys/param.h>
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_random.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 "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "wifi_provisioning.h"
#define NVS_NAMESPACE "frame_cfg"
extern const char root_start[] asm("_binary_root_html_start");
extern const char root_end[] asm("_binary_root_html_end");
static const char *TAG = "wifi_provisioning";
/* Uppercase letters + digits, excluding visually ambiguous characters
* (0/O, 1/I/L) so a human can read the password off the panel and type it
* on a desktop/laptop that can't scan the QR code. */
static const char AP_PASSWORD_CHARSET[] = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
/* ------------------------------------------------------------------------
* NVS-backed config
* ---------------------------------------------------------------------- */
esp_err_t frame_config_load(frame_config_t *out)
{
memset(out, 0, sizeof(*out));
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK) {
return err;
}
size_t len = sizeof(out->sta_ssid);
err = nvs_get_str(handle, "sta_ssid", out->sta_ssid, &len);
if (err != ESP_OK) {
nvs_close(handle);
return err;
}
len = sizeof(out->toolsserver);
err = nvs_get_str(handle, "toolsserver", out->toolsserver, &len);
if (err != ESP_OK) {
nvs_close(handle);
return err;
}
/* Password is allowed to be empty (open home network), so a missing
* key just means "no password" rather than "not provisioned". */
len = sizeof(out->sta_password);
esp_err_t pass_err = nvs_get_str(handle, "sta_pass", out->sta_password, &len);
if (pass_err != ESP_OK && pass_err != ESP_ERR_NVS_NOT_FOUND) {
nvs_close(handle);
return pass_err;
}
nvs_close(handle);
return ESP_OK;
}
esp_err_t frame_config_save(const frame_config_t *cfg)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (err != ESP_OK) {
return err;
}
err = nvs_set_str(handle, "sta_ssid", cfg->sta_ssid);
if (err == ESP_OK) {
err = nvs_set_str(handle, "sta_pass", cfg->sta_password);
}
if (err == ESP_OK) {
err = nvs_set_str(handle, "toolsserver", cfg->toolsserver);
}
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
return err;
}
static void generate_ap_password(char *out, size_t out_size)
{
size_t len = MIN(FRAME_AP_PASSWORD_LEN, out_size - 1);
uint8_t random_bytes[FRAME_AP_PASSWORD_LEN];
esp_fill_random(random_bytes, len);
size_t charset_len = strlen(AP_PASSWORD_CHARSET);
for (size_t i = 0; i < len; i++) {
out[i] = AP_PASSWORD_CHARSET[random_bytes[i] % charset_len];
}
out[len] = '\0';
}
void ap_identity_get(char *ssid_out, size_t ssid_len, char *pass_out, size_t pass_len)
{
snprintf(ssid_out, ssid_len, "%s", CONFIG_ESP_AP_SSID);
nvs_handle_t handle;
ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle));
size_t len = pass_len;
esp_err_t err = nvs_get_str(handle, "ap_pass", pass_out, &len);
if (err == ESP_ERR_NVS_NOT_FOUND) {
generate_ap_password(pass_out, pass_len);
ESP_ERROR_CHECK(nvs_set_str(handle, "ap_pass", pass_out));
ESP_ERROR_CHECK(nvs_commit(handle));
} else {
ESP_ERROR_CHECK(err);
}
nvs_close(handle);
}
/* ------------------------------------------------------------------------
* Tiny application/x-www-form-urlencoded parser for the provisioning form
* ---------------------------------------------------------------------- */
static void url_decode(char *dst, const char *src, size_t dst_size)
{
size_t di = 0;
for (size_t si = 0; src[si] != '\0' && di + 1 < dst_size; si++) {
char c = src[si];
if (c == '+') {
dst[di++] = ' ';
} else if (c == '%' && isxdigit((unsigned char)src[si + 1]) && isxdigit((unsigned char)src[si + 2])) {
char hex[3] = { src[si + 1], src[si + 2], '\0' };
dst[di++] = (char) strtol(hex, NULL, 16);
si += 2;
} else {
dst[di++] = c;
}
}
dst[di] = '\0';
}
static void extract_form_value(const char *body, const char *key, char *out, size_t out_size)
{
out[0] = '\0';
size_t key_len = strlen(key);
const char *p = body;
while (p != NULL && *p != '\0') {
const char *amp = strchr(p, '&');
size_t token_len = amp ? (size_t)(amp - p) : strlen(p);
if (token_len > key_len && p[key_len] == '=' && strncmp(p, key, key_len) == 0) {
char raw[FRAME_CFG_SERVER_MAX_LEN + 1] = {0};
size_t value_len = token_len - key_len - 1;
if (value_len > sizeof(raw) - 1) {
value_len = sizeof(raw) - 1;
}
memcpy(raw, p + key_len + 1, value_len);
raw[value_len] = '\0';
url_decode(out, raw, out_size);
return;
}
if (amp == NULL) {
break;
}
p = amp + 1;
}
}
/* ------------------------------------------------------------------------
* HTTP handlers
* ---------------------------------------------------------------------- */
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
};
static esp_err_t save_config_post_handler(httpd_req_t *req)
{
if (req->content_len <= 0 || req->content_len > 512) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Form data too large");
return ESP_FAIL;
}
char body[513];
int received = 0;
while (received < req->content_len) {
int ret = httpd_req_recv(req, body + received, req->content_len - received);
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
continue;
}
if (ret <= 0) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read body");
return ESP_FAIL;
}
received += ret;
}
body[received] = '\0';
frame_config_t cfg = {0};
extract_form_value(body, "ssid", cfg.sta_ssid, sizeof(cfg.sta_ssid));
extract_form_value(body, "password", cfg.sta_password, sizeof(cfg.sta_password));
extract_form_value(body, "toolsserver", cfg.toolsserver, sizeof(cfg.toolsserver));
if (strlen(cfg.sta_ssid) == 0 || strlen(cfg.toolsserver) == 0) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "SSID and Tools Server are required");
return ESP_FAIL;
}
esp_err_t err = frame_config_save(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to save config: %s", esp_err_to_name(err));
httpd_resp_send_500(req);
return ESP_FAIL;
}
ESP_LOGI(TAG, "Saved config: ssid='%s' toolsserver='%s'", cfg.sta_ssid, cfg.toolsserver);
static const char resp[] =
"<html><body><h3>Saved. Restarting and connecting to your WiFi...</h3></body></html>";
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
/* Let the response flush to the client before rebooting into STA mode. */
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return ESP_OK;
}
static const httpd_uri_t save_config = {
.uri = "/save_config",
.method = HTTP_POST,
.handler = save_config_post_handler
};
/* Redirects all unmatched requests to the captive portal root page. */
static esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
{
httpd_resp_set_status(req, "303 See Other");
httpd_resp_set_hdr(req, "Location", "/");
/* iOS requires content in the response to detect a captive portal. */
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;
ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
if (httpd_start(&server, &config) == ESP_OK) {
ESP_LOGI(TAG, "Registering URI handlers");
httpd_register_uri_handler(server, &root);
httpd_register_uri_handler(server, &save_config);
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
}
return server;
}
/* ------------------------------------------------------------------------
* softAP + captive portal bring-up
* ---------------------------------------------------------------------- */
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(const char *ap_ssid, const char *ap_password)
{
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 = {
.max_connection = CONFIG_ESP_MAX_STA_CONN,
.authmode = WIFI_AUTH_WPA_WPA2_PSK
},
};
snprintf((char *)wifi_config.ap.ssid, sizeof(wifi_config.ap.ssid), "%s", ap_ssid);
wifi_config.ap.ssid_len = strlen(ap_ssid);
snprintf((char *)wifi_config.ap.password, sizeof(wifi_config.ap.password), "%s", ap_password);
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'", ap_ssid, ap_password);
}
#ifdef CONFIG_ESP_ENABLE_DHCP_CAPTIVEPORTAL
static void dhcp_set_captiveportal_url(void)
{
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);
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);
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
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));
free(captiveportal_uri);
}
#endif // CONFIG_ESP_ENABLE_DHCP_CAPTIVEPORTAL
void wifi_provisioning_start(void)
{
char ap_ssid[FRAME_CFG_SSID_MAX_LEN + 1];
char ap_password[FRAME_AP_PASSWORD_LEN + 1];
ap_identity_get(ap_ssid, sizeof(ap_ssid), ap_password, sizeof(ap_password));
/* TODO(step 5): draw the WiFi-join QR code + plaintext password on the
* e-ink panel here, before the AP goes up, so the join instructions are
* always visible by the time the network is joinable. */
ESP_LOGI(TAG, "Provisioning AP: SSID='%s' password='%s'", ap_ssid, ap_password);
esp_netif_create_default_wifi_ap();
wifi_init_softap(ap_ssid, ap_password);
#ifdef CONFIG_ESP_ENABLE_DHCP_CAPTIVEPORTAL
dhcp_set_captiveportal_url();
#endif
start_webserver();
dns_server_config_t dns_config = DNS_SERVER_CONFIG_SINGLE("*" /* all A queries */, "WIFI_AP_DEF");
start_dns_server(&dns_config);
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <stddef.h>
#include "esp_err.h"
#define FRAME_CFG_SSID_MAX_LEN 32
#define FRAME_CFG_PASSWORD_MAX_LEN 64
#define FRAME_CFG_SERVER_MAX_LEN 128
#define FRAME_AP_PASSWORD_LEN 10
typedef struct {
char sta_ssid[FRAME_CFG_SSID_MAX_LEN + 1];
char sta_password[FRAME_CFG_PASSWORD_MAX_LEN + 1];
char toolsserver[FRAME_CFG_SERVER_MAX_LEN + 1];
} frame_config_t;
/**
* Loads the saved home-network config from NVS.
* Returns ESP_ERR_NVS_NOT_FOUND if the device has never been provisioned.
*/
esp_err_t frame_config_load(frame_config_t *out);
/** Saves the home-network config to NVS. */
esp_err_t frame_config_save(const frame_config_t *cfg);
/**
* Returns this device's provisioning AP identity: a fixed SSID (from
* Kconfig) and a password that's generated once on first use and persisted
* in NVS from then on. The password is drawn from an easy-to-type charset
* since it's shown on the e-ink panel (as both a QR code and plaintext) and
* may need to be typed in by hand.
*/
void ap_identity_get(char *ssid_out, size_t ssid_len, char *pass_out, size_t pass_len);
/**
* Brings up the ESPRESSO softAP + captive portal (DNS + HTTP) so the user
* can provision the device. Does not return.
*/
void wifi_provisioning_start(void);