Server: Frame.panel_type (new column + migration) is auto-derived from the device's reported board (X-Frame-Board), never user-set -- the panel is a property of the hardware, not a picker in the UI. image_pipeline's packing/render pipeline is parameterized by panel geometry instead of hardcoded 800x480 globals, with the real confirmed 13.3in geometry (1600x1200) registered alongside the original 7.3in panel. Existing 7.3in frames are unaffected (column default + board mapping both resolve to the original panel). Board identifiers are also renamed (devkit/xiao -> devkit_esp32c6/ xiao_esp32c6, plus new "ee02") since the EE02 board also carries a XIAO module -- "xiao" alone stopped disambiguating hardware. The server keeps accepting the legacy bare names indefinitely for already-flashed devices. Firmware: scaffolds a third build target (ee02, ESP32-S3 -- a real chip-target change, not just a same-chip Kconfig variant like xiao) and a new epd13in3e driver component skeleton. The actual panel init/LUT/ refresh register sequence isn't ported from vendor demo code yet (none was available), so that component deliberately fails to compile (#error) rather than risk sending unverified register values to real hardware -- devkit/xiao are unaffected and build identically to before. CI's ee02 build step is continue-on-error for the same reason.
669 lines
22 KiB
C
669 lines
22 KiB
C
#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 "epd_board.h"
|
|
#include "qr_onboarding.h"
|
|
#include "wifi_provisioning.h"
|
|
#include "board_antenna.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;
|
|
}
|
|
|
|
/* Optional: absent until the server has pushed a per-frame token
|
|
* (see frame_config_set_device_token). */
|
|
len = sizeof(out->device_token);
|
|
esp_err_t token_err = nvs_get_str(handle, "device_token", out->device_token, &len);
|
|
if (token_err != ESP_OK && token_err != ESP_ERR_NVS_NOT_FOUND) {
|
|
nvs_close(handle);
|
|
return token_err;
|
|
}
|
|
|
|
nvs_close(handle);
|
|
return ESP_OK;
|
|
}
|
|
|
|
void frame_device_id_get(char *out, size_t out_size)
|
|
{
|
|
uint8_t mac[6] = {0};
|
|
ESP_ERROR_CHECK(esp_read_mac(mac, ESP_MAC_WIFI_STA));
|
|
snprintf(out, out_size, "%02x%02x%02x%02x%02x%02x",
|
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
|
}
|
|
|
|
void frame_config_set_device_token(const char *token)
|
|
{
|
|
nvs_handle_t handle;
|
|
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
|
return;
|
|
}
|
|
nvs_set_str(handle, "device_token", token);
|
|
nvs_commit(handle);
|
|
nvs_close(handle);
|
|
ESP_LOGI(TAG, "Stored server-issued device token");
|
|
}
|
|
|
|
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) {
|
|
/* Re-provisioning restarts the identity handshake: the server
|
|
* (possibly a different one now) re-issues a device token when
|
|
* the frame next introduces itself. */
|
|
nvs_erase_key(handle, "device_token");
|
|
/* Fresh (re)provisioning -- the next successful connection should
|
|
* show the status screen again. */
|
|
err = nvs_set_u8(handle, "connected_once", 0);
|
|
}
|
|
if (err == ESP_OK) {
|
|
err = nvs_commit(handle);
|
|
}
|
|
|
|
nvs_close(handle);
|
|
|
|
/* A (re)provisioning event -- the fast-connect cache (if any) may
|
|
* belong to a different network than whatever was just saved. */
|
|
frame_wifi_cache_clear();
|
|
|
|
return err;
|
|
}
|
|
|
|
bool frame_config_has_connected_once(void)
|
|
{
|
|
nvs_handle_t handle;
|
|
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
|
|
if (err != ESP_OK) {
|
|
return false;
|
|
}
|
|
|
|
uint8_t value = 0;
|
|
err = nvs_get_u8(handle, "connected_once", &value);
|
|
nvs_close(handle);
|
|
|
|
return err == ESP_OK && value != 0;
|
|
}
|
|
|
|
void frame_config_mark_connected_once(void)
|
|
{
|
|
nvs_handle_t handle;
|
|
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
|
return;
|
|
}
|
|
nvs_set_u8(handle, "connected_once", 1);
|
|
nvs_commit(handle);
|
|
nvs_close(handle);
|
|
}
|
|
|
|
void frame_config_clear(void)
|
|
{
|
|
nvs_handle_t handle;
|
|
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
|
return;
|
|
}
|
|
nvs_erase_key(handle, "sta_ssid");
|
|
nvs_erase_key(handle, "sta_pass");
|
|
nvs_erase_key(handle, "toolsserver");
|
|
nvs_erase_key(handle, "device_token");
|
|
nvs_erase_key(handle, "connected_once");
|
|
nvs_commit(handle);
|
|
nvs_close(handle);
|
|
|
|
frame_wifi_cache_clear();
|
|
}
|
|
|
|
esp_err_t frame_config_get_last_display_crc32(uint32_t *out)
|
|
{
|
|
nvs_handle_t handle;
|
|
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
err = nvs_get_u32(handle, "last_crc32", out);
|
|
nvs_close(handle);
|
|
return err;
|
|
}
|
|
|
|
void frame_config_set_last_display_crc32(uint32_t crc32)
|
|
{
|
|
nvs_handle_t handle;
|
|
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
|
return;
|
|
}
|
|
nvs_set_u32(handle, "last_crc32", crc32);
|
|
nvs_commit(handle);
|
|
nvs_close(handle);
|
|
}
|
|
|
|
void frame_config_invalidate_last_display_crc32(void)
|
|
{
|
|
nvs_handle_t handle;
|
|
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
|
return;
|
|
}
|
|
nvs_erase_key(handle, "last_crc32");
|
|
nvs_commit(handle);
|
|
nvs_close(handle);
|
|
}
|
|
|
|
esp_err_t frame_config_get_hold_duration_ms(uint32_t *out)
|
|
{
|
|
nvs_handle_t handle;
|
|
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
err = nvs_get_u32(handle, "hold_ms", out);
|
|
nvs_close(handle);
|
|
return err;
|
|
}
|
|
|
|
void frame_config_set_hold_duration_ms(uint32_t ms)
|
|
{
|
|
nvs_handle_t handle;
|
|
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
|
return;
|
|
}
|
|
nvs_set_u32(handle, "hold_ms", ms);
|
|
nvs_commit(handle);
|
|
nvs_close(handle);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------
|
|
* WiFi fast-connect cache
|
|
* ---------------------------------------------------------------------- */
|
|
|
|
bool frame_wifi_cache_load(frame_wifi_cache_t *out)
|
|
{
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
nvs_handle_t handle;
|
|
if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle) != ESP_OK) {
|
|
return false;
|
|
}
|
|
|
|
size_t bssid_len = sizeof(out->bssid);
|
|
bool ok = nvs_get_blob(handle, "wc_bssid", out->bssid, &bssid_len) == ESP_OK
|
|
&& bssid_len == sizeof(out->bssid);
|
|
ok = ok && nvs_get_u8(handle, "wc_channel", &out->channel) == ESP_OK;
|
|
ok = ok && nvs_get_u32(handle, "wc_ip", &out->ip) == ESP_OK;
|
|
ok = ok && nvs_get_u32(handle, "wc_netmask", &out->netmask) == ESP_OK;
|
|
ok = ok && nvs_get_u32(handle, "wc_gateway", &out->gateway) == ESP_OK;
|
|
/* DNS is best-effort -- missing/zero just means the fast path's static
|
|
* IP won't have a DNS server set, which only matters if toolsserver is
|
|
* a hostname rather than a bare IP; a failed cycle clears the whole
|
|
* cache regardless (see frame_client_run), so this can't wedge. */
|
|
nvs_get_u32(handle, "wc_dns", &out->dns);
|
|
|
|
nvs_close(handle);
|
|
return ok;
|
|
}
|
|
|
|
void frame_wifi_cache_save(const frame_wifi_cache_t *cache)
|
|
{
|
|
nvs_handle_t handle;
|
|
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
|
return;
|
|
}
|
|
nvs_set_blob(handle, "wc_bssid", cache->bssid, sizeof(cache->bssid));
|
|
nvs_set_u8(handle, "wc_channel", cache->channel);
|
|
nvs_set_u32(handle, "wc_ip", cache->ip);
|
|
nvs_set_u32(handle, "wc_netmask", cache->netmask);
|
|
nvs_set_u32(handle, "wc_gateway", cache->gateway);
|
|
nvs_set_u32(handle, "wc_dns", cache->dns);
|
|
nvs_commit(handle);
|
|
nvs_close(handle);
|
|
}
|
|
|
|
void frame_wifi_cache_clear(void)
|
|
{
|
|
nvs_handle_t handle;
|
|
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
|
|
return;
|
|
}
|
|
nvs_erase_key(handle, "wc_bssid");
|
|
nvs_erase_key(handle, "wc_channel");
|
|
nvs_erase_key(handle, "wc_ip");
|
|
nvs_erase_key(handle, "wc_netmask");
|
|
nvs_erase_key(handle, "wc_gateway");
|
|
nvs_erase_key(handle, "wc_dns");
|
|
nvs_commit(handle);
|
|
nvs_close(handle);
|
|
}
|
|
|
|
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)
|
|
{
|
|
/* Suffix with the last 3 bytes of the station MAC so two frames on the
|
|
* same network never advertise the same SSID. esp_read_mac() works
|
|
* before the WiFi driver is started. */
|
|
uint8_t mac[6];
|
|
ESP_ERROR_CHECK(esp_read_mac(mac, ESP_MAC_WIFI_STA));
|
|
snprintf(ssid_out, ssid_len, "%s_%02X%02X%02X", CONFIG_ESP_AP_SSID, mac[3], mac[4], mac[5]);
|
|
|
|
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);
|
|
|
|
/* The success page hands the browser off to the server's claim page,
|
|
* carrying this device's id -- how a frame gets linked to a user
|
|
* account. This page is entirely self-contained (no external
|
|
* resources) so it renders fully from what we send now, before the
|
|
* softAP goes away -- a phone mid-load of a remote asset would just
|
|
* time out once the AP drops. The visible countdown ticks down for
|
|
* PROVISIONING_COUNTDOWN_S seconds and then redirects; the AP is kept
|
|
* alive for one second longer than that (see the vTaskDelay below) so
|
|
* the countdown always finishes, and the phone has that whole window
|
|
* to rejoin its normal WiFi and let the redirect land on the real
|
|
* server. "Redirect now" covers a phone that's already reconnected.
|
|
* Scheme handling matches frame_client.c's build_url(): a bare host
|
|
* gets http://. */
|
|
char device_id[FRAME_DEVICE_ID_LEN + 1];
|
|
frame_device_id_get(device_id, sizeof(device_id));
|
|
|
|
char claim_url[FRAME_CFG_SERVER_MAX_LEN + 64];
|
|
const char *scheme = "";
|
|
if (strncmp(cfg.toolsserver, "http://", 7) != 0 && strncmp(cfg.toolsserver, "https://", 8) != 0) {
|
|
scheme = "http://";
|
|
}
|
|
snprintf(claim_url, sizeof(claim_url), "%s%s/claim?device_id=%s", scheme, cfg.toolsserver, device_id);
|
|
|
|
#define PROVISIONING_COUNTDOWN_S 10
|
|
|
|
char resp[1536];
|
|
snprintf(resp, sizeof(resp),
|
|
"<!doctype html><html><head>"
|
|
"<meta http-equiv=\"refresh\" content=\"%d;url=%s\">"
|
|
"<style>body{font-family:sans-serif;text-align:center;padding:2em}"
|
|
"#now{display:inline-block;margin-top:1em;padding:.6em 1.2em;"
|
|
"background:#2563eb;color:#fff;text-decoration:none;border-radius:8px}</style></head>"
|
|
"<body><h3>Saved — the frame is restarting</h3>"
|
|
"<p>Reconnect to your normal WiFi if it doesn't happen automatically.</p>"
|
|
"<p>Redirecting you in <span id=\"n\">%d</span> seconds…</p>"
|
|
"<p><a id=\"now\" href=\"%s\">Redirect now</a></p>"
|
|
"<script>"
|
|
"var n=%d,e=document.getElementById('n');"
|
|
"var t=setInterval(function(){"
|
|
"n--;if(e)e.textContent=n;"
|
|
"if(n<=0){clearInterval(t);location.href='%s';}"
|
|
"},1000);"
|
|
"</script>"
|
|
"</body></html>",
|
|
PROVISIONING_COUNTDOWN_S, claim_url, PROVISIONING_COUNTDOWN_S, claim_url,
|
|
PROVISIONING_COUNTDOWN_S, claim_url);
|
|
httpd_resp_set_type(req, "text/html");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
|
|
/* Keep the softAP up for the full visible countdown (plus a 1s margin
|
|
* for the response to flush and the JS timer to fire) before tearing
|
|
* it down -- see the comment above for why. */
|
|
vTaskDelay(pdMS_TO_TICKS((PROVISIONING_COUNTDOWN_S + 1) * 1000));
|
|
esp_restart();
|
|
|
|
#undef PROVISIONING_COUNTDOWN_S
|
|
|
|
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)
|
|
{
|
|
board_antenna_select_onboard();
|
|
|
|
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));
|
|
|
|
ESP_LOGI(TAG, "Provisioning AP: SSID='%s' password='%s'", ap_ssid, ap_password);
|
|
|
|
/* Create (but don't yet start) the AP netif so its IP is known for the
|
|
* config QR before the network is actually joinable -- the default AP
|
|
* IP is fixed at netif creation, not at esp_wifi_start(). */
|
|
esp_netif_create_default_wifi_ap();
|
|
|
|
esp_netif_ip_info_t ap_ip_info;
|
|
esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), &ap_ip_info);
|
|
char ap_ip_addr[16];
|
|
inet_ntoa_r(ap_ip_info.ip.addr, ap_ip_addr, sizeof(ap_ip_addr));
|
|
char config_url[32];
|
|
snprintf(config_url, sizeof(config_url), "http://%s/", ap_ip_addr);
|
|
|
|
/* Bring up the display and show the join QR/password + config QR
|
|
* before the AP goes up, so the instructions are already on-screen by
|
|
* the time the network is joinable. */
|
|
esp_err_t epd_err = epd_init();
|
|
if (epd_err == ESP_OK) {
|
|
esp_err_t qr_err = qr_onboarding_show(ap_ssid, ap_password, config_url);
|
|
if (qr_err != ESP_OK) {
|
|
ESP_LOGW(TAG, "QR onboarding render failed (%s)", esp_err_to_name(qr_err));
|
|
}
|
|
} else {
|
|
ESP_LOGW(TAG, "EPD init failed (%s), continuing without display", esp_err_to_name(epd_err));
|
|
}
|
|
|
|
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);
|
|
}
|