fix(provisioning): rip out racy DHCP-option dance, add /log endpoint
The previous "force DHCP DNS offer" code (esp_netif_dhcps_stop / set DNS / set option / dhcps_start) ran AFTER softAP was already serving beacons. A fast iOS join — and CNA probes follow DHCP within 1-2s — could land in the middle of the dance and either get a stale lease or be racing the server start. ESP-IDF's softAP DHCP server already advertises the AP IP as DNS by default, so the dance was at best redundant. Strip it. Also drop the WiFi.disconnect(true) call before mode-switching to AP — there's nothing to disconnect from on a cold boot, and disconnect(true) cycles the radio for no benefit. Add /log: in-memory ring buffer (32 entries, FIFO) of HTTP requests and AP-state events served as plain text at http://192.168.4.1/log. Lets the user diagnose without USB serial — join the AP, browse to the URL, see exactly which CNA paths iOS hit (or didn't). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+43
-43
@@ -8,7 +8,6 @@
|
|||||||
#include <Preferences.h>
|
#include <Preferences.h>
|
||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
#include <qrcode.h>
|
#include <qrcode.h>
|
||||||
#include "esp_netif.h"
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "epd.h"
|
#include "epd.h"
|
||||||
#include "operation.h"
|
#include "operation.h"
|
||||||
@@ -98,23 +97,30 @@ min-height:100vh;margin:0;background:#fdf6ee;color:#3a2e22;text-align:center}
|
|||||||
|
|
||||||
// ── Web server handlers ───────────────────────────────────────────────────────
|
// ── Web server handlers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Diagnostic — every probe we receive during provisioning gets logged
|
// In-memory ring buffer of provisioning events (HTTP requests + WiFi
|
||||||
// with method / URI / Host / User-Agent so a serial-monitor repro tells
|
// events) so the user can browse them at http://192.168.4.1/log
|
||||||
// us whether the client (iOS in particular) is actually hitting us, and
|
// without needing serial. 32 entries is plenty for one provisioning
|
||||||
// which CNA path it's using. Cheap; only runs in AP mode.
|
// session and stays well under the ESP32's heap budget.
|
||||||
|
static constexpr size_t LOG_CAP = 32;
|
||||||
|
static String g_log[LOG_CAP];
|
||||||
|
static size_t g_log_head = 0;
|
||||||
|
static size_t g_log_count = 0;
|
||||||
|
|
||||||
|
static void log_event(const String& line) {
|
||||||
|
Serial.println(line);
|
||||||
|
g_log[g_log_head] = String(millis()) + "ms " + line;
|
||||||
|
g_log_head = (g_log_head + 1) % LOG_CAP;
|
||||||
|
if (g_log_count < LOG_CAP) g_log_count++;
|
||||||
|
}
|
||||||
|
|
||||||
static void log_provisioning_request() {
|
static void log_provisioning_request() {
|
||||||
String method = server.method() == HTTP_GET ? "GET"
|
String method = server.method() == HTTP_GET ? "GET"
|
||||||
: server.method() == HTTP_POST ? "POST"
|
: server.method() == HTTP_POST ? "POST"
|
||||||
: server.method() == HTTP_HEAD ? "HEAD"
|
: server.method() == HTTP_HEAD ? "HEAD"
|
||||||
: "OTHER";
|
: "OTHER";
|
||||||
Serial.print("[ap-http] ");
|
log_event("[http] " + method + " " + server.uri() +
|
||||||
Serial.print(method);
|
" host=" + server.hostHeader() +
|
||||||
Serial.print(" ");
|
" ua=" + server.header("User-Agent"));
|
||||||
Serial.print(server.uri());
|
|
||||||
Serial.print(" host=");
|
|
||||||
Serial.print(server.hostHeader());
|
|
||||||
Serial.print(" ua=");
|
|
||||||
Serial.println(server.header("User-Agent"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handle_root() {
|
static void handle_root() {
|
||||||
@@ -139,19 +145,33 @@ static void handle_connect() {
|
|||||||
server.send_P(200, "text/html", CONNECTING_HTML);
|
server.send_P(200, "text/html", CONNECTING_HTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diagnostic page — plain text dump of the ring buffer, oldest first.
|
||||||
|
// User browses to http://192.168.4.1/log after joining the AP. Tells
|
||||||
|
// us whether iOS hit us at all and which CNA paths it tried.
|
||||||
|
static void handle_log() {
|
||||||
|
String body = "pictureFrame provisioning log\n";
|
||||||
|
body += "uptime=" + String(millis()) + "ms entries=" + String(g_log_count) + "/" + String(LOG_CAP) + "\n\n";
|
||||||
|
size_t start = (g_log_count < LOG_CAP) ? 0 : g_log_head;
|
||||||
|
for (size_t i = 0; i < g_log_count; i++) {
|
||||||
|
body += g_log[(start + i) % LOG_CAP] + "\n";
|
||||||
|
}
|
||||||
|
server.sendHeader("Cache-Control", "no-store");
|
||||||
|
server.send(200, "text/plain", body);
|
||||||
|
}
|
||||||
|
|
||||||
static void on_ap_event(WiFiEvent_t event, WiFiEventInfo_t info) {
|
static void on_ap_event(WiFiEvent_t event, WiFiEventInfo_t info) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ARDUINO_EVENT_WIFI_AP_START:
|
case ARDUINO_EVENT_WIFI_AP_START:
|
||||||
Serial.println("[ap-event] AP started");
|
log_event("[wifi] AP started");
|
||||||
break;
|
break;
|
||||||
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
|
case ARDUINO_EVENT_WIFI_AP_STACONNECTED:
|
||||||
Serial.println("[ap-event] station associated");
|
log_event("[wifi] station associated");
|
||||||
break;
|
break;
|
||||||
case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
|
case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED:
|
||||||
Serial.println("[ap-event] DHCP lease handed out");
|
log_event("[wifi] DHCP lease handed out");
|
||||||
break;
|
break;
|
||||||
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
|
case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED:
|
||||||
Serial.println("[ap-event] station disconnected");
|
log_event("[wifi] station disconnected");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -185,6 +205,7 @@ static void enter_provisioning(const String& mac, bool retry = false) {
|
|||||||
// canonical signal that this is a captive network.
|
// canonical signal that this is a captive network.
|
||||||
server.on("/", HTTP_GET, handle_root);
|
server.on("/", HTTP_GET, handle_root);
|
||||||
server.on("/connect", HTTP_POST, handle_connect);
|
server.on("/connect", HTTP_POST, handle_connect);
|
||||||
|
server.on("/log", HTTP_GET, handle_log);
|
||||||
server.on("/hotspot-detect.html", handle_root); // iOS / macOS
|
server.on("/hotspot-detect.html", handle_root); // iOS / macOS
|
||||||
server.on("/library/test/success.html", handle_root); // iOS legacy
|
server.on("/library/test/success.html", handle_root); // iOS legacy
|
||||||
server.on("/generate_204", handle_root); // Android
|
server.on("/generate_204", handle_root); // Android
|
||||||
@@ -198,7 +219,6 @@ static void enter_provisioning(const String& mac, bool retry = false) {
|
|||||||
server.collectHeaders(tracked_headers, 2);
|
server.collectHeaders(tracked_headers, 2);
|
||||||
|
|
||||||
WiFi.onEvent(on_ap_event);
|
WiFi.onEvent(on_ap_event);
|
||||||
WiFi.disconnect(true);
|
|
||||||
WiFi.mode(WIFI_AP);
|
WiFi.mode(WIFI_AP);
|
||||||
WiFi.softAP(apSsid.c_str());
|
WiFi.softAP(apSsid.c_str());
|
||||||
|
|
||||||
@@ -207,31 +227,11 @@ static void enter_provisioning(const String& mac, bool retry = false) {
|
|||||||
// so the captive-detect window is deterministic.
|
// so the captive-detect window is deterministic.
|
||||||
WiFi.setSleep(false);
|
WiFi.setSleep(false);
|
||||||
|
|
||||||
// Belt-and-braces DHCP DNS advertisement: ESP-IDF's softAP DHCP server
|
// Trust the default ESP-IDF softAP DHCP server: it offers the AP IP
|
||||||
// *should* offer the AP IP as DNS by default, but if the client comes
|
// as DNS automatically. We previously did a stop / set-options /
|
||||||
// in with cached cellular DNS (8.8.8.8 etc) the captive DNS hijack
|
// restart dance on top of softAP to "force" the DNS offer — but
|
||||||
// gets bypassed entirely and iOS resolves captive.apple.com to the
|
// that races with iOS's DHCP request (a fast join can hit DHCP-stop
|
||||||
// real internet. Stopping the DHCP server, setting DNS-info, flipping
|
// mid-handshake) and likely caused more failures than it fixed.
|
||||||
// the offer-DNS flag, then restarting forces every lease to carry our
|
|
||||||
// address as the primary resolver.
|
|
||||||
esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
|
||||||
if (ap_netif) {
|
|
||||||
esp_netif_dhcps_stop(ap_netif);
|
|
||||||
|
|
||||||
esp_netif_ip_info_t ip;
|
|
||||||
esp_netif_get_ip_info(ap_netif, &ip);
|
|
||||||
esp_netif_dns_info_t dns_info = {};
|
|
||||||
dns_info.ip.u_addr.ip4.addr = ip.ip.addr;
|
|
||||||
dns_info.ip.type = ESP_IPADDR_TYPE_V4;
|
|
||||||
esp_netif_set_dns_info(ap_netif, ESP_NETIF_DNS_MAIN, &dns_info);
|
|
||||||
|
|
||||||
uint8_t offer_dns = 1;
|
|
||||||
esp_netif_dhcps_option(ap_netif, ESP_NETIF_OP_SET,
|
|
||||||
ESP_NETIF_DOMAIN_NAME_SERVER,
|
|
||||||
&offer_dns, sizeof(offer_dns));
|
|
||||||
|
|
||||||
esp_netif_dhcps_start(ap_netif);
|
|
||||||
}
|
|
||||||
|
|
||||||
server.begin();
|
server.begin();
|
||||||
dns.setErrorReplyCode(DNSReplyCode::NoError);
|
dns.setErrorReplyCode(DNSReplyCode::NoError);
|
||||||
|
|||||||
Reference in New Issue
Block a user