From badb2b31a8e4044b978dd91105952e013bd472c3 Mon Sep 17 00:00:00 2001 From: shodan Date: Sun, 11 Oct 2026 14:55:21 +0200 Subject: [PATCH] feat(wifi): advertise the setup AP as a captive portal over DHCP The setup AP already answered every DNS name with its own IP and redirected unknown pages to /config. It did not send DHCP option 114 (RFC 8910), so some phones did not show the sign-in prompt. Both AP start paths now go through one function that sets the option. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/api.md | 2 +- src/network/wifi_connect.cpp | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/api.md b/docs/api.md index 3a5f0716..72f5a52e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,7 +8,7 @@ Optional auth: when an `access_token` is configured in settings, all JSON API ro When WiFi credentials are not saved yet, control APIs (`/anim`, `/test/*`) return **503** `{"ok":false,"error":"wifi not configured"}`. Setup routes (`/`, `/config`, `/auth`, `/health`, `/settings`, `/setup/servo`, `/setup/led`, `/setup/audio`, `/setup/oled`) stay available on the setup AP. -If boot WiFi credentials are missing, the device opens setup AP mode (`TinyEngineer-XXXX`) and serves a five-step setup wizard at `/config` (servo calibration, OLED orientation, RGB LED, speaker test, then Wi-Fi). If saved credentials fail, it reopens setup AP mode. Hardware tests: [`docs/hardware/testing.md`](hardware/testing.md). How to wire this API into AI tools: [`integration.md`](integration.md). +If boot WiFi credentials are missing, the device opens setup AP mode (`TinyEngineer-XXXX`) and serves a five-step setup wizard at `/config` (servo calibration, OLED orientation, RGB LED, speaker test, then Wi-Fi). If saved credentials fail, it reopens setup AP mode. The setup AP acts as a captive portal: DHCP sends the portal URL (option 114) and DNS answers every name with the AP IP, so a phone that joins the network opens the wizard on its own. Hardware tests: [`docs/hardware/testing.md`](hardware/testing.md). How to wire this API into AI tools: [`integration.md`](integration.md). Firmware answers mDNS A and AAAA (IPv6 link-local) so macOS does not wait 2–3s on a missing AAAA. If a client still pauses on the hostname, force IPv4 (`curl -4`). The IP on the OLED skips DNS entirely. diff --git a/src/network/wifi_connect.cpp b/src/network/wifi_connect.cpp index d2e08853..02bf15fa 100644 --- a/src/network/wifi_connect.cpp +++ b/src/network/wifi_connect.cpp @@ -97,7 +97,19 @@ void startMdns(const char* hostname) { } } -void startCaptiveDns() { +// Phones open the setup page on their own: DHCP option 114 names the portal +// URL, and the catch-all DNS sends connectivity probes to the web server. +void openSetupAp(wifi_mode_t mode) { + buildApSsid(); + + WiFi.persistent(false); + WiFi.mode(mode); + WiFi.softAP(apSsid); + + if (!WiFi.AP.enableDhcpCaptivePortal()) { + serialLogPrintln("ERROR: DHCP captive portal option failed"); + } + if (dnsStarted) { return; } @@ -174,11 +186,7 @@ bool connectSta( } void startProvisioningAp() { - buildApSsid(); - - WiFi.persistent(false); - WiFi.mode(WIFI_AP); - WiFi.softAP(apSsid); + openSetupAp(WIFI_AP); const IPAddress apIp = WiFi.softAPIP(); storeIp(apIp); @@ -196,8 +204,6 @@ void startProvisioningAp() { staConnected = false; ipText[0] = '\0'; - startCaptiveDns(); - serialLogPrint("AP setup SSID: "); serialLogPrintln(apSsid); serialLogPrint("AP IP: "); @@ -272,11 +278,7 @@ bool wifiTestCredentials( serialLogPrintln("WIFI TEST (provisioning)"); serialLogPrintln("=========================="); - buildApSsid(); - WiFi.persistent(false); - WiFi.mode(WIFI_AP_STA); - WiFi.softAP(apSsid); - startCaptiveDns(); + openSetupAp(WIFI_AP_STA); if (!connectSta(ssid, password, hostname)) { wifiRestoreProvisioningAp();