diff --git a/README.md b/README.md
index 86a2e0b3..a10ca544 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# WiFiManager
+https://github.com/user-attachments/assets/1e86e083-0089-4639-b95f-392b61905f0a
+
Espressif ESPx WiFi Connection manager with fallback web configuration portal
:warning: This Documentation is out of date, see notes below
diff --git a/WiFiManager.cpp b/WiFiManager.cpp
index 21c9e38f..10c9e455 100644
--- a/WiFiManager.cpp
+++ b/WiFiManager.cpp
@@ -343,6 +343,7 @@ boolean WiFiManager::autoConnect(char const *apName, char const *apPassword) {
DEBUG_WM(F("AutoConnect: ESP Already Connected"));
#endif
setSTAConfig();
+ setLEDState(WM_LED_CONNECTED); // already connected – set LED
// @todo not sure if this is safe, causes dup setSTAConfig in connectwifi,
// and we have no idea WHAT we are connected to
}
@@ -657,6 +658,18 @@ void WiFiManager::setupHTTPServer(){
server->on(WM_G(R_update), std::bind(&WiFiManager::handleUpdate, this));
server->on(WM_G(R_updatedone), HTTP_POST, std::bind(&WiFiManager::handleUpdateDone, this), std::bind(&WiFiManager::handleUpdating, this));
+
+ // OS captive portal compatibility probes – these let Android/iOS/Windows automatically
+ // pop up the captive-portal UI when a user connects to the SoftAP.
+ // HTTPS is NOT intercepted; only plain HTTP probes are handled.
+ if(_captivePortalCompat) {
+ server->on(F("/generate_204"), std::bind(&WiFiManager::handleCaptivePortal204, this)); // Android
+ server->on(F("/hotspot-detect.html"), std::bind(&WiFiManager::handleCaptivePortalHotspot, this)); // iOS / macOS
+ server->on(F("/ncsi.txt"), std::bind(&WiFiManager::handleCaptivePortalNcsi, this)); // Windows
+ server->on(F("/connecttest.txt"), std::bind(&WiFiManager::handleCaptivePortalNcsi, this)); // Windows 10
+ server->on(F("/redirect"), std::bind(&WiFiManager::handleCaptivePortalNcsi, this)); // Windows
+ server->on(F("/success.txt"), std::bind(&WiFiManager::handleCaptivePortal204, this)); // Amazon Fire OS
+ }
server->begin(); // Web server start
#ifdef WM_DEBUG_LEVEL
@@ -717,7 +730,8 @@ boolean WiFiManager::startConfigPortal(char const *apName, char const *apPasswo
if(!validApPassword()) return false;
// HANDLE issues with STA connections, shutdown sta if not connected, or else this will hang channel scanning and softap will not respond
- if(_disableSTA || (!WiFi.isConnected() && _disableSTAConn)){
+ // When _keepAPDuringSTAConnect is set we intentionally run AP+STA, so we keep STA enabled.
+ if(!_keepAPDuringSTAConnect && (_disableSTA || (!WiFi.isConnected() && _disableSTAConn))){
// this fixes most ap problems, however, simply doing mode(WIFI_AP) does not work if sta connection is hanging, must `wifi_station_disconnect`
#ifdef WM_DISCONWORKAROUND
WiFi.mode(WIFI_AP_STA);
@@ -728,10 +742,23 @@ boolean WiFiManager::startConfigPortal(char const *apName, char const *apPasswo
DEBUG_WM(WM_DEBUG_VERBOSE,F("Disabling STA"));
#endif
}
+ else if(_keepAPDuringSTAConnect) {
+ // Ensure STA interface is up for AP+STA provisioning mode
+ WiFi_enableSTA(true);
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_VERBOSE,F("Keeping STA enabled (AP+STA provisioning mode)"));
+ #endif
+ }
else {
// WiFi_enableSTA(true);
}
+ // reset provisioning state for a fresh portal session
+ _provisioningState = WM_PROV_IDLE;
+ _provisioningError = "";
+ _provisioningConnecting = false;
+ _apShutdownPending = false;
+
// init configportal globals to known states
configPortalActive = true;
bool result = connect = abort = false; // loop flags, connect true success, abort true break
@@ -746,6 +773,15 @@ boolean WiFiManager::startConfigPortal(char const *apName, char const *apPasswo
startAP();
WiFiSetCountry();
+ // Set LED state: NOWIFI when no credentials are saved; if credentials exist but the
+ // LED is not already active (e.g. manual portal start), use FAILED to signal a
+ // configuration problem (credentials stored but connection could not be established).
+ if(!WiFi_hasAutoConnect()) {
+ setLEDState(WM_LED_NOWIFI);
+ } else if(_ledCurrentState == WM_LED_OFF) {
+ setLEDState(WM_LED_FAILED);
+ }
+
// do AP callback if set
if ( _apcallback != NULL) {
#ifdef WM_DEBUG_LEVEL
@@ -831,7 +867,11 @@ boolean WiFiManager::process(){
#if defined(WM_MDNS) && defined(ESP8266)
MDNS.update();
#endif
-
+
+ // Always drive the LED state machine so timeouts fire and the LED
+ // stays accurate even when the config portal is not active.
+ syncLEDState();
+
if(webPortalActive || (configPortalActive && !_configPortalIsBlocking)){
// if timed out or abort, break
if(_allowExit && (configPortalHasTimeout() || abort)){
@@ -863,6 +903,9 @@ boolean WiFiManager::process(){
* @return {[type]} [description]
*/
uint8_t WiFiManager::processConfigPortal(){
+ // Check LED timeout and reconcile with WiFi status on every iteration.
+ syncLEDState();
+
if(configPortalActive){
//DNS handler
dnsServer->processNextRequest();
@@ -871,6 +914,17 @@ uint8_t WiFiManager::processConfigPortal(){
//HTTP handler
server->handleClient();
+ // ---- AP+STA provisioning state machine ----
+ // When _keepAPDuringSTAConnect is true the connection is started
+ // non-blocking from handleWifiSave(). checkProvisioningState() advances
+ // the state machine on every call and returns WL_CONNECTED once the AP
+ // has been torn down after the shutdown delay.
+ if(_keepAPDuringSTAConnect && (_provisioningConnecting || _apShutdownPending)) {
+ uint8_t provResult = checkProvisioningState();
+ if(provResult != WL_IDLE_STATUS) return provResult;
+ return WL_IDLE_STATUS;
+ }
+
// Waiting for save...
if(connect) {
connect = false;
@@ -949,6 +1003,223 @@ uint8_t WiFiManager::processConfigPortal(){
return WL_IDLE_STATUS;
}
+// ---------------------------------------------------------------------------
+// AP+STA Provisioning state machine
+// ---------------------------------------------------------------------------
+
+/**
+ * checkProvisioningState
+ * Called from processConfigPortal() when _keepAPDuringSTAConnect is true.
+ * Polls the STA connection status and drives the provisioning state machine:
+ * CONNECTING → CONNECTED (saves credentials, starts AP-shutdown timer)
+ * CONNECTING → FAILED (keeps portal open so the user can retry)
+ * CONNECTED → shutdown (once the AP-shutdown delay has elapsed)
+ *
+ * @return WL_IDLE_STATUS while still in progress, WL_CONNECTED when the AP
+ * has been torn down after a successful connection.
+ */
+uint8_t WiFiManager::checkProvisioningState() {
+ // ---- AP shutdown timer expired after successful connect ----
+ if(_apShutdownPending) {
+ if(millis() >= _apShutdownDeadline) {
+ _apShutdownPending = false;
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_VERBOSE,F("Provisioning: AP shutdown delay elapsed, shutting down AP"));
+ #endif
+ shutdownConfigPortal(); // stops AP, sets configPortalActive = false
+ return WL_CONNECTED; // signal the blocking loop to exit with result=true
+ }
+ return WL_IDLE_STATUS; // still waiting
+ }
+
+ if(!_provisioningConnecting) return WL_IDLE_STATUS;
+
+ uint8_t status = WiFi.status();
+
+ // Default connect-timeout is _connectTimeout; fall back to 30 s if not set
+ unsigned long timeout = (_connectTimeout > 0) ? _connectTimeout : 30000UL;
+ unsigned long elapsed = millis() - _startconn;
+ bool timedOut = elapsed > timeout;
+
+ // Grace period after calling WiFi.begin(): the radio briefly retains the
+ // previous failure status from the last attempt. Ignore failure codes
+ // until the radio has had time to reset so a retry does not instantly re-fail.
+ static const unsigned long PROV_GRACE_PERIOD_MS = 2000UL;
+ bool gracePeriod = elapsed < PROV_GRACE_PERIOD_MS;
+
+ if(status == WL_CONNECTED) {
+ // Wait for DHCP to assign a real IP before declaring success.
+ // WiFi.status() can become WL_CONNECTED before the IP address is assigned
+ // (localIP() returns 0.0.0.0 during that window). 8 s covers even slow
+ // DHCP servers; typical assignment completes in under 2 s.
+ static const unsigned long DHCP_IP_WAIT_MS = 8000UL;
+ if(WiFi.localIP() == IPAddress(0,0,0,0)) {
+ if(_ipWaitStart == 0) _ipWaitStart = millis();
+ if(millis() - _ipWaitStart < DHCP_IP_WAIT_MS) {
+ return WL_IDLE_STATUS; // still waiting for DHCP
+ }
+ // timeout – proceed anyway so we don't get stuck forever
+ }
+ _ipWaitStart = 0;
+
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_VERBOSE,F("Provisioning: STA connected, IP:"),WiFi.localIP());
+ #endif
+ _provisioningState = WM_PROV_CONNECTED;
+ _provisioningConnecting = false;
+ updateConxResult(status);
+
+ // Save credentials NOW that the connection is confirmed working
+ saveWiFiCredentials(_ssid, _pass);
+
+ // Fire the save callback
+ if(_savewificallback != NULL) {
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_VERBOSE,F("[CB] _savewificallback calling"));
+ #endif
+ _savewificallback(); // @CALLBACK
+ }
+
+ // Start the AP-shutdown delay (or shut down immediately if delay == 0)
+ if(_apShutdownDelayMs > 0) {
+ _apShutdownDeadline = millis() + _apShutdownDelayMs;
+ _apShutdownPending = true;
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_VERBOSE,F("Provisioning: AP will shut down in ms:"),_apShutdownDelayMs);
+ #endif
+ } else {
+ shutdownConfigPortal();
+ return WL_CONNECTED;
+ }
+
+ } else if(!gracePeriod && (
+ status == WL_NO_SSID_AVAIL ||
+ status == WL_CONNECT_FAILED ||
+ status == WL_CONNECTION_LOST ||
+ status == WL_STATION_WRONG_PASSWORD ||
+ timedOut)) {
+
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_ERROR,F("Provisioning: STA connection failed, status:"),getWLStatusString(status));
+ #endif
+ _provisioningState = WM_PROV_FAILED;
+ _provisioningConnecting = false;
+ updateConxResult(status);
+ // Always populate the error so the frontend can show a helpful message.
+ _provisioningError = getProvisioningFailureReason(timedOut ? WL_IDLE_STATUS : status);
+ // Keep AP + portal open so the user can retry
+ }
+
+ return WL_IDLE_STATUS;
+}
+
+/**
+ * saveWiFiCredentials
+ * Persist SSID / password to flash using WiFi.begin(connect=false) so that
+ * the credential store is updated without starting a new connection attempt
+ * (the STA is already connected at this point).
+ */
+bool WiFiManager::saveWiFiCredentials(String ssid, String pass) {
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_VERBOSE,F("Saving WiFi credentials to persistent storage"));
+ #endif
+ WiFi_enableSTA(true, storeSTAmode);
+ WiFi.persistent(true);
+ // The 5-parameter form of WiFi.begin() is supported on both ESP32 (arduino-esp32)
+ // and ESP8266 (arduino-esp8266): the last parameter `connect` defaults to true and
+ // is already used by wifiConnectNew() in this library.
+ // Passing connect=false saves the credentials to NVS / flash (because persistent=true)
+ // without calling esp_wifi_connect() / wifi_station_connect(), so the existing
+ // STA session is preserved.
+ bool ret = WiFi.begin(ssid.c_str(), pass.c_str(), 0, NULL, false);
+ WiFi.persistent(false);
+ return ret;
+}
+
+/**
+ * getProvisioningStateStr
+ * Returns the current provisioning state as a JSON-safe string.
+ */
+String WiFiManager::getProvisioningStateStr() {
+ switch(_provisioningState) {
+ case WM_PROV_SCANNING: return F("scanning");
+ case WM_PROV_CONNECTING: return F("connecting");
+ case WM_PROV_CONNECTED: return F("connected");
+ case WM_PROV_FAILED: return F("failed");
+ case WM_PROV_RETRYING: return F("retrying");
+ default: return F("idle");
+ }
+}
+
+/**
+ * getProvisioningFailureReason
+ * Maps low-level WL status codes to a human-readable error string.
+ */
+String WiFiManager::getProvisioningFailureReason(uint8_t status) {
+ if(status == WL_NO_SSID_AVAIL) return F("Network not found");
+ if(status == WL_STATION_WRONG_PASSWORD) return F("Wrong password");
+ if(status == WL_CONNECT_FAILED) return F("Connection failed");
+ if(status == WL_CONNECTION_LOST) return F("Connection lost / wrong password");
+ if(status == WL_IDLE_STATUS) return F("Connection timed out");
+ return F("Unknown error");
+}
+
+// ---------------------------------------------------------------------------
+// OS captive portal probe handlers
+// ---------------------------------------------------------------------------
+
+/**
+ * handleCaptivePortal204
+ * Android: GET /generate_204 – Android expects a 204 response on a real
+ * internet connection. When in captive portal mode we redirect to the portal
+ * so Android pops up the "Sign in to network" notification.
+ * Amazon Fire OS: GET /success.txt (same treatment).
+ */
+void WiFiManager::handleCaptivePortal204() {
+ if(!configPortalActive) {
+ server->send(204, FPSTR(HTTP_HEAD_CT2), "");
+ return;
+ }
+ String loc = (String)F("http://") + toStringIp(WiFi.softAPIP());
+ server->sendHeader(F("Location"), loc, true);
+ server->send(302, FPSTR(HTTP_HEAD_CT2), "");
+ server->client().stop();
+}
+
+/**
+ * handleCaptivePortalHotspot
+ * iOS / macOS: GET /hotspot-detect.html – Apple expects a specific response
+ * body. We redirect to the portal instead so the Captive Network Assistant opens.
+ */
+void WiFiManager::handleCaptivePortalHotspot() {
+ if(!configPortalActive) {
+ server->send(200, FPSTR(HTTP_HEAD_CT),
+ F("
SuccessSuccess"));
+ return;
+ }
+ String loc = (String)F("http://") + toStringIp(WiFi.softAPIP());
+ server->sendHeader(F("Location"), loc, true);
+ server->send(302, FPSTR(HTTP_HEAD_CT2), "");
+ server->client().stop();
+}
+
+/**
+ * handleCaptivePortalNcsi
+ * Windows: GET /ncsi.txt, /connecttest.txt, /redirect – NCSI (Network
+ * Connectivity Status Indicator) probes. Redirect to the portal so Windows
+ * displays the "Additional sign-in info required" notification.
+ */
+void WiFiManager::handleCaptivePortalNcsi() {
+ if(!configPortalActive) {
+ server->send(200, FPSTR(HTTP_HEAD_CT2), F("Microsoft NCSI"));
+ return;
+ }
+ String loc = (String)F("http://") + toStringIp(WiFi.softAPIP());
+ server->sendHeader(F("Location"), loc, true);
+ server->send(302, FPSTR(HTTP_HEAD_CT2), "");
+ server->client().stop();
+}
+
/**
* [shutdownConfigPortal description]
* @access public
@@ -1022,6 +1293,8 @@ uint8_t WiFiManager::connectWifi(String ssid, String pass, bool connect) {
uint8_t retry = 1;
uint8_t connRes = (uint8_t)WL_NO_SSID_AVAIL;
+ setLEDState(WM_LED_CONNECTING);
+
setSTAConfig();
//@todo catch failures in set_config
@@ -1212,6 +1485,13 @@ void WiFiManager::updateConxResult(uint8_t status){
}
DEBUG_WM(WM_DEBUG_DEV,F("lastconxresult:"),getWLStatusString(_lastconxresult));
#endif
+
+ // Update LED state based on connection result
+ if(_lastconxresult == WL_CONNECTED) {
+ setLEDState(WM_LED_CONNECTED);
+ } else if(_lastconxresult != WL_IDLE_STATUS) {
+ setLEDState(WM_LED_FAILED);
+ }
}
@@ -1346,6 +1626,16 @@ void WiFiManager::handleRoot() {
DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP Root"));
#endif
if (captivePortal()) return; // If captive portal redirect instead of displaying the page
+
+ // When the setup portal is active, go directly to the WiFi config page
+ // so users land on the useful page immediately instead of a menu.
+ if(configPortalActive) {
+ server->sendHeader(F("Location"), F("/wifi"), true);
+ server->send(302, FPSTR(HTTP_HEAD_CT2), "");
+ server->client().stop();
+ return;
+ }
+
handleRequest();
String page = getHTTPHead(_title, FPSTR(C_root)); // @token options @todo replace options with title
String str = FPSTR(HTTP_ROOT_MAIN); // @todo custom title
@@ -1355,6 +1645,7 @@ void WiFiManager::handleRoot() {
page += FPSTR(HTTP_PORTAL_OPTIONS);
page += getMenuOut();
reportStatus(page);
+ page += FPSTR(HTTP_NAV_BOTTOM);
page += getHTTPEnd();
HTTPSend(page);
@@ -1373,6 +1664,13 @@ void WiFiManager::handleWifi(boolean scan) {
#endif
handleRequest();
String page = getHTTPHead(FPSTR(S_titlewifi), FPSTR(C_wifi)); // @token titlewifi
+
+ // Status banner + refresh icon button in a flex row
+ page += F("
");
+
//display networks in page
for (int i = 0; i < n; i++) {
if (indices[i] == -1) continue; // skip dups
@@ -1682,7 +2002,7 @@ String WiFiManager::WiFiManager::getScanItemOut(){
}
}
- page += FPSTR(HTTP_BR);
+ page += F("
"); // close .wl scrollable div
}
return page;
@@ -1808,12 +2128,59 @@ void WiFiManager::handleWiFiStatus(){
DEBUG_WM(WM_DEBUG_VERBOSE,F("<- HTTP WiFi status "));
#endif
handleRequest();
- String page;
- // String page = "{\"result\":true,\"count\":1}";
- #ifdef WM_JSTEST
- page = FPSTR(HTTP_JS);
+
+ // Build a JSON response with the current provisioning / connection state.
+ // This endpoint is polled by the provisioning UI page after a save and by
+ // the live status script on the WiFi-setup page.
+ bool connected = (WiFi.status() == WL_CONNECTED);
+
+ String json = F("{\"state\":\"");
+ json += getProvisioningStateStr();
+ json += F("\",\"ssid\":\"");
+ json += htmlEntities(WiFi_SSID());
+ json += F("\",\"ip\":\"");
+ if(connected) json += WiFi.localIP().toString();
+ json += F("\",\"error\":\"");
+ json += _provisioningError;
+ json += F("\",\"wlstatus\":\"");
+ json += connected ? F("connected") : F("disconnected");
+ json += F("\"");
+
+ if(connected) {
+ int rssi = WiFi.RSSI();
+ json += F(",\"rssi\":");
+ json += String(rssi);
+ json += F(",\"quality\":");
+ json += String(getRSSIasQuality(rssi));
+ }
+
+ // Map last connection result to a short machine-readable string so the
+ // frontend can show user-friendly error messages.
+ json += F(",\"lastResult\":\"");
+ switch(_lastconxresult) {
+ case WL_STATION_WRONG_PASSWORD: json += F("wrong_password"); break;
+ case WL_NO_SSID_AVAIL: json += F("not_found"); break;
+ case WL_CONNECT_FAILED:
+ case WL_CONNECTION_LOST: json += F("failed"); break;
+ case WL_CONNECTED: json += F("connected"); break;
+ default: break; // WL_IDLE_STATUS – no attempt yet
+ }
+ json += F("\"");
+
+ if(_apShutdownPending && _apShutdownDeadline > millis()) {
+ json += F(",\"apShutdownIn\":");
+ json += String((long)(_apShutdownDeadline - millis()));
+ }
+ #ifdef WM_MDNS
+ if(_hostname != "") {
+ json += F(",\"hostname\":\"");
+ json += _hostname + F(".local");
+ json += F("\"");
+ }
#endif
- HTTPSend(page);
+ json += F("}");
+
+ server->send(200, F("application/json"), json);
}
/**
@@ -1837,6 +2204,24 @@ void WiFiManager::handleWifiSave() {
#endif
}
+ // --- Server-side input validation ---
+ // WPA2-Personal PSK: 8–63 printable ASCII characters; empty = open network.
+ // The client-side HTML pattern enforces the same rule, but we guard here
+ // too so a raw HTTP POST cannot bypass it.
+ size_t passLen = _pass.length();
+ if (passLen > 0 && (passLen < 8 || passLen > 63)) {
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] WiFi password length invalid:"),passLen);
+ #endif
+ String page = getHTTPHead(FPSTR(S_titlewifisaved), FPSTR(C_wifi));
+ page += F("
Invalid password "
+ "WiFi password must be between 8 and 63 characters.
"
+ " ");
+ page += getHTTPEnd();
+ server->sendHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate"));
+ HTTPSend(page);
+ return;
+ }
#ifdef WM_DEBUG_LEVEL
String requestinfo = "SERVER_REQUEST\n----------------\n";
requestinfo += "URI: ";
@@ -1890,6 +2275,52 @@ void WiFiManager::handleWifiSave() {
if(_paramsInWifi) doParamSave();
+ // ---- AP+STA provisioning mode: start a non-blocking STA connection ----
+ // When _keepAPDuringSTAConnect is true we start the WiFi connection
+ // immediately without blocking, keep the AP + web server running, and
+ // let the frontend poll /status for the result.
+ if(_keepAPDuringSTAConnect && _ssid != "") {
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_VERBOSE,F("Provisioning: starting non-blocking STA connect to:"),_ssid);
+ #endif
+ // Reset provisioning state
+ _provisioningState = WM_PROV_CONNECTING;
+ _provisioningConnecting = true;
+ _provisioningError = "";
+ _apShutdownPending = false;
+ _startconn = millis();
+ _ipWaitStart = 0; // reset DHCP-wait timer for new attempt
+
+ setLEDState(WM_LED_CONNECTING); // signal LED: connecting
+
+ // Apply static IP config if set
+ setSTAConfig();
+
+ // Start STA connection without persistent-save (credentials are saved on
+ // success by saveWiFiCredentials() inside checkProvisioningState()).
+ WiFi_enableSTA(true, storeSTAmode);
+ WiFi.persistent(false);
+ WiFi.begin(_ssid.c_str(), _pass.c_str());
+ WiFi.persistent(false);
+
+ // Return the provisioning status page which polls /status via JS
+ // Replace custom SVG tokens before sending.
+ String page = getHTTPHead(FPSTR(S_titlewifisaved), FPSTR(C_wifi));
+ String provPage = FPSTR(HTTP_SAVED_PROVISIONING);
+ provPage.replace(F("{svgC}"), _customConnectingSVG ? _customConnectingSVG : "");
+ provPage.replace(F("{svgS}"), _customSuccessSVG ? _customSuccessSVG : "");
+ provPage.replace(F("{svgF}"), _customFailureSVG ? _customFailureSVG : "");
+ page += provPage;
+ if(_showBack) page += FPSTR(HTTP_BACKBTN);
+ page += FPSTR(HTTP_NAV_BOTTOM);
+ page += getHTTPEnd();
+ HTTPSend(page);
+ #ifdef WM_DEBUG_LEVEL
+ DEBUG_WM(WM_DEBUG_DEV,F("Sent provisioning save page"));
+ #endif
+ return; // do NOT set connect=true; state machine in processConfigPortal handles the rest
+ }
+
String page;
if(_ssid == ""){
@@ -1929,6 +2360,7 @@ void WiFiManager::handleParamSave() {
String page = getHTTPHead(FPSTR(S_titleparamsaved), FPSTR(C_param)); // @token titleparamsaved
page += FPSTR(HTTP_PARAMSAVED);
if(_showBack) page += FPSTR(HTTP_BACKBTN);
+ page += FPSTR(HTTP_NAV_BOTTOM);
page += getHTTPEnd();
HTTPSend(page);
@@ -2086,6 +2518,7 @@ void WiFiManager::handleInfo() {
if(_showInfoErase) page += FPSTR(HTTP_ERASEBTN);
if(_showBack) page += FPSTR(HTTP_BACKBTN);
page += FPSTR(HTTP_HELP);
+ page += FPSTR(HTTP_NAV_BOTTOM);
page += getHTTPEnd();
HTTPSend(page);
@@ -3254,6 +3687,59 @@ void WiFiManager::setParamsPage(bool enable){
// GETTERS
+// --- AP+STA Provisioning setters/getters ---
+
+/**
+ * setKeepAPDuringSTAConnect
+ * When true the config portal runs in AP+STA mode: the SoftAP remains active
+ * during and after the STA connection attempt. Credentials are saved only on
+ * a successful connection. The AP is shut down after _apShutdownDelayMs ms.
+ * Default: false (legacy blocking behaviour unchanged).
+ */
+void WiFiManager::setKeepAPDuringSTAConnect(bool keep) {
+ _keepAPDuringSTAConnect = keep;
+}
+
+/**
+ * setAPShutdownDelay
+ * Set the number of milliseconds to keep the SoftAP running after a
+ * successful STA connection before it is shut down (default: 10 000 ms).
+ * Set to 0 to shut down immediately on connect.
+ */
+void WiFiManager::setAPShutdownDelay(unsigned long ms) {
+ _apShutdownDelayMs = ms;
+}
+
+/**
+ * setDetailedFailureReasons
+ * When true the /status JSON endpoint maps low-level WL status codes to
+ * human-readable strings (e.g. "Wrong password", "Network not found").
+ * Default: false.
+ */
+void WiFiManager::setDetailedFailureReasons(bool enable) {
+ _detailedFailureReasons = enable;
+}
+
+/**
+ * setCaptivePortalCompatibility
+ * When true (default) the portal registers extra HTTP handlers for the OS
+ * captive portal probes used by Android (/generate_204), iOS/macOS
+ * (/hotspot-detect.html) and Windows (/ncsi.txt, /connecttest.txt).
+ * These redirects cause the OS to automatically open the captive-portal UI.
+ * HTTPS traffic is never intercepted.
+ */
+void WiFiManager::setCaptivePortalCompatibility(bool enable) {
+ _captivePortalCompat = enable;
+}
+
+/**
+ * getProvisioningState
+ * Returns the current provisioning state as a wm_provstate_t enum value.
+ */
+wm_provstate_t WiFiManager::getProvisioningState() {
+ return _provisioningState;
+}
+
/**
* get config portal AP SSID
* @since 0.0.1
@@ -3882,6 +4368,18 @@ String WiFiManager::WiFi_psk(bool persistent) const {
#endif
WiFi.reconnect();
#endif
+ // LED: show FAILED on disconnect, but ignore the spurious disconnect that
+ // WiFi.begin(connect=false) / AP teardown fires during a successful provisioning
+ // save – _provisioningState is CONNECTED from the moment we confirm the STA link
+ // until the portal fully closes.
+ if(_provisioningState != WM_PROV_CONNECTED) {
+ setLEDState(WM_LED_FAILED);
+ }
+ }
+ else if(event == ARDUINO_EVENT_WIFI_STA_GOT_IP){
+ // LED: STA obtained an IP – this covers autonomous reconnects as well as
+ // initial provisioning, overriding whatever state the LED was in.
+ setLEDState(WM_LED_CONNECTED);
}
else if(event == ARDUINO_EVENT_WIFI_SCAN_DONE && _asyncScan){
uint16_t scans = WiFi.scanComplete();
@@ -3893,6 +4391,24 @@ String WiFiManager::WiFi_psk(bool persistent) const {
void WiFiManager::WiFi_autoReconnect(){
#ifdef ESP8266
WiFi.setAutoReconnect(_wifiAutoReconnect);
+ // Register persistent WiFi event handlers so the LED is updated whenever
+ // the network connects or disconnects autonomously (router restart,
+ // device moves in/out of range, password changed on router, etc.).
+ // The WiFiEventHandler objects are stored as members to keep them alive.
+ if(!_wifiGotIPHandler) {
+ _wifiGotIPHandler = WiFi.onStationModeGotIP([this](const WiFiEventStationModeGotIP&) {
+ setLEDState(WM_LED_CONNECTED);
+ });
+ }
+ if(!_wifiDisconnectedHandler) {
+ _wifiDisconnectedHandler = WiFi.onStationModeDisconnected([this](const WiFiEventStationModeDisconnected&) {
+ // Ignore the spurious disconnect fired by WiFi.begin(connect=false) / AP teardown
+ // during a successful provisioning save.
+ if(_provisioningState != WM_PROV_CONNECTED) {
+ setLEDState(WM_LED_FAILED);
+ }
+ });
+ }
#elif defined(ESP32)
// if(_wifiAutoReconnect){
// @todo move to seperate method, used for event listener now
@@ -4051,3 +4567,156 @@ void WiFiManager::handleUpdateDone() {
}
#endif
+
+// ---------------------------------------------------------------------------
+// LED behaviour
+// ---------------------------------------------------------------------------
+
+/**
+ * setLEDCallback
+ * Register a function to be called whenever the LED state changes.
+ * The callback receives a wm_ledstate_t:
+ * WM_LED_OFF – LED should be turned off (timeout elapsed)
+ * WM_LED_NOWIFI – No WiFi configured (suggest: Orange, solid)
+ * WM_LED_CONNECTED – WiFi connected (suggest: Green, solid)
+ * WM_LED_FAILED – Connection failed (suggest: Red, solid)
+ * WM_LED_CONNECTING – Connecting in progress (suggest: Blue, pulsing)
+ */
+void WiFiManager::setLEDCallback(std::function func) {
+ _ledcallback = func;
+}
+
+/** Set how long (ms) the LED stays on for the "no WiFi configured" state. 0 = infinite. */
+void WiFiManager::setLEDTimeoutNoWifi(unsigned long ms) {
+ _ledTimeoutNoWifi = ms;
+}
+
+/** Set how long (ms) the LED stays on after a successful WiFi connection. 0 = infinite. */
+void WiFiManager::setLEDTimeoutConnected(unsigned long ms) {
+ _ledTimeoutConnected = ms;
+}
+
+/** Set how long (ms) the LED stays on after a failed connection attempt. 0 = infinite. */
+void WiFiManager::setLEDTimeoutFailed(unsigned long ms) {
+ _ledTimeoutFailed = ms;
+}
+
+/** Set how long (ms) the LED stays on while a connection attempt is in progress. 0 = infinite. */
+void WiFiManager::setLEDTimeoutConnecting(unsigned long ms) {
+ _ledTimeoutConnecting = ms;
+}
+
+/**
+ * setLEDState (private)
+ * Transition to a new LED state, reset the timeout timer, and invoke the
+ * user callback. Calling with the same state that is already active is a
+ * no-op (avoids flooding the callback on every processConfigPortal tick).
+ */
+void WiFiManager::setLEDState(wm_ledstate_t state) {
+ if(_ledcallback == nullptr) return;
+ if(state == _ledCurrentState) return;
+ _ledCurrentState = state;
+ _ledStateStart = millis();
+ _ledcallback(state);
+}
+
+/**
+ * checkLEDTimeout (private)
+ * If the current LED state has been active for longer than its configured
+ * timeout (and the timeout is > 0), transition to WM_LED_OFF.
+ * Called from processConfigPortal() on every iteration.
+ */
+void WiFiManager::checkLEDTimeout() {
+ if(_ledcallback == nullptr) return;
+ if(_ledCurrentState == WM_LED_OFF) return;
+
+ unsigned long timeout = 0;
+ switch(_ledCurrentState) {
+ case WM_LED_NOWIFI: timeout = _ledTimeoutNoWifi; break;
+ case WM_LED_CONNECTED: timeout = _ledTimeoutConnected; break;
+ case WM_LED_FAILED: timeout = _ledTimeoutFailed; break;
+ case WM_LED_CONNECTING: timeout = _ledTimeoutConnecting; break;
+ default: return;
+ }
+
+ if(timeout > 0 && (millis() - _ledStateStart) >= timeout) {
+ _ledCurrentState = WM_LED_OFF;
+ _ledcallback(WM_LED_OFF);
+ }
+}
+
+/**
+ * syncLEDState (private)
+ * Should be called regularly from loop() via process() and from processConfigPortal().
+ * Two jobs:
+ * 1. Always run checkLEDTimeout() so the per-state timer fires even when
+ * the config portal is not active (fixes "OFF never triggered" after autoConnect).
+ * 2. Once per second, correct "stuck on CONNECTING" if WiFi is fully connected
+ * (WL_CONNECTED + valid IP assigned). This covers the case where the
+ * GOT_IP event callback was missed.
+ *
+ * Intentionally NOT done here:
+ * - Converting CONNECTED → FAILED when WiFi.status() is not WL_CONNECTED.
+ * WiFi.status() is not a stable signal: DHCP renewal, a background scan
+ * (ESP8266), or normal beacon-miss recovery can produce a brief
+ * WL_DISCONNECTED reading even on a healthy link. Polling that state and
+ * immediately calling setLEDState(FAILED) causes false-positive red flashes.
+ * Real disconnects are already handled by the event callbacks registered in
+ * WiFi_autoReconnect() (onStationModeDisconnected / ARDUINO_EVENT_WIFI_STA_DISCONNECTED).
+ * - Correcting FAILED/NOWIFI → CONNECTED on an autonomous reconnect.
+ * Those transitions are also handled by the GOT_IP event callback.
+ */
+void WiFiManager::syncLEDState() {
+ if(_ledcallback == nullptr) return;
+
+ // Always check timeout so WM_LED_CONNECTED (and others) time out correctly
+ // even when the config portal loop is not running.
+ checkLEDTimeout();
+
+ // Rate-limit the WiFi-status poll to once per second.
+ if(millis() - _ledLastPoll < 1000) return;
+ _ledLastPoll = millis();
+
+ // Only correct "stuck on blue": if we are still showing CONNECTING but WiFi
+ // has actually fully connected (status AND a valid IP have both settled),
+ // advance to CONNECTED.
+ // We require a non-zero localIP so we do not fire prematurely during the
+ // DHCP-assignment window that checkProvisioningState() waits through.
+ if(_ledCurrentState == WM_LED_CONNECTING
+ && WiFi.status() == WL_CONNECTED
+ && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
+ setLEDState(WM_LED_CONNECTED);
+ }
+}
+
+
+
+/**
+ * setCustomConnectingSVG
+ * Set custom SVG (or any HTML) to display on the provisioning status page
+ * while a WiFi connection attempt is in progress.
+ * Pass NULL to remove.
+ */
+void WiFiManager::setCustomConnectingSVG(const char* svg) {
+ _customConnectingSVG = svg;
+}
+
+/**
+ * setCustomSuccessSVG
+ * Set custom SVG (or any HTML) to display on the provisioning status page
+ * when the WiFi connection succeeds.
+ * Pass NULL to remove.
+ */
+void WiFiManager::setCustomSuccessSVG(const char* svg) {
+ _customSuccessSVG = svg;
+}
+
+/**
+ * setCustomFailureSVG
+ * Set custom SVG (or any HTML) to display on the provisioning status page
+ * when the WiFi connection fails.
+ * Pass NULL to remove.
+ */
+void WiFiManager::setCustomFailureSVG(const char* svg) {
+ _customFailureSVG = svg;
+}
diff --git a/WiFiManager.h b/WiFiManager.h
index 744e65a3..76d5fa4a 100644
--- a/WiFiManager.h
+++ b/WiFiManager.h
@@ -255,6 +255,25 @@ class WiFiManagerParameter {
WM_DEBUG_MAX = 5 // MAX extra dev auditing, var dumps etc (MAX+1 will print timing,mem and frag info)
} wm_debuglevel_t;
+ // Provisioning connection state (AP+STA mode)
+ typedef enum {
+ WM_PROV_IDLE = 0, // no active provisioning
+ WM_PROV_SCANNING = 1, // scanning for networks
+ WM_PROV_CONNECTING = 2, // STA connection in progress
+ WM_PROV_CONNECTED = 3, // STA connected, waiting for AP shutdown delay
+ WM_PROV_FAILED = 4, // STA connection failed
+ WM_PROV_RETRYING = 5 // retrying STA connection
+ } wm_provstate_t;
+
+ // LED indicator state for developer-supplied LED callback
+ typedef enum {
+ WM_LED_OFF = 0, // LED off (timeout elapsed)
+ WM_LED_NOWIFI = 1, // No wifi configured – suggest Orange (infinite by default)
+ WM_LED_CONNECTED = 2, // WiFi connected – suggest Green (15 s by default)
+ WM_LED_FAILED = 3, // Connection failed – suggest Red (infinite by default)
+ WM_LED_CONNECTING = 4 // Trying to connect – suggest Blue pulsing (5 s by default)
+ } wm_ledstate_t;
+
class WiFiManager
{
public:
@@ -514,6 +533,49 @@ class WiFiManager
// get hostname helper
String getWiFiHostname();
+ // --- AP+STA Provisioning options ---
+
+ // if true, keep AP active while STA connection attempt is in progress (default false)
+ void setKeepAPDuringSTAConnect(bool keep);
+
+ // set the delay in ms before the AP is shut down after a successful STA connection (default 10000)
+ void setAPShutdownDelay(unsigned long ms);
+
+ // if true, map low-level errors to descriptive messages in /status JSON (default false)
+ void setDetailedFailureReasons(bool enable);
+
+ // if true (default), register OS captive-portal probe handlers (/generate_204, /hotspot-detect.html, /ncsi.txt)
+ void setCaptivePortalCompatibility(bool enable);
+
+ // get the current provisioning state (see wm_provstate_t)
+ wm_provstate_t getProvisioningState();
+
+ // --- LED behaviour ---
+
+ // Set a callback invoked whenever the LED state changes.
+ // The callback receives a wm_ledstate_t value; WM_LED_OFF means the
+ // timeout has elapsed and the LED should be turned off.
+ void setLEDCallback(std::function func);
+
+ // Per-state LED timeout in milliseconds. 0 = stay on indefinitely.
+ // Defaults: NoWifi=0 (infinite), Connected=15000 (15 s),
+ // Failed=0 (infinite), Connecting=5000 (5 s).
+ void setLEDTimeoutNoWifi(unsigned long ms);
+ void setLEDTimeoutConnected(unsigned long ms);
+ void setLEDTimeoutFailed(unsigned long ms);
+ void setLEDTimeoutConnecting(unsigned long ms);
+
+ // --- Custom animated SVGs for the provisioning status page ---
+
+ // Set a custom SVG (or any HTML) shown while connecting to WiFi.
+ void setCustomConnectingSVG(const char* svg);
+
+ // Set a custom SVG (or any HTML) shown on a successful connection.
+ void setCustomSuccessSVG(const char* svg);
+
+ // Set a custom SVG (or any HTML) shown when the connection failed.
+ void setCustomFailureSVG(const char* svg);
+
std::unique_ptr dnsServer;
@@ -545,6 +607,7 @@ class WiFiManager
unsigned long _webPortalAccessed = 0; // ms last web access time
uint8_t _lastconxresult = WL_IDLE_STATUS; // store last result when doing connect operations
int _numNetworks = 0; // init index for numnetworks wifiscans
+ bool _scanFailed = false; // true when last sync scan returned WIFI_SCAN_FAILED
unsigned long _lastscan = 0; // ms for timing wifi scans
unsigned long _startscan = 0; // ms for timing wifi scans
unsigned long _startconn = 0; // ms for timing wifi connects
@@ -581,13 +644,34 @@ class WiFiManager
// https://github.com/tzapu/WiFiManager/issues/1067
bool _allowExit = true; // allow exit in nonblocking, else user exit/abort calls will be ignored including cptimeout
+ // AP+STA provisioning flow options
+ bool _keepAPDuringSTAConnect = false; // keep AP running while STA connection is in progress
+ unsigned long _apShutdownDelayMs = 10000; // ms to keep AP alive after successful STA connect before shutting down
+ bool _detailedFailureReasons = false; // map low-level errors to human-readable strings in /status
+ bool _captivePortalCompat = true; // register OS captive portal probe handlers (generate_204 etc.)
+
+ // provisioning state machine (used when _keepAPDuringSTAConnect is true)
+ wm_provstate_t _provisioningState = WM_PROV_IDLE;
+ String _provisioningError = "";
+ bool _provisioningConnecting = false; // true while non-blocking STA connect is running
+ unsigned long _apShutdownDeadline = 0; // millis() deadline for AP shutdown
+ bool _apShutdownPending = false; // true when waiting for AP shutdown delay
+ unsigned long _ipWaitStart = 0; // millis() when WL_CONNECTED first seen with 0.0.0.0 IP
+
#ifdef ESP32
wifi_event_id_t wm_event_id = 0;
static uint8_t _lastconxresulttmp; // tmp var for esp32 callback
#endif
+ #ifdef ESP8266
+ // WiFiEventHandler objects must be kept alive for the duration of the
+ // callback registration; storing them as members achieves this.
+ WiFiEventHandler _wifiGotIPHandler;
+ WiFiEventHandler _wifiDisconnectedHandler;
+ #endif
+
#ifndef WL_STATION_WRONG_PASSWORD
- uint8_t WL_STATION_WRONG_PASSWORD = 7; // @kludge define a WL status for wrong password
+ static constexpr uint8_t WL_STATION_WRONG_PASSWORD = 7; // @kludge define a WL status for wrong password
#endif
// parameter options
@@ -619,6 +703,21 @@ class WiFiManager
String _bodyClass = ""; // class to add to body
String _title = FPSTR(S_brand); // app title - default WiFiManager
+ // Custom SVG/HTML slots for provisioning status page
+ const char* _customConnectingSVG = nullptr; // shown while connecting
+ const char* _customSuccessSVG = nullptr; // shown on successful connection
+ const char* _customFailureSVG = nullptr; // shown on connection failure
+
+ // LED state callback and per-state timeouts
+ std::function _ledcallback = nullptr;
+ unsigned long _ledTimeoutNoWifi = 0; // 0 = infinite (orange – no wifi configured)
+ unsigned long _ledTimeoutConnected = 15000; // 15 s (green – connected)
+ unsigned long _ledTimeoutFailed = 0; // 0 = infinite (red – failed)
+ unsigned long _ledTimeoutConnecting = 5000; // 5 s (blue pulsing – connecting)
+ wm_ledstate_t _ledCurrentState = WM_LED_OFF;
+ unsigned long _ledStateStart = 0; // millis() when current LED state was set
+ unsigned long _ledLastPoll = 0; // millis() of last 1-second LED reconciliation
+
// internal options
// wifiscan notes
@@ -680,6 +779,17 @@ class WiFiManager
uint8_t waitForConnectResult(uint32_t timeout);
void updateConxResult(uint8_t status);
+ // provisioning state machine helpers
+ uint8_t checkProvisioningState(); // poll STA status; returns WL_IDLE_STATUS or WL_CONNECTED
+ bool saveWiFiCredentials(String ssid, String pass); // persist credentials only on success
+ String getProvisioningStateStr(); // convert _provisioningState to string for /status JSON
+ String getProvisioningFailureReason(uint8_t status); // map WL status to human-readable string
+
+ // LED state helpers
+ void setLEDState(wm_ledstate_t state); // set LED state and invoke callback
+ void checkLEDTimeout(); // turn LED off when per-state timeout elapses
+ void syncLEDState(); // call from loop(): check timeout + reconcile with WiFi status every 1 s
+
// webserver handlers
public:
void handleNotFound();
@@ -691,6 +801,11 @@ class WiFiManager
void handleInfo();
void handleReset();
+ // OS captive portal probe handlers
+ void handleCaptivePortal204(); // Android /generate_204
+ void handleCaptivePortalHotspot(); // iOS/macOS /hotspot-detect.html
+ void handleCaptivePortalNcsi(); // Windows /ncsi.txt
+
void handleExit();
void handleClose();
// void handleErase();
diff --git a/examples/Provisioning/Provisioning.ino b/examples/Provisioning/Provisioning.ino
new file mode 100644
index 00000000..b7cbea12
--- /dev/null
+++ b/examples/Provisioning/Provisioning.ino
@@ -0,0 +1,242 @@
+/**
+ * WiFiManager – AP+STA Provisioning Flow Example
+ * ================================================
+ * Demonstrates the non-blocking AP+STA provisioning flow:
+ *
+ * 1. On first boot (or when stored credentials fail) the device starts a
+ * SoftAP so users can connect and reach the captive-portal setup page.
+ * 2. The OS automatically opens the portal thanks to the built-in captive-
+ * portal probe handlers (Android /generate_204, iOS /hotspot-detect.html,
+ * Windows /ncsi.txt).
+ * 3. When the user selects a network and submits a password the device starts
+ * a STA connection attempt WITHOUT tearing down the AP or rebooting.
+ * 4. The browser polls /status (JSON) and displays real-time feedback
+ * (connecting → connected / failed).
+ * 5. On success: credentials are saved, the IP address is shown, and the AP
+ * shuts down after a configurable delay (default 15 s).
+ * 6. On failure: a descriptive error is shown and the user can retry – the AP
+ * stays up and no credentials are saved.
+ * 7. On subsequent boots the stored credentials are tried first; if they fail
+ * the provisioning portal starts again automatically.
+ *
+ * LED feedback (NeoPixel RGB on pin 38)
+ * ---------------------------------------
+ * Orange – No WiFi configured (stays on indefinitely)
+ * Blue – Connecting (pulsing, 5 s)
+ * Green – Connected (15 s then off)
+ * Red – Connection failed (stays on indefinitely)
+ *
+ * Custom animated SVGs on the status page
+ * ----------------------------------------
+ * Connecting : spinning ring (blue)
+ * Success : animated check mark (green)
+ * Failure : pulsing X (red)
+ *
+ * Works on ESP32 and ESP8266.
+ * Library: https://github.com/tzapu/WiFiManager
+ */
+
+#include
+#include
+
+// ---- NeoPixel configuration ----
+#define LED_PIN 38
+#define LED_COUNT 1
+Adafruit_NeoPixel pixel(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
+
+// ---- user-configurable options ----
+static const char AP_NAME[] = "ESP-Setup";
+static const char AP_PASS[] = ""; // leave empty for open AP
+static const unsigned long AP_SHUTDOWN_DELAY_MS = 15000; // 15 s
+// -----------------------------------
+
+WiFiManager wm;
+bool provisioningDone = false;
+
+// ----- animated SVG strings -----
+
+// Connecting: spinning blue ring
+static const char SVG_CONNECTING[] PROGMEM =
+ "";
+
+// Success: animated check-mark in a green circle
+static const char SVG_SUCCESS[] PROGMEM =
+ "";
+
+// Failure: pulsing red X
+static const char SVG_FAILURE[] PROGMEM =
+ "";
+
+// ----- LED helpers -----
+
+// Pulse blue: brightness oscillates between ~20% and 100% driven by millis()
+void updateConnectingPulse() {
+ float t = (millis() % 1000) / 1000.0f;
+ uint8_t brightness = (uint8_t)(51 + 204 * (0.5f + 0.5f * sin(t * 2.0f * 3.14159f)));
+ pixel.setPixelColor(0, pixel.Color(0, 0, brightness));
+ pixel.show();
+}
+
+void ledSolid(uint8_t r, uint8_t g, uint8_t b) {
+ pixel.setPixelColor(0, pixel.Color(r, g, b));
+ pixel.show();
+}
+
+void ledOff() {
+ pixel.clear();
+ pixel.show();
+}
+
+// Track whether we're in connecting-pulse mode so loop() can keep updating it
+bool ledPulsing = false;
+
+// LED state callback – invoked by WiFiManager on every state change
+void onLEDState(wm_ledstate_t state) {
+ ledPulsing = false;
+ switch (state) {
+ case WM_LED_NOWIFI:
+ ledSolid(255, 100, 0); // Orange: no WiFi configured
+ Serial.println(F("[LED] Orange – no WiFi configured"));
+ break;
+ case WM_LED_CONNECTING:
+ ledPulsing = true; // Blue pulsing: will be updated in loop()
+ Serial.println(F("[LED] Blue pulsing – connecting"));
+ break;
+ case WM_LED_CONNECTED:
+ ledSolid(0, 200, 0); // Green: connected
+ Serial.println(F("[LED] Green – connected"));
+ break;
+ case WM_LED_FAILED:
+ ledSolid(220, 0, 0); // Red: connection failed
+ Serial.println(F("[LED] Red – connection failed"));
+ break;
+ case WM_LED_OFF:
+ ledOff(); // Timeout elapsed: LED off
+ Serial.println(F("[LED] Off"));
+ break;
+ }
+}
+
+void setup() {
+ Serial.begin(115200);
+ Serial.println(F("\n\n=== WiFiManager AP+STA Provisioning Example ==="));
+
+ // Init NeoPixel
+ pixel.begin();
+ pixel.setBrightness(80); // 0-255; 80 is ~31% – comfortable for indoor use
+ ledOff();
+
+ // Optional: uncomment to wipe saved credentials and force provisioning
+ // wm.resetSettings();
+
+ // ---- AP+STA provisioning options ----
+ wm.setKeepAPDuringSTAConnect(true); // Keep AP while STA connects
+ wm.setAPShutdownDelay(AP_SHUTDOWN_DELAY_MS); // Shut down AP 15 s after success
+ wm.setDetailedFailureReasons(true); // Show "Wrong password" etc.
+ wm.setCaptivePortalCompatibility(true); // Android/iOS/Windows auto-open (default)
+ wm.setShowPassword(true);
+
+ // ---- LED callback ----
+ wm.setLEDCallback(onLEDState);
+ wm.setLEDTimeoutConnected(15000); // Green stays on for 15 s, then off
+ wm.setLEDTimeoutConnecting(5000); // Blue pulsing shown for max 5 s even if still connecting
+ wm.setLEDTimeoutNoWifi(0); // Orange stays on indefinitely
+ wm.setLEDTimeoutFailed(0); // Red stays on indefinitely
+
+ // ---- Custom animated SVGs on the provisioning status page ----
+ wm.setCustomConnectingSVG(SVG_CONNECTING);
+ wm.setCustomSuccessSVG(SVG_SUCCESS);
+ wm.setCustomFailureSVG(SVG_FAILURE);
+
+ // ---- general portal options ----
+ wm.setConfigPortalBlocking(false); // Non-blocking so loop() keeps running
+ wm.setConfigPortalTimeout(0); // 0 = never time out while user is present
+
+ // Callback: called when SoftAP is started
+ wm.setAPCallback([](WiFiManager *w) {
+ Serial.print(F("AP started: "));
+ Serial.println(w->getConfigPortalSSID());
+ Serial.print(F("AP IP: "));
+ Serial.println(WiFi.softAPIP());
+ });
+
+ // Callback: called when credentials are saved and STA is confirmed connected
+ wm.setSaveConfigCallback([]() {
+ Serial.println(F("Credentials saved! STA connected."));
+ });
+
+ // Try stored credentials; on failure start provisioning portal
+ if (wm.autoConnect(AP_NAME, AP_PASS[0] ? AP_PASS : nullptr)) {
+ // Stored credentials worked – device is already connected
+ Serial.print(F("Auto-connected! IP: "));
+ Serial.println(WiFi.localIP());
+ provisioningDone = true;
+ } else {
+ Serial.println(F("No stored credentials (or they failed). Provisioning portal running."));
+ Serial.println(F("Connect to the AP and open the captive portal to configure WiFi."));
+ }
+}
+
+void loop() {
+ // Keep the blue pulse animation alive while connecting
+ if (ledPulsing) {
+ updateConnectingPulse();
+ }
+
+ // Drive the non-blocking portal (processes DNS, HTTP, and the provisioning
+ // state machine). Returns true once STA is connected AND the AP has been
+ // shut down (i.e. provisioning is complete).
+ if (!provisioningDone) {
+ if (wm.process()) {
+ provisioningDone = true;
+ Serial.print(F("Provisioning complete! STA IP: "));
+ Serial.println(WiFi.localIP());
+ }
+
+ // You can also inspect the state directly:
+ wm_provstate_t state = wm.getProvisioningState();
+ static wm_provstate_t lastState = WM_PROV_IDLE;
+ if (state != lastState) {
+ lastState = state;
+ switch (state) {
+ case WM_PROV_CONNECTING:
+ Serial.println(F("[Provisioning] Connecting to STA..."));
+ break;
+ case WM_PROV_CONNECTED:
+ Serial.print(F("[Provisioning] STA connected, IP: "));
+ Serial.println(WiFi.localIP());
+ Serial.println(F("AP will shut down shortly."));
+ break;
+ case WM_PROV_FAILED:
+ Serial.println(F("[Provisioning] Connection failed. Portal still open."));
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ // ---- put your normal application code here ----
+ // It runs concurrently while the provisioning portal is active.
+}
diff --git a/wm_strings_en.h b/wm_strings_en.h
index 137d2e99..d9aba7b6 100644
--- a/wm_strings_en.h
+++ b/wm_strings_en.h
@@ -34,16 +34,17 @@ const char HTTP_SCRIPT[] PROGMEM = ""; // @todo add button states, disable on click , show ack , spinner etc
+
const char HTTP_HEAD_END[] PROGMEM = "
"; // {c} = _bodyclass
// example of embedded logo, base64 encoded inline, No styling here
// const char HTTP_ROOT_MAIN[] PROGMEM = "