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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user