diff --git a/src/main.cpp b/src/main.cpp index e0f343a..b86325d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,7 +8,6 @@ #include #include #include -#include "esp_netif.h" #include "config.h" #include "epd.h" #include "operation.h" @@ -98,23 +97,30 @@ min-height:100vh;margin:0;background:#fdf6ee;color:#3a2e22;text-align:center} // ── Web server handlers ─────────────────────────────────────────────────────── -// Diagnostic — every probe we receive during provisioning gets logged -// with method / URI / Host / User-Agent so a serial-monitor repro tells -// us whether the client (iOS in particular) is actually hitting us, and -// which CNA path it's using. Cheap; only runs in AP mode. +// In-memory ring buffer of provisioning events (HTTP requests + WiFi +// events) so the user can browse them at http://192.168.4.1/log +// without needing serial. 32 entries is plenty for one provisioning +// 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() { String method = server.method() == HTTP_GET ? "GET" : server.method() == HTTP_POST ? "POST" : server.method() == HTTP_HEAD ? "HEAD" : "OTHER"; - Serial.print("[ap-http] "); - Serial.print(method); - Serial.print(" "); - Serial.print(server.uri()); - Serial.print(" host="); - Serial.print(server.hostHeader()); - Serial.print(" ua="); - Serial.println(server.header("User-Agent")); + log_event("[http] " + method + " " + server.uri() + + " host=" + server.hostHeader() + + " ua=" + server.header("User-Agent")); } static void handle_root() { @@ -139,19 +145,33 @@ static void handle_connect() { 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) { switch (event) { case ARDUINO_EVENT_WIFI_AP_START: - Serial.println("[ap-event] AP started"); + log_event("[wifi] AP started"); break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: - Serial.println("[ap-event] station associated"); + log_event("[wifi] station associated"); break; case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: - Serial.println("[ap-event] DHCP lease handed out"); + log_event("[wifi] DHCP lease handed out"); break; case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: - Serial.println("[ap-event] station disconnected"); + log_event("[wifi] station disconnected"); break; default: break; @@ -185,6 +205,7 @@ static void enter_provisioning(const String& mac, bool retry = false) { // canonical signal that this is a captive network. server.on("/", HTTP_GET, handle_root); 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("/library/test/success.html", handle_root); // iOS legacy 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); WiFi.onEvent(on_ap_event); - WiFi.disconnect(true); WiFi.mode(WIFI_AP); 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. WiFi.setSleep(false); - // Belt-and-braces DHCP DNS advertisement: ESP-IDF's softAP DHCP server - // *should* offer the AP IP as DNS by default, but if the client comes - // in with cached cellular DNS (8.8.8.8 etc) the captive DNS hijack - // gets bypassed entirely and iOS resolves captive.apple.com to the - // real internet. Stopping the DHCP server, setting DNS-info, flipping - // 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); - } + // Trust the default ESP-IDF softAP DHCP server: it offers the AP IP + // as DNS automatically. We previously did a stop / set-options / + // restart dance on top of softAP to "force" the DNS offer — but + // that races with iOS's DHCP request (a fast join can hit DHCP-stop + // mid-handshake) and likely caused more failures than it fixed. server.begin(); dns.setErrorReplyCode(DNSReplyCode::NoError);