feat: prepare WiFiManager 3.1.0

This commit is contained in:
2026-08-27 14:25:38 +10:00
parent c10f1a3e92
commit 25977a7320
21 changed files with 1214 additions and 321 deletions
+177 -69
View File
@@ -1,9 +1,9 @@
/**
* WiFiManager.h
*
*
* WiFiManager, a library for the ESP8266/Arduino platform
* for configuration of WiFi credentials using a Captive Portal
*
*
* @author Creator tzapu
* @author tablatronix
* @author alexhopeoconnor
@@ -22,6 +22,7 @@
#include <vector>
#include <functional>
#include <string.h>
#include "WiFiManagerParameter.h"
#include "WiFiManagerLogLevel.h"
@@ -39,13 +40,13 @@
// #define WM_DFTE_LOGGING // opt-in: bridge DFTE logging into WiFiManager (see README)
// #define WM_MDNS // includes MDNS, also set MDNS with sethostname
// #define WM_FIXERASECONFIG // use erase flash fix
// #define WM_ERASE_NVS // esp32 erase(true) will erase NVS
// #define WM_ERASE_NVS // esp32 erase(true) will erase NVS
// #define WM_RTC // esp32 info page will include reset reasons
// #define WIFI_MANAGER_OVERRIDE_STRINGS // build flag for using own strings include
#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
#warning "ARDUINO_ESP8266_RELEASE_2_3_0, some WM features disabled"
#warning "ARDUINO_ESP8266_RELEASE_2_3_0, some WM features disabled"
// @todo check failing on platform = espressif8266@1.7.3
#define WM_NOASYNC // esp8266 no async scan wifi
#define WM_NOCOUNTRY // esp8266 no country
@@ -91,16 +92,16 @@
#include <ESP8266mDNS.h>
#endif
#define WIFI_getChipId() ESP.getChipId()
#define WIFI_getChipId() ESP.getChipId()
#define WM_WIFIOPEN ENC_TYPE_NONE
#elif defined(ESP32)
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_wifi.h>
#include <Update.h>
#include <AsyncTCP.h>
#define WIFI_getChipId() (uint32_t)ESP.getEfuseMac()
#define WM_WIFIOPEN WIFI_AUTH_OPEN
@@ -142,12 +143,44 @@
// Include utility functions
#include "WiFiManagerUtils.h"
// A station profile is intentionally fixed-size. ESP Wi-Fi accepts one station
// configuration at a time, so multi-network behaviour belongs to the
// application/controller rather than the SDK's saved station configuration.
constexpr uint8_t WM_STATION_PROFILE_COUNT = 2;
constexpr uint8_t WM_NO_STATION_PROFILE = 0xFF;
struct WiFiManagerStationProfile {
bool enabled = false;
bool hasPassword = false;
char ssid[33] = {};
char password[65] = {};
};
struct WiFiManagerStationProfiles {
WiFiManagerStationProfile slots[WM_STATION_PROFILE_COUNT] = {};
uint8_t preferredSlot = 0;
uint8_t lastSuccessfulSlot = WM_NO_STATION_PROFILE;
};
/**
* Optional durable backing for multi-profile station credentials. The manager
* owns profile policy and never owns this store. A consumer that needs durable
* profiles supplies one; otherwise the profiles remain in RAM.
*/
class WiFiManagerStationProfileStore {
public:
virtual bool load(WiFiManagerStationProfiles& profiles) = 0;
virtual bool save(const WiFiManagerStationProfiles& candidate) = 0;
virtual bool clear() = 0;
virtual ~WiFiManagerStationProfileStore() = default;
};
// prep string concat vars
#define WM_STRING2(x) #x
#define WM_STRING(x) WM_STRING2(x)
// WiFiManager version
const char WM_VERSION_STR[] PROGMEM = "v2.0.19";
const char WM_VERSION_STR[] PROGMEM = "v3.1.0";
// #include <esp_idf_version.h>
#ifdef ESP_IDF_VERSION
@@ -155,7 +188,7 @@ const char WM_VERSION_STR[] PROGMEM = "v2.0.19";
// #pragma message "ESP_IDF_VERSION_MINOR = " WM_STRING(ESP_IDF_VERSION_MINOR)
// #pragma message "ESP_IDF_VERSION_PATCH = " WM_STRING(ESP_IDF_VERSION_PATCH)
#define VER_IDF_STR WM_STRING(ESP_IDF_VERSION_MAJOR) "." WM_STRING(ESP_IDF_VERSION_MINOR) "." WM_STRING(ESP_IDF_VERSION_PATCH)
#else
#else
#define VER_IDF_STR "Unknown"
#endif
@@ -184,7 +217,7 @@ const char WM_VERSION_STR[] PROGMEM = "v2.0.19";
#define VER_ARDUINO_STR "Unknown"
#endif
#endif
#else
#else
#define VER_ARDUINO_STR "Unknown"
#endif
@@ -327,6 +360,27 @@ class WiFiManager
WM_CP_CONNECT_FAILED,
};
enum wm_station_state_t : uint8_t {
WM_STATION_IDLE = 0,
WM_STATION_LOADING,
WM_STATION_ATTEMPTING,
WM_STATION_SWITCHING,
WM_STATION_CONNECTED,
WM_STATION_BACKOFF,
WM_STATION_PORTAL,
};
struct wm_station_status_t {
wm_station_state_t state = WM_STATION_IDLE;
uint8_t activeSlot = WM_NO_STATION_PROFILE;
uint8_t attemptedSlot = WM_NO_STATION_PROFILE;
uint8_t configuredProfiles = 0;
uint8_t wifiStatus = WL_IDLE_STATUS;
bool lastConnectionWasCandidate = false;
bool storageSaveFailed = false;
String message = "Idle";
};
/** Optional notification hook; prefer getters for consumers. */
enum wm_event_t : uint8_t {
WM_EVENT_PORTAL_STARTED = 0,
@@ -334,10 +388,16 @@ class WiFiManager
WM_EVENT_PORTAL_CONNECT_QUEUED,
WM_EVENT_PORTAL_CONNECT_START,
WM_EVENT_PORTAL_CONNECT_SUCCESS,
WM_EVENT_PORTAL_CONNECT_FAILED
WM_EVENT_PORTAL_CONNECT_FAILED,
WM_EVENT_STATION_PROFILE_ATTEMPT,
WM_EVENT_STATION_PROFILE_CONNECTED,
WM_EVENT_STATION_PROFILE_FAILED,
WM_EVENT_STATION_LINK_LOST,
WM_EVENT_STATION_BACKOFF,
WM_EVENT_STATION_PROFILES_CLEARED
};
using WiFiManagerEventCallback = std::function<void(wm_event_t)>;
WiFiManager(Print& consolePort);
WiFiManager();
~WiFiManager();
@@ -347,14 +407,28 @@ class WiFiManager
boolean autoConnect();
boolean autoConnect(char const *apName, char const *apPassword = NULL);
// Fixed two-profile station mode. When a store is attached, WiFiManager
// owns profile selection and explicit connection attempts rather than the
// platform's one saved station configuration.
void setStationProfileStore(WiFiManagerStationProfileStore* store);
bool startStationConnection(char const *apName = NULL, char const *apPassword = NULL);
bool startStationCandidate(const WiFiManagerStationProfiles& candidate);
bool startStationCandidate(const WiFiManagerStationProfiles& candidate, char const *apName, char const *apPassword = NULL);
bool saveStationProfiles(const WiFiManagerStationProfiles& profiles);
void clearStationProfiles();
bool isStationProfileMode() const;
void setStationRecoveryInterval(unsigned long intervalMs);
const WiFiManagerStationProfiles& getStationProfiles() const;
const wm_station_status_t& getStationStatus() const;
//manually start the config portal, autoconnect does this automatically on connect failure
void startConfigPortal(); // auto generates apname
void startConfigPortal(char const *apName, char const *apPassword = NULL);
//manually stop the config portal - immediately shuts down the portal
void stopConfigPortal();
//manually start the web portal, autoconnect does this automatically on connect failure
//manually start the web portal, autoconnect does this automatically on connect failure
void startWebPortal();
//manually stop the web portal if started manually
@@ -438,13 +512,13 @@ class WiFiManager
// sets number of retries for autoconnect, force retry after wait failure exit
void setConnectRetries(uint8_t numRetries); // default 1
//sets timeout for which to attempt connecting on saves, useful if there are bugs in esp waitforconnectloop
void setSaveConnectTimeout(unsigned long seconds);
// lets you disable automatically connecting after save from webportal
void setSaveConnect(bool connect = true);
void setLogEnabled(boolean enabled);
void setLogPrefix(String prefix);
void setLogOutput(boolean enabled, WiFiManagerLogLevel maxLevel);
@@ -463,44 +537,44 @@ class WiFiManager
//set min quality percentage to include in scan, defaults to 8% if not specified
void setMinimumSignalQuality(int quality = 8);
//sets a custom ip /gateway /subnet configuration
void setAPStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn);
//sets config for a static IP
void setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn);
//sets config for a static IP with DNS
void setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn, IPAddress dns);
//if this is set, it will exit after config, even if connection is unsuccessful.
void setBreakAfterConfig(boolean shouldBreak);
//if this is true, remove duplicated Access Points - defaut true
void setRemoveDuplicateAPs(boolean removeDuplicates);
//setter for ESP wifi.persistent so we can remember it and restore user preference, as WIFi._persistent is protected
void setRestorePersistent(boolean persistent);
//if true, always show static net inputs, IP, subnet, gateway, else only show if set via setSTAStaticIPConfig
void setShowStaticFields(boolean alwaysShow);
//if true, always show static dns, esle only show if set via setSTAStaticIPConfig
void setShowDnsFields(boolean alwaysShow);
//if false, timeout captive portal even if a STA client connected to softAP (false), suggest disabling if captiveportal is open
void setAPClientCheck(boolean enabled);
//if true, reset timeout when webclient connects (true), suggest disabling if captiveportal is open
//if true, reset timeout when webclient connects (true), suggest disabling if captiveportal is open
void setWebPortalClientCheck(boolean enabled);
// if true, enable autoreconnecting
void setWiFiAutoReconnect(boolean enabled);
// if true, wifiscan will show percentage instead of quality icons, until we have better templating
void setScanDispPerc(boolean enabled);
// if true (default) then start the config portal from autoConnect if connection failed
void setEnableConfigPortal(boolean enable);
@@ -513,13 +587,13 @@ class WiFiManager
// set ap channel
void setWiFiAPChannel(int32_t channel);
// set ap hidden
void setWiFiAPHidden(bool hidden); // default false
// clean connect, always disconnect before connecting
void setCleanConnect(bool enable); // default false
// ---- Portal (all customization entry points use the portal* prefix) ----
void portalSetBrandTitle(const String& title);
void portalSetContextIdentityText(const String& identityText);
@@ -563,10 +637,10 @@ class WiFiManager
// get last connection result, including autoconnect and portal credential-save attempts
uint8_t getLastConxResult();
// get a status as string
String getWLStatusString(uint8_t status);
String getWLStatusString();
String getWLStatusString(uint8_t status);
String getWLStatusString();
// get wifi mode as string
String getModeString(uint8_t mode);
@@ -574,7 +648,7 @@ class WiFiManager
// check if the module has a saved ap to connect to
bool getWiFiIsSaved();
// helper to get saved password, if persistent get stored, else get current if connected
// helper to get saved password, if persistent get stored, else get current if connected
String getWiFiPass(bool persistent = true);
// helper to get saved ssid, if persistent get stored, else get current if connected
@@ -588,17 +662,17 @@ class WiFiManager
// helper for html
String htmlEntities(String str, bool whitespace = false);
// set the country code for wifi settings, CN
void setCountry(String cc);
// get default ap esp uses , esp_chipid etc
String getDefaultAPName();
// set the WiFi SSID prefix for default AP name, default platform-specific (ESP/ESP32/WM)
void setWiFiSSIDPrefix(String prefix);
// set port of webserver, 80
void setHttpPort(uint16_t port);
@@ -615,7 +689,7 @@ class WiFiManager
uint8_t getConfigPortalConnectStatus() const;
String getConfigPortalConnectMessage() const;
void setEventCallback(WiFiManagerEventCallback cb);
// check if web portal is active (true)
bool getWebPortalActive();
@@ -624,10 +698,10 @@ class WiFiManager
// get hostname helper
String getWiFiHostname();
// get server instance (for testing)
AsyncWebServer* getServer();
// get DNS server instance (for testing)
DNSServer* getDNSServer();
@@ -636,7 +710,7 @@ class WiFiManager
class WiFiManagerRequestArgs {
public:
std::unordered_map<std::string, std::string> args;
// Constructor - builds from AsyncWebServerRequest
WiFiManagerRequestArgs(AsyncWebServerRequest* request) {
if (request) {
@@ -647,19 +721,19 @@ class WiFiManager
}
}
}
// Default constructor for tests and manually assembled argument sets
WiFiManagerRequestArgs() {}
// Check if argument exists
bool hasArg(const char* name) const {
return args.find(std::string(name)) != args.end();
}
bool hasArg(const String& name) const {
return hasArg(name.c_str());
}
// Get argument value as String
String getArg(const char* name, const String& defaultValue = "") const {
auto it = args.find(std::string(name));
@@ -668,29 +742,29 @@ class WiFiManager
}
return defaultValue;
}
String getArg(const String& name, const String& defaultValue = "") const {
return getArg(name.c_str(), defaultValue);
}
// Type conversion helpers
int getArgAsInt(const char* name, int defaultValue = 0) const {
String value = getArg(name);
return value.length() > 0 ? value.toInt() : defaultValue;
}
float getArgAsFloat(const char* name, float defaultValue = 0.0f) const {
String value = getArg(name);
return value.length() > 0 ? value.toFloat() : defaultValue;
}
bool getArgAsBool(const char* name, bool defaultValue = false) const {
String value = getArg(name);
if (value.length() == 0) return defaultValue;
return value == "1" || value.equalsIgnoreCase("true") ||
return value == "1" || value.equalsIgnoreCase("true") ||
value.equalsIgnoreCase("on") || value.equalsIgnoreCase("yes");
}
size_t count() const {
return args.size();
}
@@ -723,7 +797,7 @@ class WiFiManager
WiFiScanRuntimeState _scan;
std::vector<WiFiScanNetwork> _scanResultsCache;
bool _scanLifecycleBlocked = false;
// async reboot/abort scheduling
bool _rebootScheduled = false; // flag for scheduled reboot
unsigned long _rebootTime = 0; // ms when reboot should occur
@@ -742,13 +816,30 @@ class WiFiManager
String _defaultssid = ""; // preload ssid
String _defaultpass = ""; // preload pass
WiFiManagerStationProfileStore* _stationProfileStore = nullptr;
WiFiManagerStationProfiles _stationProfiles;
WiFiManagerStationProfiles _stationCandidate;
wm_station_status_t _stationStatus;
bool _stationProfilesLoaded = false;
bool _stationCandidateActive = false;
bool _stationCandidateFromPortal = false;
bool _stationEverConnected = false;
uint8_t _stationAttemptMask = 0;
uint8_t _stationPendingSlot = WM_NO_STATION_PROFILE;
unsigned long _stationNextAttemptAt = 0;
unsigned long _stationAttemptStartedAt = 0;
unsigned long _stationBackoffStartedAt = 0;
unsigned long _stationRecoveryInterval = 5000UL;
String _stationPortalApName = "";
String _stationPortalApPassword = "";
// options flags
unsigned long _configPortalTimeout = 0; // ms close config portal loop if set (depending on _cp/webClientCheck options)
unsigned long _connectTimeout = 0; // ms stop trying to connect to ap if set
unsigned long _saveTimeout = 0; // ms stop trying to connect to ap on saves, in case bugs in esp waitforconnectresult
WiFiMode_t _usermode = WIFI_STA; // Default user mode
String _wifissidprefix =
String _wifissidprefix =
#ifdef ESP8266
"ESP"
#elif defined(ESP32)
@@ -808,7 +899,7 @@ class WiFiManager
PortalStructuredExtrasState _portalStructured;
// internal options
// wifiscan notes
// currently disabled due to issues with caching, sometimes first scan is empty esp32 wifi not init yet race, or portals hit server nonstop flood
// The following are background wifi scanning optimizations
@@ -819,22 +910,22 @@ class WiFiManager
// async enables asyncronous scans, so they do not block anything
// the refresh button bypasses cache
// no aps found is problematic as scans are always going to want to run, leading to page load delays
//
//
// These settings really only make sense with _preloadwifiscan true
// but not limited to, we could run continuous background scans on various page hits, or xhr hits
// which would be better coupled with asyncscan
// atm preload is only done on root hit and startcp
//
//
// preload scanning causes AP to delay showing for users, but also caches and lets the cp load faster once its open
// my scan takes 7-10 seconds
public:
boolean _preloadwifiscan = false; // preload wifiscan if true
unsigned int _scancachetime = 30000; // ms cache time for preload scans
protected:
boolean _autoforcerescan = false; // automatically force rescan if scan networks is 0, ignoring cache
boolean _disableIpFields = false; // modify function of setShow_X_Fields(false), forces ip fields off instead of default show if set, eg. _staShowStaticFields=-1
String _wificountry = ""; // country code, @todo define in strings lang
@@ -848,7 +939,7 @@ protected:
void setupConfigPortal();
bool shutdownConfigPortal();
bool setupHostname(bool restart);
#ifdef NO_EXTRA_4K_HEAP
boolean _tryWPS = false; // try WPS on save failure, unsupported
void startWPS();
@@ -869,6 +960,23 @@ protected:
bool wifiConnectDefault();
bool wifiConnectNew(String ssid, String pass,bool connect = true);
void processStationController();
bool beginStationProfile(uint8_t slot);
void queueStationProfile(uint8_t slot);
void beginStationCycle(bool preferLastSuccessful);
void handleStationAttemptFailure(uint8_t status, const String& message);
void handleStationConnectionSuccess();
void completePortalStationAttempt(bool success, uint8_t status, const String& message);
void enterStationPortal();
bool hasUsableStationConnection() const;
bool isStationProfileEnabled(const WiFiManagerStationProfiles& profiles, uint8_t slot) const;
uint8_t configuredStationProfileCount(const WiFiManagerStationProfiles& profiles) const;
uint8_t chooseStationProfile(const WiFiManagerStationProfiles& profiles, bool preferLastSuccessful) const;
const WiFiManagerStationProfiles& stationProfilesForAttempt() const;
WiFiManagerStationProfiles& stationProfilesForAttempt();
bool validateStationProfiles(const WiFiManagerStationProfiles& profiles) const;
unsigned long stationAttemptTimeout() const;
uint8_t waitForConnectResult();
uint8_t waitForConnectResult(uint32_t timeout);
void updateConxResult(uint8_t status);
@@ -931,12 +1039,12 @@ protected:
#define WM_DISCONWORKAROUND
#endif
#else
#else
#define WM_NOCOUNTRY
#endif
#ifdef WM_NOCOUNTRY
#warning "ESP32 set country unavailable"
#warning "ESP32 set country unavailable"
#endif
@@ -1042,9 +1150,9 @@ protected:
// if we decide to support this, these checks will need to be replaced with something client aware to check if client origin is ap or web
// These state checks are critical and used for internal function checks
boolean webPortalActive = false;
boolean storeSTAmode = true; // option store persistent STA mode in connectwifi
boolean storeSTAmode = true; // option store persistent STA mode in connectwifi
int timer = 0; // timer for debug throttle for numclients, and portal timeout messages
// WiFiManagerParameter
int _paramsCount = 0;
int _max_params;
@@ -101,6 +101,7 @@ class WiFiManagerHandlers {
void appendVisibleScanResultsJson(String& json, const std::vector<const WiFiManager::WiFiScanNetwork*>& networks);
void applyWifiAndParamsFromRequest(AsyncWebServerRequest *request);
bool buildStationProfilesFromRequest(AsyncWebServerRequest *request, WiFiManagerStationProfiles& profiles);
void buildPlainStatusSummary(String& out);
void appendPortalJsonStaticFields(String& json, bool& first);
void appendPortalJsonCustomParams(String& json, bool& first);
+53 -10
View File
@@ -465,6 +465,14 @@ function bindWifiViewEvents(){
if(p)p.type=this.checked?'text':'password';
});
}
var profileShowPass=document.querySelectorAll('.wm-showpass');
var profileIndex;
for(profileIndex=0;profileIndex<profileShowPass.length;profileIndex++){
profileShowPass[profileIndex].addEventListener('change',function(){
var target=$(this.getAttribute('data-target')||'');
if(target)target.type=this.checked?'text':'password';
});
}
var scanResults=$('wm-scan-results');
if(scanResults){
@@ -473,8 +481,8 @@ function bindWifiViewEvents(){
while(el && el !== scanResults){
if(el.tagName==='A' && el.getAttribute('data-ssid')){
ev.preventDefault();
var inp=$('wm-s');
var pass=$('wm-p');
var inp=$('wm-s0')||$('wm-s');
var pass=$('wm-p0')||$('wm-p');
if(inp)inp.value=el.getAttribute('data-ssid')||'';
if(pass){
if(pass.scrollIntoView)pass.scrollIntoView({behavior:'smooth',block:'center'});
@@ -491,6 +499,30 @@ function bindWifiViewEvents(){
}
}
function renderStationProfileFields(profiles){
var html='';
var i;
for(i=0;i<profiles.length;i++){
var p=profiles[i]||{};
var primary=i===0;
var title=primary?'Primary WiFi':'Fallback WiFi (optional)';
var hint=primary
? 'This network is tried first for a new connection.'
: 'Tried when the primary network is unavailable.';
html+="<div class='wm-card wm-station-profile'><h2>"+esc(title)+"</h2><p class='wm-help'>"+esc(hint)+"</p>";
html+="<label for='wm-s"+i+"'>SSID"+(primary?'':' (leave empty to disable)')+"</label>";
html+="<input id='wm-s"+i+"' name='s"+i+"' type='text' maxlength='32' value='"+esc(p.ssid||'')+"'/>";
html+="<label for='wm-p"+i+"'>Password</label>";
html+="<input id='wm-p"+i+"' class='wm-profile-password' name='p"+i+"' type='password' maxlength='64' placeholder='"+(p.passwordSet?'Configured — enter a new password to replace it':'Leave blank for an open network')+"'/>";
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass"+i+"' class='wm-showpass' data-target='wm-p"+i+"'/> <label for='wm-showpass"+i+"'>Show password</label></div>";
if(p.passwordSet){
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-clear"+i+"' name='clear"+i+"' value='1'/> <label for='wm-clear"+i+"'>Clear password / use an open network</label></div>";
}
html+='</div>';
}
return html;
}
function viewWifi(){
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading WiFi options…</p></div>");
api('/api/wifi/meta').then(function(res){
@@ -502,17 +534,25 @@ function viewWifi(){
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button></div>";
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
html+="<form id='wm-wifi-form'>";
html+=renderFieldList(m.wifiFields||[]);
var wf=m.wifiFields||[];
var hasPass=false;
var i;
for(i=0;i<wf.length;i++){if(wf[i]&&wf[i].id==='p'){hasPass=true;break;}}
if(hasPass){
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/> <label for='wm-showpass'>Show password</label></div>";
if(Array.isArray(m.profiles) && m.profiles.length){
html+=renderStationProfileFields(m.profiles);
}else{
html+=renderFieldList(m.wifiFields||[]);
var wf=m.wifiFields||[];
var hasPass=false;
var i;
for(i=0;i<wf.length;i++){if(wf[i]&&wf[i].id==='p'){hasPass=true;break;}}
if(hasPass){
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/> <label for='wm-showpass'>Show password</label></div>";
}
}
html+=renderFieldList(m.staticFields||[]);
html+=renderFieldList(m.params||[]);
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('connect','Save and connect')+"</button></div></form>";
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block' name='stationAction' value='connect'>"+labelWithIcon('connect','Save and connect')+"</button>";
if(Array.isArray(m.profiles) && m.profiles.length){
html+="<button type='submit' class='wm-btn wm-btn--secondary wm-btn--block' name='stationAction' value='save'>Save for later</button>";
}
html+="</div></form>";
html+="<div id='wm-wifi-msg'></div></div>";
setView(html);
bindWifiViewEvents();
@@ -598,6 +638,9 @@ function pollWifiConnectStatus(){
window.portalWifiSave=function(ev){
ev.preventDefault();
var fd=new FormData(document.getElementById('wm-wifi-form'));
if(ev.submitter && ev.submitter.name){
fd.append(ev.submitter.name,ev.submitter.value||'');
}
var msg=$('wm-wifi-msg');
if(msg)msg.innerHTML='Submitting...';
api('/api/wifi/save',{method:'POST',body:fd}).then(function(res){
+466 -2
View File
@@ -27,6 +27,7 @@ uint8_t WiFiManager::_lastconxresulttmp = WL_IDLE_STATUS;
namespace {
constexpr size_t kMaxHostnameLength = 32;
constexpr uint8_t kSoftApStartMaxAttempts = 3;
constexpr unsigned long kStationProfileSwitchDelayMs = 2000UL; // ESP WiFi needs time to leave a failed association.
bool isValidHostnameChar(char c) {
return isAlphaNumeric(c) || c == '-';
@@ -197,6 +198,465 @@ void WiFiManager::_end(){
if(_userpersistent) WiFi.persistent(true); // reenable persistent, there is no getter we rely on _userpersistent
}
void WiFiManager::setStationProfileStore(WiFiManagerStationProfileStore* store) {
_stationProfileStore = store;
_stationProfilesLoaded = false;
_stationCandidateActive = false;
_stationCandidateFromPortal = false;
_stationAttemptMask = 0;
_stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0;
_stationStatus = wm_station_status_t();
}
bool WiFiManager::isStationProfileMode() const {
return _stationProfileStore != nullptr;
}
void WiFiManager::setStationRecoveryInterval(unsigned long intervalMs) {
if (intervalMs > 0) {
_stationRecoveryInterval = intervalMs;
}
}
const WiFiManagerStationProfiles& WiFiManager::getStationProfiles() const {
return _stationProfiles;
}
const WiFiManager::wm_station_status_t& WiFiManager::getStationStatus() const {
return _stationStatus;
}
bool WiFiManager::isStationProfileEnabled(const WiFiManagerStationProfiles& profiles, uint8_t slot) const {
return slot < WM_STATION_PROFILE_COUNT && profiles.slots[slot].enabled && profiles.slots[slot].ssid[0] != '\0';
}
uint8_t WiFiManager::configuredStationProfileCount(const WiFiManagerStationProfiles& profiles) const {
uint8_t count = 0;
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
if (isStationProfileEnabled(profiles, slot)) {
++count;
}
}
return count;
}
bool WiFiManager::validateStationProfiles(const WiFiManagerStationProfiles& profiles) const {
if (profiles.preferredSlot >= WM_STATION_PROFILE_COUNT || !isStationProfileEnabled(profiles, 0)) {
return false;
}
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
const WiFiManagerStationProfile& profile = profiles.slots[slot];
if (!profile.enabled) {
continue;
}
if (memchr(profile.ssid, '\0', sizeof(profile.ssid)) == nullptr ||
memchr(profile.password, '\0', sizeof(profile.password)) == nullptr) {
return false;
}
}
return profiles.lastSuccessfulSlot == WM_NO_STATION_PROFILE ||
isStationProfileEnabled(profiles, profiles.lastSuccessfulSlot);
}
uint8_t WiFiManager::chooseStationProfile(const WiFiManagerStationProfiles& profiles,
bool preferLastSuccessful) const {
if (preferLastSuccessful && isStationProfileEnabled(profiles, profiles.lastSuccessfulSlot)) {
return profiles.lastSuccessfulSlot;
}
if (isStationProfileEnabled(profiles, profiles.preferredSlot)) {
return profiles.preferredSlot;
}
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
if (isStationProfileEnabled(profiles, slot)) {
return slot;
}
}
return WM_NO_STATION_PROFILE;
}
const WiFiManagerStationProfiles& WiFiManager::stationProfilesForAttempt() const {
return _stationCandidateActive ? _stationCandidate : _stationProfiles;
}
WiFiManagerStationProfiles& WiFiManager::stationProfilesForAttempt() {
return _stationCandidateActive ? _stationCandidate : _stationProfiles;
}
unsigned long WiFiManager::stationAttemptTimeout() const {
// The synchronous legacy connection path allows an unlimited timeout. A
// profile controller must stay bounded so it can advance to the fallback.
return _connectTimeout > 0 ? _connectTimeout : 15000UL;
}
bool WiFiManager::hasUsableStationConnection() const {
if (WiFi.status() != WL_CONNECTED) {
return false;
}
return WiFi.localIP() != IPAddress(0, 0, 0, 0);
}
bool WiFiManager::startStationConnection(char const *apName, char const *apPassword) {
if (!isStationProfileMode()) {
return false;
}
_begin();
_stationPortalApName = apName ? apName : getDefaultAPName();
_stationPortalApPassword = apPassword ? apPassword : "";
_stationCandidateActive = false;
_stationCandidateFromPortal = false;
_stationAttemptMask = 0;
_stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0;
_stationStatus = wm_station_status_t();
_stationStatus.state = WM_STATION_LOADING;
_stationStatus.message = F("Loading WiFi profiles");
WiFiManagerStationProfiles loaded;
if (_stationProfileStore->load(loaded)) {
_stationProfiles = loaded;
} else {
_stationProfiles = WiFiManagerStationProfiles();
}
_stationProfilesLoaded = true;
_stationStatus.configuredProfiles = configuredStationProfileCount(_stationProfiles);
if (!validateStationProfiles(_stationProfiles)) {
_stationStatus.message = _stationStatus.configuredProfiles == 0
? F("No WiFi profiles configured")
: F("Stored WiFi profiles are invalid");
enterStationPortal();
return false;
}
beginStationCycle(true);
return true;
}
bool WiFiManager::startStationCandidate(const WiFiManagerStationProfiles& candidate) {
if (!validateStationProfiles(candidate)) {
_stationStatus.state = WM_STATION_IDLE;
_stationStatus.message = F("WiFi profile candidate is invalid");
return false;
}
if (!_stationProfilesLoaded && _stationProfileStore) {
WiFiManagerStationProfiles loaded;
_stationProfiles = _stationProfileStore->load(loaded)
? loaded
: WiFiManagerStationProfiles();
_stationProfilesLoaded = true;
}
_begin();
_stationCandidate = candidate;
_stationCandidateActive = true;
_stationCandidateFromPortal = configPortalActive;
_stationAttemptMask = 0;
_stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0;
_stationStatus = wm_station_status_t();
_stationStatus.state = WM_STATION_LOADING;
_stationStatus.configuredProfiles = configuredStationProfileCount(candidate);
_stationStatus.message = F("WiFi profile candidate queued");
if (_stationCandidateFromPortal) {
_cpConnectStatus = WL_IDLE_STATUS;
_cpConnectStationIp = "";
_cpConnectState = wm_cp_connect_state_t::queued;
_cpConnectMessage = F("WiFi profile candidate queued");
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_QUEUED);
}
beginStationCycle(false);
return true;
}
bool WiFiManager::startStationCandidate(const WiFiManagerStationProfiles& candidate,
char const *apName, char const *apPassword) {
_stationPortalApName = apName ? apName : getDefaultAPName();
_stationPortalApPassword = apPassword ? apPassword : "";
return startStationCandidate(candidate);
}
bool WiFiManager::saveStationProfiles(const WiFiManagerStationProfiles& profiles) {
if (!isStationProfileMode() || !validateStationProfiles(profiles)) {
return false;
}
if (!_stationProfileStore->save(profiles)) {
_stationStatus.storageSaveFailed = true;
_stationStatus.message = F("WiFi profiles could not be saved");
return false;
}
_stationProfiles = profiles;
_stationProfilesLoaded = true;
_stationStatus.configuredProfiles = configuredStationProfileCount(_stationProfiles);
_stationStatus.storageSaveFailed = false;
_stationStatus.message = F("WiFi profiles saved");
return true;
}
void WiFiManager::clearStationProfiles() {
if (_stationProfileStore && !_stationProfileStore->clear()) {
_stationStatus.storageSaveFailed = true;
_stationStatus.message = F("WiFi profiles could not be cleared");
return;
}
WiFi_Disconnect();
WiFi_eraseConfig();
_stationProfiles = WiFiManagerStationProfiles();
_stationCandidate = WiFiManagerStationProfiles();
_stationCandidateActive = false;
_stationCandidateFromPortal = false;
_stationProfilesLoaded = false;
_stationAttemptMask = 0;
_stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0;
_stationEverConnected = false;
_stationStatus = wm_station_status_t();
_stationStatus.message = F("WiFi profiles cleared");
emitPortalEvent(WM_EVENT_STATION_PROFILES_CLEARED);
}
void WiFiManager::beginStationCycle(bool preferLastSuccessful) {
_stationAttemptMask = 0;
_stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0;
const uint8_t first = chooseStationProfile(stationProfilesForAttempt(), preferLastSuccessful);
if (first == WM_NO_STATION_PROFILE) {
enterStationPortal();
return;
}
beginStationProfile(first);
}
bool WiFiManager::beginStationProfile(uint8_t slot) {
const WiFiManagerStationProfiles& profiles = stationProfilesForAttempt();
if (!isStationProfileEnabled(profiles, slot)) {
return false;
}
const WiFiManagerStationProfile& profile = profiles.slots[slot];
_stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0;
_stationAttemptMask |= static_cast<uint8_t>(1U << slot);
_stationAttemptStartedAt = millis();
_stationStatus.state = WM_STATION_ATTEMPTING;
_stationStatus.attemptedSlot = slot;
_stationStatus.wifiStatus = WL_IDLE_STATUS;
_stationStatus.message = String(F("Connecting profile ")) + String(slot + 1);
emitPortalEvent(WM_EVENT_STATION_PROFILE_ATTEMPT);
WiFi.persistent(false);
WiFi.setAutoReconnect(false);
WiFi_enableSTA(true, false);
setSTAConfig();
if (_cleanConnect) {
WiFi_Disconnect();
}
WiFi.begin(
profile.ssid,
profile.hasPassword ? profile.password : nullptr);
return true;
}
void WiFiManager::queueStationProfile(uint8_t slot) {
_stationPendingSlot = slot;
_stationNextAttemptAt = millis() + kStationProfileSwitchDelayMs;
_stationStatus.state = WM_STATION_SWITCHING;
_stationStatus.message = String(F("Switching to profile ")) + String(slot + 1);
WiFi_Disconnect();
}
void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, const String& message) {
if (!_stationCandidateFromPortal && !configPortalActive) {
return;
}
_cpConnectStatus = status;
_cpConnectMessage = message;
_cpConnectStationIp = success ? WiFi.localIP().toString() : "";
_cpConnectState = success ? wm_cp_connect_state_t::success : wm_cp_connect_state_t::failed;
if (success) {
if (_savewificallback != NULL) {
_savewificallback();
}
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_SUCCESS);
if (_disableConfigPortal) {
shutdownConfigPortal();
}
} else {
updateConxResult(status);
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_FAILED);
}
}
void WiFiManager::handleStationConnectionSuccess() {
const uint8_t slot = _stationStatus.attemptedSlot;
const bool wasCandidate = _stationCandidateActive;
const bool wasPortalCandidate = _stationCandidateFromPortal;
bool persisted = true;
if (wasCandidate) {
const WiFiManagerStationProfiles previous = _stationProfiles;
_stationCandidate.lastSuccessfulSlot = slot;
if (_stationProfileStore) {
persisted = _stationProfileStore->save(_stationCandidate);
}
if (persisted) {
_stationProfiles = _stationCandidate;
_stationProfilesLoaded = true;
} else {
_stationProfiles = previous;
}
_stationCandidateActive = false;
_stationCandidateFromPortal = false;
} else {
const bool changed = _stationProfiles.lastSuccessfulSlot != slot;
_stationProfiles.lastSuccessfulSlot = slot;
if (changed && _stationProfileStore) {
persisted = _stationProfileStore->save(_stationProfiles);
}
}
_stationEverConnected = true;
_stationStatus.state = WM_STATION_CONNECTED;
_stationStatus.activeSlot = slot;
_stationStatus.wifiStatus = WL_CONNECTED;
_stationStatus.lastConnectionWasCandidate = wasCandidate;
_stationStatus.storageSaveFailed = !persisted;
_stationStatus.message = persisted ? F("WiFi connected") : F("WiFi connected but profiles could not be saved");
emitPortalEvent(WM_EVENT_STATION_PROFILE_CONNECTED);
if (wasPortalCandidate) {
completePortalStationAttempt(persisted, persisted ? WL_CONNECTED : WL_CONNECT_FAILED,
_stationStatus.message);
}
}
void WiFiManager::enterStationPortal() {
_stationStatus.state = WM_STATION_PORTAL;
if (_stationStatus.message.length() == 0) {
_stationStatus.message = F("WiFi configuration required");
}
if (configPortalActive || !_enableConfigPortal) {
return;
}
String apName = _stationPortalApName.length() ? _stationPortalApName : getDefaultAPName();
startConfigPortal(apName.c_str(), _stationPortalApPassword.length() ? _stationPortalApPassword.c_str() : NULL);
}
void WiFiManager::handleStationAttemptFailure(uint8_t status, const String& message) {
_stationStatus.wifiStatus = status;
_stationStatus.message = message;
const WiFiManagerStationProfiles& profiles = stationProfilesForAttempt();
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
const uint8_t bit = static_cast<uint8_t>(1U << slot);
if (isStationProfileEnabled(profiles, slot) && !(_stationAttemptMask & bit)) {
queueStationProfile(slot);
return;
}
}
const bool wasCandidate = _stationCandidateActive;
const bool wasPortalCandidate = _stationCandidateFromPortal;
if (wasCandidate) {
_stationCandidateActive = false;
_stationCandidateFromPortal = false;
_stationAttemptMask = 0;
_stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0;
if (wasPortalCandidate) {
_stationStatus.state = WM_STATION_PORTAL;
completePortalStationAttempt(false, status, message);
emitPortalEvent(WM_EVENT_STATION_PROFILE_FAILED);
return;
}
if (validateStationProfiles(_stationProfiles)) {
_stationStatus.configuredProfiles = configuredStationProfileCount(_stationProfiles);
beginStationCycle(true);
return;
}
}
emitPortalEvent(WM_EVENT_STATION_PROFILE_FAILED);
if (_stationEverConnected) {
_stationStatus.state = WM_STATION_BACKOFF;
_stationBackoffStartedAt = millis();
_stationStatus.message = F("WiFi profiles unavailable; retrying shortly");
emitPortalEvent(WM_EVENT_STATION_BACKOFF);
} else {
enterStationPortal();
}
}
void WiFiManager::processStationController() {
if (!isStationProfileMode() && !_stationCandidateActive) {
return;
}
switch (_stationStatus.state) {
case WM_STATION_ATTEMPTING: {
const uint8_t status = WiFi.status();
_stationStatus.wifiStatus = status;
if (hasUsableStationConnection()) {
handleStationConnectionSuccess();
return;
}
if ((millis() - _stationAttemptStartedAt) >= stationAttemptTimeout()) {
handleStationAttemptFailure(WL_CONNECT_FAILED, F("WiFi connection timed out"));
}
return;
}
case WM_STATION_SWITCHING:
if (static_cast<long>(millis() - _stationNextAttemptAt) >= 0) {
const uint8_t nextSlot = _stationPendingSlot;
_stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0;
if (nextSlot == WM_NO_STATION_PROFILE) {
handleStationAttemptFailure(WL_CONNECT_FAILED, F("WiFi profile switch lost its target"));
} else {
beginStationProfile(nextSlot);
}
}
return;
case WM_STATION_CONNECTED:
if (!hasUsableStationConnection()) {
emitPortalEvent(WM_EVENT_STATION_LINK_LOST);
_stationAttemptMask = 0;
_stationPendingSlot = WM_NO_STATION_PROFILE;
_stationNextAttemptAt = 0;
const uint8_t active = isStationProfileEnabled(_stationProfiles, _stationStatus.activeSlot)
? _stationStatus.activeSlot
: chooseStationProfile(_stationProfiles, true);
if (active == WM_NO_STATION_PROFILE) {
enterStationPortal();
} else {
beginStationProfile(active);
}
}
return;
case WM_STATION_BACKOFF:
if ((millis() - _stationBackoffStartedAt) >= _stationRecoveryInterval) {
beginStationCycle(true);
}
return;
default:
return;
}
}
boolean WiFiManager::autoConnect() {
String ssid = getDefaultAPName();
return autoConnect(ssid.c_str(), NULL);
@@ -656,7 +1116,10 @@ boolean WiFiManager::process(){
#endif
processScan();
processPortalConnect();
processStationController();
if (!_stationCandidateFromPortal) {
processPortalConnect();
}
if(webPortalActive || configPortalActive){
// if timed out or abort, break
@@ -813,7 +1276,8 @@ void WiFiManager::processPortalConnect() {
}
return;
}
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL || status == WL_CONNECTION_LOST) {
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL ||
status == WL_CONNECTION_LOST) {
failPortalConnect(status, getWLStatusString(status));
return;
}
+122
View File
@@ -783,6 +783,62 @@ void WiFiManagerHandlers::applyWifiAndParamsFromRequest(AsyncWebServerRequest *r
}
}
bool WiFiManagerHandlers::buildStationProfilesFromRequest(
AsyncWebServerRequest *request, WiFiManagerStationProfiles& profiles) {
profiles = _wm->_stationProfiles;
if (!_wm->validateStationProfiles(profiles)) {
profiles = WiFiManagerStationProfiles();
}
profiles.preferredSlot = 0;
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
WiFiManagerStationProfile& profile = profiles.slots[slot];
const String ssidName = String(F("s")) + String(slot);
const String passwordName = String(F("p")) + String(slot);
const String clearName = String(F("clear")) + String(slot);
if (request->hasParam(ssidName.c_str(), true)) {
const String ssid = request->getParam(ssidName.c_str(), true)->value();
if (ssid.length() >= sizeof(profile.ssid)) {
return false;
}
memset(profile.ssid, 0, sizeof(profile.ssid));
memcpy(profile.ssid, ssid.c_str(), ssid.length());
profile.enabled = ssid.length() > 0;
if (!profile.enabled) {
profile.hasPassword = false;
memset(profile.password, 0, sizeof(profile.password));
if (profiles.lastSuccessfulSlot == slot) {
profiles.lastSuccessfulSlot = WM_NO_STATION_PROFILE;
}
}
}
if (request->hasParam(clearName.c_str(), true)) {
profile.hasPassword = false;
memset(profile.password, 0, sizeof(profile.password));
} else if (request->hasParam(passwordName.c_str(), true)) {
const String password = request->getParam(passwordName.c_str(), true)->value();
if (password.length() >= sizeof(profile.password)) {
return false;
}
// A blank password means "unchanged". Explicit clear is used for an
// open network so a browser never erases a stored secret by accident.
if (password.length() > 0) {
memset(profile.password, 0, sizeof(profile.password));
memcpy(profile.password, password.c_str(), password.length());
profile.hasPassword = true;
}
}
}
if (profiles.lastSuccessfulSlot != WM_NO_STATION_PROFILE &&
!_wm->isStationProfileEnabled(profiles, profiles.lastSuccessfulSlot)) {
profiles.lastSuccessfulSlot = WM_NO_STATION_PROFILE;
}
return _wm->validateStationProfiles(profiles);
}
void WiFiManagerHandlers::doParamSave(WiFiManager::WiFiManagerRequestArgs requestArgs){
if ( _wm->_presaveparamscallback != NULL) {
_wm->_presaveparamscallback();
@@ -1193,6 +1249,48 @@ void WiFiManagerHandlers::handleApiWifiScan(AsyncWebServerRequest *request) {
}
String WiFiManagerHandlers::buildApiWifiMetaJson() {
if (_wm->isStationProfileMode()) {
const WiFiManagerStationProfiles& profiles = _wm->getStationProfiles();
const WiFiManager::wm_station_status_t& station = _wm->getStationStatus();
String json = F("{\"profiles\":[");
for (uint8_t slot = 0; slot < WM_STATION_PROFILE_COUNT; ++slot) {
if (slot > 0) json += ',';
const WiFiManagerStationProfile& profile = profiles.slots[slot];
json += F("{\"slot\":\"");
json += slot == 0 ? F("primary") : F("fallback");
json += F("\",\"configured\":");
json += profile.enabled ? F("true") : F("false");
json += F(",\"ssid\":\"");
jsonAppendEscaped(json, profile.enabled ? String(profile.ssid) : String());
json += F("\",\"passwordSet\":");
json += profile.hasPassword ? F("true") : F("false");
json += F("}");
}
json += F("],\"activeSlot\":");
if (station.activeSlot == WM_NO_STATION_PROFILE) json += F("null");
else json += station.activeSlot == 0 ? F("\"primary\"") : F("\"fallback\"");
json += F(",\"state\":");
switch (station.state) {
case WiFiManager::WM_STATION_ATTEMPTING: json += F("\"connecting\""); break;
case WiFiManager::WM_STATION_CONNECTED: json += F("\"connected\""); break;
case WiFiManager::WM_STATION_BACKOFF: json += F("\"backoff\""); break;
case WiFiManager::WM_STATION_PORTAL: json += F("\"portal\""); break;
default: json += F("\"idle\""); break;
}
json += F(",\"wifiFields\":[],\"staticFields\":[");
bool first = true;
appendPortalJsonStaticFields(json, first);
json += F("],\"params\":[");
first = true;
if (_wm->_portalLayout.paramsOnWifiPage && _wm->getParametersCount() > 0) {
appendPortalJsonCustomParams(json, first);
}
json += F("],\"actions\":{\"canRefreshScan\":true,\"showBack\":");
json += _wm->_portalActions.backVisible ? F("true") : F("false");
json += F("}}");
return json;
}
String ssidPlaceholder = _wm->WiFi_SSID();
String passwordPlaceholder = "";
switch (_wm->_portalPasswordPlaceholderMode) {
@@ -1240,6 +1338,30 @@ void WiFiManagerHandlers::handleApiWifiSave(AsyncWebServerRequest *request) {
#endif
handleRequest(request);
applyWifiAndParamsFromRequest(request);
if (_wm->isStationProfileMode()) {
WiFiManagerStationProfiles candidate;
if (!buildStationProfilesFromRequest(request, candidate)) {
sendApiJson(request, 400, F("{\"ok\":false,\"message\":\"Primary WiFi is required and SSID/password lengths must be valid\"}"));
return;
}
const bool saveForLater = request->hasParam("stationAction", true) &&
request->getParam("stationAction", true)->value() == F("save");
if (saveForLater) {
if (!_wm->saveStationProfiles(candidate)) {
sendApiJson(request, 500, F("{\"ok\":false,\"message\":\"WiFi profiles could not be saved\"}"));
return;
}
sendApiJson(request, 200, F("{\"ok\":true,\"message\":\"WiFi profiles saved for later\"}"));
return;
}
if (!_wm->startStationCandidate(candidate)) {
sendApiJson(request, 400, F("{\"ok\":false,\"message\":\"WiFi profile candidate was rejected\"}"));
return;
}
sendApiJson(request, 202,
F("{\"ok\":true,\"message\":\"WiFi profiles accepted\",\"next\":{\"poll\":\"/api/wifi/connect-status\"}}"));
return;
}
_wm->queuePortalConnect(_wm->_ssid, _wm->_pass);
sendApiJson(
request, 202,