From 9401c20df66d949bd0d7a6b6e58384c7dbbb749a Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Sat, 18 Apr 2026 00:23:57 +1000 Subject: [PATCH] WiFiManager: fragment templates, handler refactor, and tests. Split reusable HTML into Fragments, Message, and Param headers; route more pages through DFTE streaming. Update handlers, WiFi polling JS, and configuration/scanning tests; add template rendering coverage. --- lib/WiFiManager/include/WiFiManager.h | 108 +- lib/WiFiManager/include/WiFiManagerHandlers.h | 6 +- lib/WiFiManager/include/WiFiManagerServer.h | 2 +- lib/WiFiManager/include/templates/Fragments.h | 53 + lib/WiFiManager/include/templates/HTML.h | 77 +- lib/WiFiManager/include/templates/Info.h | 26 +- lib/WiFiManager/include/templates/JS.h | 44 +- lib/WiFiManager/include/templates/Message.h | 15 + lib/WiFiManager/include/templates/PageShell.h | 2 + lib/WiFiManager/include/templates/Param.h | 18 + lib/WiFiManager/include/templates/WiFi.h | 36 +- .../include/templates/WiFiPollingJS.h | 117 +- lib/WiFiManager/src/WiFiManager.cpp | 524 ++++-- lib/WiFiManager/src/WiFiManagerHandlers.cpp | 1494 +++++++++-------- lib/WiFiManager/src/WiFiManagerServer.cpp | 8 +- test/test_wifimanager/test_main.cpp | 14 + test/test_wifimanager/test_main.h | 14 + .../tests/test_configuration.cpp | 23 +- .../tests/test_template_rendering.cpp | 251 +++ .../tests/test_wifi_scanning.cpp | 200 ++- 20 files changed, 1988 insertions(+), 1044 deletions(-) create mode 100644 lib/WiFiManager/include/templates/Fragments.h create mode 100644 lib/WiFiManager/include/templates/Message.h create mode 100644 lib/WiFiManager/include/templates/Param.h create mode 100644 test/test_wifimanager/tests/test_template_rendering.cpp diff --git a/lib/WiFiManager/include/WiFiManager.h b/lib/WiFiManager/include/WiFiManager.h index e543326..734c277 100644 --- a/lib/WiFiManager/include/WiFiManager.h +++ b/lib/WiFiManager/include/WiFiManager.h @@ -29,7 +29,6 @@ // #define WM_ERASE_NVS // esp32 erase(true) will erase NVS // #define WM_RTC // esp32 info page will include reset reasons -// #define WM_JSTEST // build flag for enabling js xhr tests // #define WIFI_MANAGER_OVERRIDE_STRINGS // build flag for using own strings include #ifdef ARDUINO_ESP8266_RELEASE_2_3_0 @@ -203,6 +202,49 @@ class WiFiManager public: // Forward declare nested class - defined later after WM_WebServer is available class WiFiManagerRequestArgs; + + enum wm_scan_state_t : uint8_t { + WM_SCAN_IDLE = 0, + WM_SCAN_QUEUED, + WM_SCAN_RUNNING, + WM_SCAN_COMPLETE, + WM_SCAN_FAILED, + WM_SCAN_TIMEOUT, + }; + + enum wm_scan_schedule_reason_t : uint8_t { + WM_SCAN_SCHEDULE_NONE = 0, + WM_SCAN_SCHEDULE_PRELOAD, + WM_SCAN_SCHEDULE_USER_REFRESH, + WM_SCAN_SCHEDULE_STALE_CACHE, + WM_SCAN_SCHEDULE_UI_RESUME, + }; + + struct WiFiScanNetwork { + String ssid; + int32_t rssi = 0; + uint8_t encType = 0; + }; + + struct WiFiScanRuntimeState { + wm_scan_state_t state = WM_SCAN_IDLE; + bool resultsValid = false; + bool schedulePending = false; + bool forceRefresh = false; + bool completionPending = false; + unsigned long requestedAt = 0; + unsigned long startedAt = 0; + unsigned long finishedAt = 0; + unsigned long timeoutMs = 15000; + unsigned long minRestartIntervalMs = 2000; + uint32_t generation = 0; + uint32_t runningGeneration = 0; + uint32_t completionGeneration = 0; + int lastScanResult = WIFI_SCAN_FAILED; + int completionResult = WIFI_SCAN_FAILED; + int visibleNetworkCount = 0; + wm_scan_schedule_reason_t scheduledReason = WM_SCAN_SCHEDULE_NONE; + }; WiFiManager(Print& consolePort); WiFiManager(); @@ -229,6 +271,15 @@ class WiFiManager // Run webserver processing - must be called periodically when config portal is active boolean process(); + // async scan state and cached result accessors + void requestAsyncScan(bool forceRefresh = false); + const WiFiScanRuntimeState& getScanSnapshot() const { return _scan; } + wm_scan_state_t getScanState() const { return _scan.state; } + bool isScanRunning() const { return _scan.state == WM_SCAN_RUNNING || _scan.state == WM_SCAN_QUEUED; } + bool hasValidScanResults() const { return _scan.resultsValid; } + const WiFiScanRuntimeState& getScanRuntimeState() const { return _scan; } + const std::vector& getScanResults() const { return _scanResultsCache; } + // get the AP name of the config portal, so it can be used in the callback String getConfigPortalSSID(); int getRSSIasQuality(int RSSI); @@ -460,7 +511,7 @@ class WiFiManager } } - // Legacy constructor for backward compatibility (if needed during transition) + // Default constructor for tests and manually assembled argument sets WiFiManagerRequestArgs() {} // Check if argument exists @@ -529,13 +580,12 @@ class WiFiManager uint8_t _lastconxresult = WL_IDLE_STATUS; // store last result when doing connect operations int _numNetworks = 0; // init index for numnetworks wifiscans 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 - + // async scan state management - bool _scanInProgress = false; // flag indicating scan is currently running - bool _scanRequested = false; // flag indicating scan was requested but not yet started - unsigned long _scanRequestTime = 0; // ms when scan was requested + WiFiScanRuntimeState _scan; + std::vector _scanResultsCache; + bool _scanLifecycleBlocked = false; // async reboot/abort scheduling bool _rebootScheduled = false; // flag for scheduled reboot @@ -697,6 +747,19 @@ protected: bool WiFi_scanNetworks(bool force); // Always async - returns false if scan started but not complete bool WiFi_scanNetworks(unsigned int cachetime); void WiFi_scanComplete(int networksFound); + void processScan(); + bool shouldScheduleScan(unsigned int cachetime, + wm_scan_schedule_reason_t reason, + bool forceRefresh = false) const; + void scheduleScan(wm_scan_schedule_reason_t reason, bool forceRefresh = false); + bool startAsyncScan(); + void finalizeAsyncScan(int networksFound); + void failAsyncScan(wm_scan_state_t state, int scanResult = WIFI_SCAN_FAILED); + void resetAsyncScan(bool clearResults); + void invalidateScanResults(); + bool hasFreshScanResults(unsigned int cachetime) const; + bool canRunAsyncScan() const; + void cacheScanResults(int networksFound); bool WiFiSetCountry(); #ifdef ESP32 @@ -734,6 +797,37 @@ protected: #endif #endif + #ifdef UNIT_TEST + public: + void wmTestForceScanState(wm_scan_state_t state) { _scan.state = state; } + void wmTestSetScanStartedAt(unsigned long startedAt) { _scan.startedAt = startedAt; } + void wmTestSetScanTimeoutMs(unsigned long timeoutMs) { _scan.timeoutMs = timeoutMs; } + void wmTestInjectScanResults(const std::vector& results) { + _scanResultsCache = results; + _numNetworks = static_cast(results.size()); + _scan.visibleNetworkCount = _numNetworks; + _scan.resultsValid = true; + _scan.state = WM_SCAN_COMPLETE; + _lastscan = millis(); + _scan.finishedAt = _lastscan; + } + void wmTestSetScanCompletionPending(int completionResult) { + _scan.completionPending = true; + _scan.completionResult = completionResult; + } + void wmTestSetPortalActive(bool active) { configPortalActive = active; } + void wmTestSetConnectPending(bool active) { connect = active; } + void wmTestSetScanLifecycleBlocked(bool blocked) { _scanLifecycleBlocked = blocked; } + void wmTestSetScanGenerations(uint32_t generation, uint32_t runningGeneration, uint32_t completionGeneration) { + _scan.generation = generation; + _scan.runningGeneration = runningGeneration; + _scan.completionGeneration = completionGeneration; + } + void wmTestClearScanResults() { resetAsyncScan(true); } + #endif + + protected: + //helpers (rendering methods moved to WiFiManagerHandlers) boolean isIp(String str); String toStringIp(IPAddress ip); diff --git a/lib/WiFiManager/include/WiFiManagerHandlers.h b/lib/WiFiManager/include/WiFiManagerHandlers.h index 790c3db..901eb25 100644 --- a/lib/WiFiManager/include/WiFiManagerHandlers.h +++ b/lib/WiFiManager/include/WiFiManagerHandlers.h @@ -64,7 +64,7 @@ class WiFiManagerHandlers { void handleErase(AsyncWebServerRequest *request, boolean opt); void handleNotFound(AsyncWebServerRequest *request); void handleRequest(AsyncWebServerRequest *request); - void handleWiFiStatus(AsyncWebServerRequest *request); + void handleWiFiScanRequest(AsyncWebServerRequest *request); void handleWiFiScanStatus(AsyncWebServerRequest *request); void handleUpdate(AsyncWebServerRequest *request); void handleUpdating(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); @@ -75,8 +75,6 @@ class WiFiManagerHandlers { void stopCaptivePortal(); // Rendering Methods - String getHTTPHead(String title, String classes = ""); - String getHTTPEnd(); String getMenuOut(); String getMenuOut(String* outOpt); String getScanItemOut(); @@ -91,6 +89,8 @@ class WiFiManagerHandlers { private: WiFiManager* _wm; + void collectVisibleScanResults(std::vector& networks); + void appendVisibleScanResultsJson(String& json, const std::vector& networks); }; #endif // defined(ESP8266) || defined(ESP32) diff --git a/lib/WiFiManager/include/WiFiManagerServer.h b/lib/WiFiManager/include/WiFiManagerServer.h index 6cba122..0d026ee 100644 --- a/lib/WiFiManager/include/WiFiManagerServer.h +++ b/lib/WiFiManager/include/WiFiManagerServer.h @@ -46,8 +46,8 @@ const char R_restart[] PROGMEM = "/restart"; const char R_exit[] PROGMEM = "/exit"; const char R_close[] PROGMEM = "/close"; const char R_erase[] PROGMEM = "/erase"; -const char R_status[] PROGMEM = "/status"; const char R_scanstatus[] PROGMEM = "/wifistatus"; +const char R_scan[] PROGMEM = "/wifi/scan"; const char R_update[] PROGMEM = "/update"; const char R_updatedone[] PROGMEM = "/u"; diff --git a/lib/WiFiManager/include/templates/Fragments.h b/lib/WiFiManager/include/templates/Fragments.h new file mode 100644 index 0000000..3c9208b --- /dev/null +++ b/lib/WiFiManager/include/templates/Fragments.h @@ -0,0 +1,53 @@ +/** + * Fragments.h + * Reusable DFTE fragment templates for WiFiManager page composition. + */ + +#ifndef _WM_FRAGMENTS_TEMPLATE_H_ +#define _WM_FRAGMENTS_TEMPLATE_H_ + +#include + +const char WM_ACTION_FORM_TEMPLATE[] PROGMEM = +"%ACTION_PREFIX%
%BUTTON_LABEL%%ACTION_SUFFIX%"; + +const char WM_SUBMIT_BUTTON_TEMPLATE[] PROGMEM = +"

"; + +const char WM_CENTERED_BUTTON_TEMPLATE[] PROGMEM = +"
"; + +const char WM_SECTION_BREAK_TEMPLATE[] PROGMEM = +"

%SECTION_CONTENT%"; + +const char WM_SCAN_MESSAGE_TEMPLATE[] PROGMEM = +"%SCAN_MESSAGE%

"; + +const char WM_SCAN_RESULT_ROW_TEMPLATE[] PROGMEM = +"
%SSID_TEXT%" +"" +"
%QUALITY_VALUE%
"; + +const char WM_FIELD_LABEL_BEFORE_TEMPLATE[] PROGMEM = +"
"; + +const char WM_FIELD_LABEL_AFTER_TEMPLATE[] PROGMEM = +"
" +""; + +const char WM_FIELD_INPUT_ONLY_TEMPLATE[] PROGMEM = +"
"; + +const char WM_INFO_ROW_TEMPLATE[] PROGMEM = +"
%INFO_LABEL%
%INFO_VALUE%
"; + +const char WM_INFO_SECTION_TEMPLATE[] PROGMEM = +"%SECTION_PREFIX%

%SECTION_TITLE%


%SECTION_ROWS%
%SECTION_SUFFIX%"; + +const char WM_STATUS_MESSAGE_TEMPLATE[] PROGMEM = +"
%STATUS_TITLE%%STATUS_BODY%
"; + +const char WM_PAGE_HEADING_TEMPLATE[] PROGMEM = +"

%HEADER_TITLE%

%HEADER_SUBTITLE%

"; + +#endif // _WM_FRAGMENTS_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/HTML.h b/lib/WiFiManager/include/templates/HTML.h index 53f4ef0..398b2d7 100644 --- a/lib/WiFiManager/include/templates/HTML.h +++ b/lib/WiFiManager/include/templates/HTML.h @@ -9,39 +9,6 @@ #include -// HTML document structure -const char HTML_HEAD_START[] PROGMEM = -"" -"" -"" -"" -"" -""; - -const char HTML_TITLE_END[] PROGMEM = ""; - -const char HTML_HEAD_END_START[] PROGMEM = "
"; - -const char HTML_END[] PROGMEM = "
"; - -// Form elements -const char HTML_FORM_END[] PROGMEM = "

"; - -const char HTML_FORM_STATIC_HEAD[] PROGMEM = "

"; - -const char HTML_FORM_PARAM_HEAD[] PROGMEM = "

"; - -const char HTML_FORM_WIFI_END[] PROGMEM = ""; - -const char HTML_BR[] PROGMEM = "
"; - -// Buttons -const char HTML_BACKBTN[] PROGMEM = "

"; - -const char HTML_ERASEBTN[] PROGMEM = "
"; - // Status messages const char HTML_STATUS_OFFPW[] PROGMEM = "
Authentication failure"; @@ -62,22 +29,6 @@ const char HTML_UPDATE_FAIL[] PROGMEM = "
Update faile const char HTML_UPDATE_SUCCESS[] PROGMEM = "
Update successful.
Device rebooting now...
"; -// Portal menu -const char * const HTML_PORTAL_MENU[] PROGMEM = { -"

\n", // MENU_WIFI -"

\n", // MENU_WIFINOSCAN -"

\n", // MENU_INFO -"

\n",//MENU_PARAM -"

\n", // MENU_CLOSE -"

\n",// MENU_RESTART -"

\n", // MENU_EXIT -"

\n", // MENU_ERASE -"

\n",// MENU_UPDATE -"

" // MENU_SEP -}; - -const char HTML_PORTAL_OPTIONS[] PROGMEM = ""; - // Help page #ifndef WM_NOHELP const char HTML_HELP[] PROGMEM = @@ -87,15 +38,17 @@ const char HTML_HELP[] PROGMEM = "/" "Menu page." "/wifi" - "Show WiFi scan results and enter WiFi configuration.(/0wifi noscan)" + "Show WiFi scan results and enter WiFi configuration." + "/0wifi" + "Show WiFi configuration without loading scan results until refresh is requested." "/wifisave" "Save WiFi configuration information and configure device. Needs variables supplied." "/param" "Parameter page" "/info" "Information page" - "/u" - "OTA Update" + "/update" + "OTA update page. Firmware upload posts to /u." "/close" "Close the captiveportal popup, config portal will remain active" "/exit" @@ -110,25 +63,5 @@ const char HTML_HELP[] PROGMEM = const char HTML_HELP[] PROGMEM = ""; #endif -#ifdef WM_JSTEST -const char HTML_JS[] PROGMEM = -""; -#endif - #endif // _HTML_TEMPLATES_H_ diff --git a/lib/WiFiManager/include/templates/Info.h b/lib/WiFiManager/include/templates/Info.h index e89c487..66921c5 100644 --- a/lib/WiFiManager/include/templates/Info.h +++ b/lib/WiFiManager/include/templates/Info.h @@ -1,6 +1,6 @@ /** * Info.h - * Streamed info page template for WiFiManager. + * Streamed info page content template for WiFiManager. */ #ifndef _WM_INFO_TEMPLATE_H_ @@ -8,25 +8,11 @@ #include -const char WM_INFO_TEMPLATE[] PROGMEM = -"" -"" -"" -"" -"" -"" -"%DOC_TITLE%" -"%SCRIPTS%" -"%STYLES%" -"" -"" -"
" +const char WM_INFO_CONTENT_TEMPLATE[] PROGMEM = "%INFO_STATUS%" -"%INFO_DEVICE_SECTION%" -"%INFO_WIFI_SECTION%" -"%INFO_ABOUT_SECTION%" -"
" -"" -""; +"
%INFO_DEVICE_SECTION%
" +"
%INFO_WIFI_SECTION%
" +"
%INFO_ABOUT_SECTION%
" +"%INFO_FOOTER%"; #endif // _WM_INFO_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/JS.h b/lib/WiFiManager/include/templates/JS.h index 936e3dc..26e9785 100644 --- a/lib/WiFiManager/include/templates/JS.h +++ b/lib/WiFiManager/include/templates/JS.h @@ -9,16 +9,40 @@ #include -const char JS_SCRIPT[] PROGMEM = ""; // @todo add button states, disable on click , show ack , spinner etc +const char JS_SCRIPT[] PROGMEM = R"rawliteral( + +)rawliteral"; #endif // _JS_TEMPLATES_H_ diff --git a/lib/WiFiManager/include/templates/Message.h b/lib/WiFiManager/include/templates/Message.h new file mode 100644 index 0000000..9284ffa --- /dev/null +++ b/lib/WiFiManager/include/templates/Message.h @@ -0,0 +1,15 @@ +/** + * Message.h + * Streamed message page content template for WiFiManager. + */ + +#ifndef _WM_MESSAGE_TEMPLATE_H_ +#define _WM_MESSAGE_TEMPLATE_H_ + +#include + +const char WM_MESSAGE_CONTENT_TEMPLATE[] PROGMEM = +"%MESSAGE_BODY%" +"%MESSAGE_ACTIONS%"; + +#endif // _WM_MESSAGE_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/PageShell.h b/lib/WiFiManager/include/templates/PageShell.h index f714359..b3f7070 100644 --- a/lib/WiFiManager/include/templates/PageShell.h +++ b/lib/WiFiManager/include/templates/PageShell.h @@ -17,7 +17,9 @@ const char WM_PAGE_SHELL_TEMPLATE[] PROGMEM = "" "%DOC_TITLE%" "%SCRIPTS%" +"%PAGE_SCRIPTS%" "%STYLES%" +"%PAGE_STYLES%" "" "" "
" diff --git a/lib/WiFiManager/include/templates/Param.h b/lib/WiFiManager/include/templates/Param.h new file mode 100644 index 0000000..1dfef89 --- /dev/null +++ b/lib/WiFiManager/include/templates/Param.h @@ -0,0 +1,18 @@ +/** + * Param.h + * Streamed setup page content template for WiFiManager. + */ + +#ifndef _WM_PARAM_TEMPLATE_H_ +#define _WM_PARAM_TEMPLATE_H_ + +#include + +const char WM_PARAM_CONTENT_TEMPLATE[] PROGMEM = +"
" +"%PARAM_FIELDS%" +"%PARAM_FORM_ACTIONS%
" +"%PARAM_PAGE_ACTIONS%" +"%PARAM_STATUS%"; + +#endif // _WM_PARAM_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/WiFi.h b/lib/WiFiManager/include/templates/WiFi.h index d4f2b74..27435f2 100644 --- a/lib/WiFiManager/include/templates/WiFi.h +++ b/lib/WiFiManager/include/templates/WiFi.h @@ -1,6 +1,6 @@ /** * WiFi.h - * Streamed WiFi configuration page template for WiFiManager. + * Streamed WiFi configuration page content template for WiFiManager. */ #ifndef _WM_WIFI_TEMPLATE_H_ @@ -8,25 +8,19 @@ #include -const char WM_WIFI_TEMPLATE[] PROGMEM = -"" -"" -"" -"" -"" -"" -"%DOC_TITLE%" -"%SCRIPTS%" -"%STYLES%" -"" -"" -"
" -"%WIFI_SCAN_RESULTS%" -"%WIFI_FORM_SECTION%" -"%WIFI_BACK_SECTION%" -"%WIFI_STATUS%" -"
" -"" -""; +const char WM_WIFI_CONTENT_TEMPLATE[] PROGMEM = +"
%WIFI_SCAN_CONTENT%
" +"
" +"" +"" +"
" +"" +"" +"
" +"%WIFI_STATIC_FIELDS%" +"%WIFI_PARAM_SECTION%" +"%WIFI_FORM_ACTIONS%
" +"%WIFI_PAGE_ACTIONS%" +"%WIFI_STATUS%"; #endif // _WM_WIFI_TEMPLATE_H_ diff --git a/lib/WiFiManager/include/templates/WiFiPollingJS.h b/lib/WiFiManager/include/templates/WiFiPollingJS.h index f761772..9060c92 100644 --- a/lib/WiFiManager/include/templates/WiFiPollingJS.h +++ b/lib/WiFiManager/include/templates/WiFiPollingJS.h @@ -12,49 +12,81 @@ const char PROGMEM WIFI_POLLING_JS[] = R"rawliteral( diff --git a/lib/WiFiManager/src/WiFiManager.cpp b/lib/WiFiManager/src/WiFiManager.cpp index 63af706..a32e21b 100644 --- a/lib/WiFiManager/src/WiFiManager.cpp +++ b/lib/WiFiManager/src/WiFiManager.cpp @@ -19,6 +19,39 @@ uint8_t WiFiManager::_lastconxresulttmp = WL_IDLE_STATUS; #endif +namespace { +constexpr size_t kMaxHostnameLength = 32; +constexpr uint8_t kSoftApStartMaxAttempts = 3; + +bool isValidHostnameChar(char c) { + return isAlphaNumeric(c) || c == '-'; +} + +bool normalizeHostname(String& hostname) { + hostname.trim(); + + if (hostname.length() > kMaxHostnameLength) { + return false; + } + + if (hostname.length() == 0) { + return true; + } + + if (hostname[0] == '-' || hostname[hostname.length() - 1] == '-') { + return false; + } + + for (size_t i = 0; i < hostname.length(); i++) { + if (!isValidHostnameChar(hostname[i])) { + return false; + } + } + + return true; +} +} // namespace + /** * Add a custom parameter to the config portal * @param p Pointer to WiFiManagerParameter to add @@ -354,24 +387,27 @@ bool WiFiManager::startAP(){ #endif } - // Start soft AP with password or anonymous - if (_apPassword != "") { - if(channel>0){ - ret = WiFi.softAP(_apName.c_str(), _apPassword.c_str(),channel,_apHidden); - } - else{ - ret = WiFi.softAP(_apName.c_str(), _apPassword.c_str(),1,_apHidden);//password option + uint8_t apChannel = channel > 0 ? channel : 1; + auto startSoftAP = [&]() -> bool { + if (_apPassword != "") { + return WiFi.softAP(_apName.c_str(), _apPassword.c_str(), apChannel, _apHidden); } - } else { + #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("AP has anonymous access!")); + DEBUG_WM(WM_DEBUG_VERBOSE,F("AP has anonymous access!")); #endif - if(channel>0){ - ret = WiFi.softAP(_apName.c_str(),"",channel,_apHidden); - } - else{ - ret = WiFi.softAP(_apName.c_str(),"",1,_apHidden); - } + return WiFi.softAP(_apName.c_str(), "", apChannel, _apHidden); + }; + + // Start soft AP with password or anonymous + ret = startSoftAP(); + for (uint8_t attempt = 1; !ret && attempt < kSoftApStartMaxAttempts; attempt++) { + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_ERROR, F("[ERROR] softAP start failed, retry attempt"), attempt + 1); + #endif + WiFi.softAPdisconnect(false); + delay(150 * attempt); + ret = startSoftAP(); } if(_debugLevel >= WM_DEBUG_DEV) debugSoftAPConfig(); @@ -379,7 +415,6 @@ bool WiFiManager::startAP(){ delay(500); // slight delay to make sure we get an AP IP #ifdef WM_DEBUG_LEVEL if(!ret) DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] There was a problem starting the AP")); - // TODO: Implement simple retry/backoff if AP start fails (bounded attempts) DEBUG_WM(F("AP IP address:"),WiFi.softAPIP()); #endif @@ -467,8 +502,8 @@ void WiFiManager::setupConfigPortal() { } _serverManager->createServer(_httpPort); _serverManager->registerRoutes(); - _lastscan = 0; // reset network scan cache - if(_preloadwifiscan) WiFi_scanNetworks(true); // preload wifiscan (async) + resetAsyncScan(true); + if(_preloadwifiscan) scheduleScan(WM_SCAN_SCHEDULE_PRELOAD); // process() starts it later } void WiFiManager::startConfigPortal() { @@ -518,6 +553,7 @@ void WiFiManager::startConfigPortal(char const *apName, char const *apPassword) // init configportal globals to known states configPortalActive = true; connect = abort = false; // loop flags, connect true success, abort true break + _scanLifecycleBlocked = true; _configPortalStart = millis(); @@ -548,7 +584,7 @@ void WiFiManager::startConfigPortal(char const *apName, char const *apPassword) if (_serverManager) { _serverManager->setupDNSD(); } - + _scanLifecycleBlocked = false; #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("Config Portal Running (call process() periodically)")); @@ -579,30 +615,7 @@ boolean WiFiManager::process(){ MDNS.update(); #endif - // Poll WiFi scan status (non-blocking) - // Check if scan is in progress and poll for completion - if(_scanInProgress){ - int8_t scanStatus = WiFi.scanComplete(); - if(scanStatus >= 0){ - // Scan completed - WiFi_scanComplete(scanStatus); - } - else if(scanStatus == WIFI_SCAN_FAILED){ - // Scan failed - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan failed during polling")); - #endif - _scanInProgress = false; - _scanRequested = false; - } - // If WIFI_SCAN_RUNNING, continue polling next time (non-blocking) - } - - // Handle queued scan requests - if(_scanRequested && !_scanInProgress){ - WiFi_scanNetworks(true); - _scanRequested = false; - } + processScan(); if(webPortalActive || configPortalActive){ // if timed out or abort, break @@ -639,6 +652,8 @@ uint8_t WiFiManager::processConfigPortal(){ // Waiting for save... if(connect) { connect = false; + _scanLifecycleBlocked = true; + resetAsyncScan(false); #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("processing save")); #endif @@ -670,8 +685,12 @@ uint8_t WiFiManager::processConfigPortal(){ #endif _savewificallback(); // @CALLBACK } - if(!_connectonsave) return WL_IDLE_STATUS; + if(!_connectonsave) { + _scanLifecycleBlocked = false; + return WL_IDLE_STATUS; + } if(_disableConfigPortal) shutdownConfigPortal(); + _scanLifecycleBlocked = false; return WL_CONNECTED; // CONNECT SUCCESS } #ifdef WM_DEBUG_LEVEL @@ -688,6 +707,7 @@ uint8_t WiFiManager::processConfigPortal(){ _savewificallback(); // @CALLBACK } if(_disableConfigPortal) shutdownConfigPortal(); + _scanLifecycleBlocked = false; return WL_CONNECT_FAILED; // CONNECT FAIL } else{ @@ -696,6 +716,7 @@ uint8_t WiFiManager::processConfigPortal(){ DEBUG_WM(WM_DEBUG_VERBOSE,F("Portal remaining open")); #endif } + _scanLifecycleBlocked = false; } return WL_IDLE_STATUS; @@ -723,10 +744,12 @@ bool WiFiManager::shutdownConfigPortal(){ _serverManager.reset(); } - WiFi.scanDelete(); // free wifi scan results + resetAsyncScan(true); if(!configPortalActive) return false; + _scanLifecycleBlocked = true; + // Turn off AP bool ret = false; ret = WiFi.softAPdisconnect(false); @@ -750,6 +773,7 @@ bool WiFiManager::shutdownConfigPortal(){ configPortalActive = false; DEBUG_WM(WM_DEBUG_VERBOSE,F("configportal closed")); _end(); + _scanLifecycleBlocked = false; return ret; } @@ -994,14 +1018,12 @@ void WiFiManager::startWPS() { #endif void WiFiManager::WiFi_scanComplete(int networksFound){ - _lastscan = millis(); - _numNetworks = networksFound; - _scanInProgress = false; - _scanRequested = false; - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC completed"), "in "+(String)(_lastscan - _startscan)+" ms"); - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC found:"),_numNetworks); - #endif + if (_scan.state != WM_SCAN_RUNNING) { + return; + } + _scan.completionPending = true; + _scan.completionResult = networksFound; + _scan.completionGeneration = _scan.runningGeneration; } bool WiFiManager::WiFi_scanNetworks(){ @@ -1009,85 +1031,321 @@ bool WiFiManager::WiFi_scanNetworks(){ } bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime){ - return WiFi_scanNetworks(millis()-_lastscan > cachetime); + if (hasFreshScanResults(cachetime)) { + return true; + } + scheduleScan(WM_SCAN_SCHEDULE_STALE_CACHE); + return false; } bool WiFiManager::WiFi_scanNetworks(bool force){ - // If 0 networks found, force rescan if autoforcerescan is enabled if(_numNetworks == 0 && _autoforcerescan){ DEBUG_WM(WM_DEBUG_DEV,"NO APs found forcing new scan"); force = true; } - // if scan is empty or stale (last scantime > _scancachetime), this avoids fast reloading wifi page and constant scan delayed page loads appearing to freeze. - if(!_lastscan || (_lastscan>0 && (millis()-_lastscan > _scancachetime))){ - force = true; - } - - // If scan already in progress, don't start another one - if(_scanInProgress){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan already in progress")); - #endif - return false; // Scan in progress, not complete yet - } - - if(force){ - _startscan = millis(); - _scanRequestTime = millis(); - - #ifdef ESP8266 - #ifndef WM_NOASYNC // no async available < 2.4.0 - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started")); - #endif - using namespace std::placeholders; // for `_1` - WiFi.scanNetworksAsync(std::bind(&WiFiManager::WiFi_scanComplete,this,_1)); - _scanInProgress = true; - return false; // Scan started, not complete yet - #else - // ESP8266 < 2.4.0 - no async available, but we can't block here - // Return false and let process() handle it - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] Async scan not available on this ESP8266 core")); - #endif - return false; - #endif - #else // ESP32 - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started")); - #endif - int8_t res = WiFi.scanNetworks(true); - if(res == WIFI_SCAN_RUNNING){ - _scanInProgress = true; - return false; // Scan started, not complete yet - } - else if(res == WIFI_SCAN_FAILED){ - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan failed")); - #endif - _scanInProgress = false; - return false; - } - else if(res >= 0){ - // Scan completed immediately (unlikely but possible) - _numNetworks = res; - _lastscan = millis(); - _scanInProgress = false; - #ifdef WM_DEBUG_LEVEL - DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan completed immediately"), "in "+(String)(_lastscan - _startscan)+" ms"); - #endif - return true; - } - return false; - #endif - } - else { + if(!force && hasFreshScanResults(_scancachetime)){ #ifdef WM_DEBUG_LEVEL DEBUG_WM(WM_DEBUG_VERBOSE,F("Scan is cached"),(String)(millis()-_lastscan )+" ms ago"); #endif - return true; // Using cached data + return true; } + + if (force) { + scheduleScan(WM_SCAN_SCHEDULE_USER_REFRESH, true); + } else { + scheduleScan(WM_SCAN_SCHEDULE_STALE_CACHE); + } + return false; +} + +void WiFiManager::requestAsyncScan(bool forceRefresh) { + scheduleScan(WM_SCAN_SCHEDULE_USER_REFRESH, forceRefresh); +} + +bool WiFiManager::shouldScheduleScan(unsigned int cachetime, + wm_scan_schedule_reason_t reason, + bool forceRefresh) const { + if (forceRefresh) { + return true; + } + + switch (reason) { + case WM_SCAN_SCHEDULE_USER_REFRESH: + return true; + + case WM_SCAN_SCHEDULE_PRELOAD: + case WM_SCAN_SCHEDULE_STALE_CACHE: + case WM_SCAN_SCHEDULE_UI_RESUME: + return !hasFreshScanResults(cachetime); + + case WM_SCAN_SCHEDULE_NONE: + default: + return false; + } +} + +void WiFiManager::scheduleScan(wm_scan_schedule_reason_t reason, bool forceRefresh) { + if (_numNetworks == 0 && _autoforcerescan) { + forceRefresh = true; + } + + if (!shouldScheduleScan(_scancachetime, reason, forceRefresh)) { + return; + } + + _scan.schedulePending = true; + _scan.forceRefresh = _scan.forceRefresh || forceRefresh; + _scan.requestedAt = millis(); + _scan.scheduledReason = reason; + + if (_scan.state != WM_SCAN_RUNNING && _scan.state != WM_SCAN_QUEUED) { + _scan.state = WM_SCAN_QUEUED; + _scan.lastScanResult = WIFI_SCAN_RUNNING; + } +} + +void WiFiManager::processScan() { + const unsigned long now = millis(); + + if (_scan.completionPending) { + const int completionResult = _scan.completionResult; + const uint32_t completionGeneration = _scan.completionGeneration; + _scan.completionPending = false; + if (completionGeneration == _scan.runningGeneration) { + if (completionResult >= 0) { + finalizeAsyncScan(completionResult); + } else { + failAsyncScan(completionResult == WIFI_SCAN_FAILED ? WM_SCAN_FAILED : WM_SCAN_TIMEOUT, completionResult); + } + } else { + _scan.completionGeneration = 0; + } + } + + if ((_scan.state == WM_SCAN_RUNNING || _scan.state == WM_SCAN_QUEUED) && !canRunAsyncScan()) { + resetAsyncScan(false); + return; + } + + switch (_scan.state) { + case WM_SCAN_IDLE: + case WM_SCAN_COMPLETE: + case WM_SCAN_FAILED: + case WM_SCAN_TIMEOUT: + if (_scan.schedulePending) { + _scan.state = WM_SCAN_QUEUED; + } + break; + + case WM_SCAN_QUEUED: + startAsyncScan(); + break; + + case WM_SCAN_RUNNING: + if ((now - _scan.startedAt) > _scan.timeoutMs) { + failAsyncScan(WM_SCAN_TIMEOUT); + return; + } + + #ifdef ESP32 + if (!_scan.completionPending) { + int scanStatus = WiFi.scanComplete(); + if (scanStatus >= 0) { + finalizeAsyncScan(scanStatus); + } else if (scanStatus == WIFI_SCAN_FAILED) { + failAsyncScan(WM_SCAN_FAILED, scanStatus); + } + } + #endif + break; + } +} + +bool WiFiManager::startAsyncScan() { + const unsigned long now = millis(); + + if (!canRunAsyncScan()) { + return false; + } + + if (_scan.finishedAt > 0 && (now - _scan.finishedAt) < _scan.minRestartIntervalMs) { + return false; + } + + if (_scan.forceRefresh) { + invalidateScanResults(); + } + + _scan.completionPending = false; + _scan.completionResult = WIFI_SCAN_FAILED; + _scan.generation++; + _scan.runningGeneration = _scan.generation; + _scan.completionGeneration = 0; + + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC started")); + #endif + + #ifdef ESP8266 + #ifndef WM_NOASYNC + using namespace std::placeholders; + WiFi.scanDelete(); + WiFi.scanNetworksAsync(std::bind(&WiFiManager::WiFi_scanComplete, this, _1)); + _scan.state = WM_SCAN_RUNNING; + _scan.startedAt = now; + _scan.lastScanResult = WIFI_SCAN_RUNNING; + _scan.schedulePending = false; + _scan.forceRefresh = false; + return true; + #else + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] Async scan not available on this ESP8266 core")); + #endif + failAsyncScan(WM_SCAN_FAILED); + return false; + #endif + #else + WiFi.scanDelete(); + int scanResult = WiFi.scanNetworks(true); + if (scanResult == WIFI_SCAN_RUNNING) { + _scan.state = WM_SCAN_RUNNING; + _scan.startedAt = now; + _scan.lastScanResult = WIFI_SCAN_RUNNING; + _scan.schedulePending = false; + _scan.forceRefresh = false; + return true; + } + + if (scanResult >= 0) { + finalizeAsyncScan(scanResult); + return true; + } + + failAsyncScan(WM_SCAN_FAILED, scanResult); + return false; + #endif +} + +void WiFiManager::cacheScanResults(int networksFound) { + _scanResultsCache.clear(); + if (networksFound <= 0) { + return; + } + + _scanResultsCache.reserve(static_cast(networksFound)); + for (int i = 0; i < networksFound; i++) { + WiFiScanNetwork network; + network.ssid = WiFi.SSID(i); + network.rssi = WiFi.RSSI(i); + network.encType = WiFi.encryptionType(i); + _scanResultsCache.push_back(network); + } +} + +void WiFiManager::finalizeAsyncScan(int networksFound) { + if (_scan.completionGeneration != 0 && _scan.completionGeneration != _scan.runningGeneration) { + return; + } + + _lastscan = millis(); + _scan.finishedAt = _lastscan; + _scan.lastScanResult = networksFound; + _scan.state = WM_SCAN_COMPLETE; + _scan.resultsValid = networksFound >= 0; + _scan.schedulePending = false; + _scan.forceRefresh = false; + + cacheScanResults(networksFound); + _numNetworks = static_cast(_scanResultsCache.size()); + _scan.visibleNetworkCount = _numNetworks; + _scan.completionGeneration = 0; + _scan.runningGeneration = 0; + WiFi.scanDelete(); + + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC completed"), "in "+(String)(_scan.finishedAt - _scan.startedAt)+" ms"); + DEBUG_WM(WM_DEBUG_VERBOSE,F("WiFi Scan ASYNC found:"),_numNetworks); + #endif +} + +void WiFiManager::failAsyncScan(wm_scan_state_t state, int scanResult) { + WiFi.scanDelete(); + _scan.finishedAt = millis(); + _scan.lastScanResult = scanResult; + _scan.state = state; + _scan.resultsValid = false; + _scan.completionPending = false; + _scan.schedulePending = false; + _scan.forceRefresh = false; + _scan.visibleNetworkCount = 0; + _numNetworks = 0; + _lastscan = 0; + _scan.completionGeneration = 0; + _scan.runningGeneration = 0; + _scanResultsCache.clear(); + + #ifdef WM_DEBUG_LEVEL + if (state == WM_SCAN_TIMEOUT) { + DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan timed out")); + } else { + DEBUG_WM(WM_DEBUG_ERROR,F("[ERROR] scan failed")); + } + #endif +} + +void WiFiManager::resetAsyncScan(bool clearResults) { + WiFi.scanDelete(); + _scan.generation++; + _scan.state = WM_SCAN_IDLE; + _scan.resultsValid = clearResults ? false : _scan.resultsValid; + _scan.schedulePending = false; + _scan.forceRefresh = false; + _scan.completionPending = false; + _scan.requestedAt = 0; + _scan.startedAt = 0; + _scan.finishedAt = 0; + _scan.lastScanResult = WIFI_SCAN_FAILED; + _scan.completionResult = WIFI_SCAN_FAILED; + _scan.runningGeneration = 0; + _scan.completionGeneration = 0; + _scan.scheduledReason = WM_SCAN_SCHEDULE_NONE; + if (clearResults) { + _numNetworks = 0; + _lastscan = 0; + _scan.visibleNetworkCount = 0; + _scanResultsCache.clear(); + } else { + _scan.visibleNetworkCount = _numNetworks; + } +} + +void WiFiManager::invalidateScanResults() { + _scan.resultsValid = false; + _scan.visibleNetworkCount = 0; + _numNetworks = 0; + _lastscan = 0; + _scanResultsCache.clear(); +} + +bool WiFiManager::hasFreshScanResults(unsigned int cachetime) const { + if (!_scan.resultsValid || _scanResultsCache.empty() || _lastscan == 0) { + return false; + } + + return cachetime == 0 || (millis() - _lastscan) <= cachetime; +} + +bool WiFiManager::canRunAsyncScan() const { + if (!configPortalActive && !webPortalActive) { + return false; + } + + if (_scanLifecycleBlocked || connect || _abortScheduled || _rebootScheduled || abort) { + return false; + } + + return true; } // PUBLIC @@ -1606,13 +1864,33 @@ void WiFiManager::setDisableConfigPortal(boolean enable) * @return bool false if hostname is not valid */ bool WiFiManager::setHostname(const char * hostname){ - // TODO: Enforce max 32-char hostname and reject/trim invalid input - _hostname = String(hostname); + if (hostname == nullptr) { + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_ERROR, F("[ERROR] hostname: null value rejected")); + #endif + return false; + } + + String candidate(hostname); + if (!normalizeHostname(candidate)) { + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_ERROR, F("[ERROR] hostname: invalid value rejected")); + #endif + return false; + } + + _hostname = candidate; return true; } bool WiFiManager::setHostname(String hostname){ - // TODO: Enforce max 32-char hostname and reject/trim invalid input + if (!normalizeHostname(hostname)) { + #ifdef WM_DEBUG_LEVEL + DEBUG_WM(WM_DEBUG_ERROR, F("[ERROR] hostname: invalid value rejected")); + #endif + return false; + } + _hostname = hostname; return true; } diff --git a/lib/WiFiManager/src/WiFiManagerHandlers.cpp b/lib/WiFiManager/src/WiFiManagerHandlers.cpp index 9eec362..f709c12 100644 --- a/lib/WiFiManager/src/WiFiManagerHandlers.cpp +++ b/lib/WiFiManager/src/WiFiManagerHandlers.cpp @@ -13,8 +13,12 @@ #include "templates/HTML.h" #include "templates/CSS.h" #include "templates/JS.h" +#include "templates/WiFiPollingJS.h" #include "templates/PageShell.h" +#include "templates/Fragments.h" #include "templates/Info.h" +#include "templates/Message.h" +#include "templates/Param.h" #include "templates/WiFi.h" #include "templates/RootSelector.h" #include @@ -26,12 +30,10 @@ #define WM_PAGE_RESERVE_BYTES 8192 #endif -#ifndef WM_ROOT_REQUEST_STATE -#define WM_ROOT_REQUEST_STATE 1 -#endif - namespace { +const char kEmptyTemplateChunk[] PROGMEM = ""; + inline void reservePage(String& page, size_t extraBytes = WM_PAGE_RESERVE_BYTES) { if (extraBytes == 0) return; const size_t targetLen = page.length() + extraBytes; @@ -78,6 +80,21 @@ void registerSharedShellPlaceholders(WiFiManagerServer* server, PlaceholderRegis registry.registerProgmemData("%SCRIPTS%", JS_SCRIPT); } +void registerShellTemplate(PlaceholderRegistry& registry, + DynamicTemplateDescriptor& docTitleDescriptor, + String& docTitle, + const char* bodyClass, + const char* pageContentTemplate, + const char* pageScripts = kEmptyTemplateChunk, + const char* pageStyles = kEmptyTemplateChunk) { + configureDynamicStringDescriptor(docTitleDescriptor, docTitle); + registry.registerDynamicTemplate("%DOC_TITLE%", &docTitleDescriptor); + registry.registerProgmemData("%BODY_CLASS%", bodyClass); + registry.registerProgmemData("%PAGE_SCRIPTS%", pageScripts); + registry.registerProgmemData("%PAGE_STYLES%", pageStyles); + registry.registerProgmemTemplate("%PAGE_CONTENT%", pageContentTemplate); +} + struct RootState { String menu; String status; @@ -85,8 +102,11 @@ struct RootState { struct MessagePageState { String docTitle; - String bodyClass; - String content; + const char* bodyClass; + String messageBody; + String actions; + + MessagePageState() : bodyClass(C_root) {} }; struct InfoPageState { @@ -95,13 +115,26 @@ struct InfoPageState { String deviceSection; String wifiSection; String aboutSection; + String footer; }; struct WiFiPageState { String docTitle; - String scanResults; - String formSection; - String backSection; + String scanContent; + String ssidPlaceholder; + String passwordPlaceholder; + String staticFields; + String paramSection; + String formActions; + String pageActions; + String status; +}; + +struct ParamPageState { + String docTitle; + String fields; + String formActions; + String pageActions; String status; }; @@ -120,6 +153,241 @@ void escapePercentsForTemplate(String& value) { value = escaped; } +void registerDynamicStringPlaceholder(PlaceholderRegistry& registry, + const char* placeholder, + String& value, + DynamicTemplateDescriptor& descriptor) { + escapePercentsForTemplate(value); + configureDynamicStringDescriptor(descriptor, value); + registry.registerDynamicTemplate(placeholder, &descriptor); +} + +String renderTemplateToString(const char* templateData, PlaceholderRegistry& registry) { + TemplateContext context; + context.setRegistry(®istry); + TemplateRenderer::initializeContext(context, templateData); + + uint8_t buffer[128]; + String output; + while (true) { + size_t written = TemplateRenderer::renderNextChunk(context, buffer, sizeof(buffer)); + if (written == 0) { + break; + } + output.concat(reinterpret_cast(buffer), written); + } + return output; +} + +String renderActionForm(const String& action, + const String& method, + const String& label, + const String& buttonClassAttr = String(), + const String& prefix = String(), + const String& suffix = String()) { + PlaceholderRegistry registry(6); + DynamicTemplateDescriptor actionDescriptor; + DynamicTemplateDescriptor methodDescriptor; + DynamicTemplateDescriptor labelDescriptor; + DynamicTemplateDescriptor classDescriptor; + DynamicTemplateDescriptor prefixDescriptor; + DynamicTemplateDescriptor suffixDescriptor; + + String actionValue = action; + String methodValue = method; + String labelValue = label; + String classValue = buttonClassAttr; + String prefixValue = prefix; + String suffixValue = suffix; + + registerDynamicStringPlaceholder(registry, "%ACTION%", actionValue, actionDescriptor); + registerDynamicStringPlaceholder(registry, "%METHOD%", methodValue, methodDescriptor); + registerDynamicStringPlaceholder(registry, "%BUTTON_LABEL%", labelValue, labelDescriptor); + registerDynamicStringPlaceholder(registry, "%BUTTON_CLASS_ATTR%", classValue, classDescriptor); + registerDynamicStringPlaceholder(registry, "%ACTION_PREFIX%", prefixValue, prefixDescriptor); + registerDynamicStringPlaceholder(registry, "%ACTION_SUFFIX%", suffixValue, suffixDescriptor); + + return renderTemplateToString(WM_ACTION_FORM_TEMPLATE, registry); +} + +String renderSubmitButton(const String& label) { + PlaceholderRegistry registry(1); + DynamicTemplateDescriptor labelDescriptor; + String labelValue = label; + registerDynamicStringPlaceholder(registry, "%BUTTON_LABEL%", labelValue, labelDescriptor); + return renderTemplateToString(WM_SUBMIT_BUTTON_TEMPLATE, registry); +} + +String renderCenteredButton(const String& id, + const String& type, + const String& onClick, + const String& label, + const String& extraAttrs = String()) { + PlaceholderRegistry registry(5); + DynamicTemplateDescriptor idDescriptor; + DynamicTemplateDescriptor typeDescriptor; + DynamicTemplateDescriptor onClickDescriptor; + DynamicTemplateDescriptor labelDescriptor; + DynamicTemplateDescriptor extraAttrsDescriptor; + + String idValue = id; + String typeValue = type; + String onClickValue = onClick; + String labelValue = label; + String extraAttrsValue = extraAttrs; + + registerDynamicStringPlaceholder(registry, "%BUTTON_ID%", idValue, idDescriptor); + registerDynamicStringPlaceholder(registry, "%BUTTON_TYPE%", typeValue, typeDescriptor); + registerDynamicStringPlaceholder(registry, "%BUTTON_ONCLICK%", onClickValue, onClickDescriptor); + registerDynamicStringPlaceholder(registry, "%BUTTON_LABEL%", labelValue, labelDescriptor); + registerDynamicStringPlaceholder(registry, "%BUTTON_EXTRA_ATTRS%", extraAttrsValue, extraAttrsDescriptor); + + return renderTemplateToString(WM_CENTERED_BUTTON_TEMPLATE, registry); +} + +String renderSectionBreak(const String& content) { + if (content.length() == 0) { + return String(); + } + + PlaceholderRegistry registry(1); + DynamicTemplateDescriptor contentDescriptor; + String contentValue = content; + registerDynamicStringPlaceholder(registry, "%SECTION_CONTENT%", contentValue, contentDescriptor); + return renderTemplateToString(WM_SECTION_BREAK_TEMPLATE, registry); +} + +String renderScanMessage(const String& message) { + PlaceholderRegistry registry(1); + DynamicTemplateDescriptor messageDescriptor; + String messageValue = message; + registerDynamicStringPlaceholder(registry, "%SCAN_MESSAGE%", messageValue, messageDescriptor); + return renderTemplateToString(WM_SCAN_MESSAGE_TEMPLATE, registry); +} + +String renderScanRow(const String& ssidAttr, + const String& ssidText, + const String& qualityLabel, + const String& qualityIcon, + const String& lockClass, + const String& iconVisibilityClass, + const String& valueVisibilityClass, + const String& qualityValue) { + PlaceholderRegistry registry(8); + DynamicTemplateDescriptor ssidAttrDescriptor; + DynamicTemplateDescriptor ssidTextDescriptor; + DynamicTemplateDescriptor qualityLabelDescriptor; + DynamicTemplateDescriptor qualityIconDescriptor; + DynamicTemplateDescriptor lockClassDescriptor; + DynamicTemplateDescriptor iconVisibilityDescriptor; + DynamicTemplateDescriptor valueVisibilityDescriptor; + DynamicTemplateDescriptor qualityValueDescriptor; + + String ssidAttrValue = ssidAttr; + String ssidTextValue = ssidText; + String qualityLabelValue = qualityLabel; + String qualityIconValue = qualityIcon; + String lockClassValue = lockClass; + String iconVisibilityValue = iconVisibilityClass; + String valueVisibilityValue = valueVisibilityClass; + String qualityValueValue = qualityValue; + + registerDynamicStringPlaceholder(registry, "%SSID_ATTR%", ssidAttrValue, ssidAttrDescriptor); + registerDynamicStringPlaceholder(registry, "%SSID_TEXT%", ssidTextValue, ssidTextDescriptor); + registerDynamicStringPlaceholder(registry, "%QUALITY_LABEL%", qualityLabelValue, qualityLabelDescriptor); + registerDynamicStringPlaceholder(registry, "%QUALITY_ICON%", qualityIconValue, qualityIconDescriptor); + registerDynamicStringPlaceholder(registry, "%LOCK_CLASS%", lockClassValue, lockClassDescriptor); + registerDynamicStringPlaceholder(registry, "%ICON_VISIBILITY_CLASS%", iconVisibilityValue, iconVisibilityDescriptor); + registerDynamicStringPlaceholder(registry, "%VALUE_VISIBILITY_CLASS%", valueVisibilityValue, valueVisibilityDescriptor); + registerDynamicStringPlaceholder(registry, "%QUALITY_VALUE%", qualityValueValue, qualityValueDescriptor); + + return renderTemplateToString(WM_SCAN_RESULT_ROW_TEMPLATE, registry); +} + +String renderFieldTemplate(const char* templateData, + const String& id, + const String& label, + const String& maxLength, + const String& value, + const String& extraAttrs = String()) { + PlaceholderRegistry registry(6); + DynamicTemplateDescriptor idDescriptor; + DynamicTemplateDescriptor nameDescriptor; + DynamicTemplateDescriptor labelDescriptor; + DynamicTemplateDescriptor maxLengthDescriptor; + DynamicTemplateDescriptor valueDescriptor; + DynamicTemplateDescriptor extraAttrsDescriptor; + + String idValue = id; + String nameValue = id; + String labelValue = label; + String maxLengthValue = maxLength; + String valueValue = value; + String extraAttrsValue = extraAttrs; + + registerDynamicStringPlaceholder(registry, "%FIELD_ID%", idValue, idDescriptor); + registerDynamicStringPlaceholder(registry, "%FIELD_NAME%", nameValue, nameDescriptor); + registerDynamicStringPlaceholder(registry, "%FIELD_LABEL%", labelValue, labelDescriptor); + registerDynamicStringPlaceholder(registry, "%FIELD_MAXLENGTH%", maxLengthValue, maxLengthDescriptor); + registerDynamicStringPlaceholder(registry, "%FIELD_VALUE%", valueValue, valueDescriptor); + registerDynamicStringPlaceholder(registry, "%FIELD_EXTRA_ATTRS%", extraAttrsValue, extraAttrsDescriptor); + + return renderTemplateToString(templateData, registry); +} + +String renderInfoRow(const String& label, const String& value) { + PlaceholderRegistry registry(2); + DynamicTemplateDescriptor labelDescriptor; + DynamicTemplateDescriptor valueDescriptor; + String labelValue = label; + String valueValue = value; + registerDynamicStringPlaceholder(registry, "%INFO_LABEL%", labelValue, labelDescriptor); + registerDynamicStringPlaceholder(registry, "%INFO_VALUE%", valueValue, valueDescriptor); + return renderTemplateToString(WM_INFO_ROW_TEMPLATE, registry); +} + +String renderInfoSection(const String& title, + const String& rows, + const String& prefix = String(), + const String& suffix = String()) { + PlaceholderRegistry registry(4); + DynamicTemplateDescriptor titleDescriptor; + DynamicTemplateDescriptor rowsDescriptor; + DynamicTemplateDescriptor prefixDescriptor; + DynamicTemplateDescriptor suffixDescriptor; + + String titleValue = title; + String rowsValue = rows; + String prefixValue = prefix; + String suffixValue = suffix; + + registerDynamicStringPlaceholder(registry, "%SECTION_TITLE%", titleValue, titleDescriptor); + registerDynamicStringPlaceholder(registry, "%SECTION_ROWS%", rowsValue, rowsDescriptor); + registerDynamicStringPlaceholder(registry, "%SECTION_PREFIX%", prefixValue, prefixDescriptor); + registerDynamicStringPlaceholder(registry, "%SECTION_SUFFIX%", suffixValue, suffixDescriptor); + + return renderTemplateToString(WM_INFO_SECTION_TEMPLATE, registry); +} + +String renderStatusMessage(const String& title, + const String& body, + const String& statusClassSuffix = String()) { + PlaceholderRegistry registry(3); + DynamicTemplateDescriptor titleDescriptor; + DynamicTemplateDescriptor bodyDescriptor; + DynamicTemplateDescriptor classDescriptor; + + String titleValue = title; + String bodyValue = body; + String classValue = statusClassSuffix; + + registerDynamicStringPlaceholder(registry, "%STATUS_TITLE%", titleValue, titleDescriptor); + registerDynamicStringPlaceholder(registry, "%STATUS_BODY%", bodyValue, bodyDescriptor); + registerDynamicStringPlaceholder(registry, "%STATUS_CLASS_SUFFIX%", classValue, classDescriptor); + + return renderTemplateToString(WM_STATUS_MESSAGE_TEMPLATE, registry); +} + void buildRootState(WiFiManagerHandlers* handlers, RootState& state) { reservePage(state.menu, 768); reservePage(state.status, 512); @@ -132,24 +400,55 @@ void buildRootState(WiFiManagerHandlers* handlers, RootState& state) { escapePercentsForTemplate(state.status); } -void appendPortalHeader(const String& title, - const String& subtitle, - String& content) { - content += F("

"); - content += title; - content += F("

"); - content += subtitle; - content += F("

"); +String renderPageHeading(const String& title, const String& subtitle) { + PlaceholderRegistry registry(2); + DynamicTemplateDescriptor titleDescriptor; + DynamicTemplateDescriptor subtitleDescriptor; + String titleValue = title; + String subtitleValue = subtitle; + registerDynamicStringPlaceholder(registry, "%HEADER_TITLE%", titleValue, titleDescriptor); + registerDynamicStringPlaceholder(registry, "%HEADER_SUBTITLE%", subtitleValue, subtitleDescriptor); + return renderTemplateToString(WM_PAGE_HEADING_TEMPLATE, registry); } void buildMessagePageState(MessagePageState& state, - const __FlashStringHelper* title, - const __FlashStringHelper* bodyClass, - const String& contentHtml) { - state.docTitle = String(title); - state.bodyClass = String(bodyClass); - state.content = contentHtml; - escapePercentsForTemplate(state.content); + const String& title, + const char* bodyClass, + const String& messageBodyHtml, + const String& actionsHtml) { + state.docTitle = title; + state.bodyClass = bodyClass; + state.messageBody = messageBodyHtml; + state.actions = actionsHtml; + escapePercentsForTemplate(state.messageBody); + escapePercentsForTemplate(state.actions); +} + +String buildUpdatePanelContent(const String& title, const String& subtitle) { + String content; + reservePage(content, 2048); + content += renderPageHeading(title, subtitle); + content += FPSTR(HTML_UPDATE); + return content; +} + +String buildUpdateResultContent(const String& title, const String& subtitle) { + String content; + reservePage(content, 2048); + content += renderPageHeading(title, subtitle); + + if (Update.hasError()) { + content += FPSTR(HTML_UPDATE_FAIL); + #ifdef ESP32 + content += "OTA Error: " + String(Update.errorString()); + #else + content += "OTA Error: " + String(Update.getError()); + #endif + } else { + content += FPSTR(HTML_UPDATE_SUCCESS); + } + + return content; } void appendInfoEntries(WiFiManagerHandlers* handlers, @@ -170,58 +469,65 @@ void buildInfoPageState(WiFiManagerHandlers* handlers, reservePage(state.deviceSection, 4096); reservePage(state.wifiSection, 4096); reservePage(state.aboutSection, 1536); + reservePage(state.footer, 1024); state.docTitle = F("Info"); handlers->reportStatus(state.status); #ifdef ESP8266 static const char* const deviceIds[] = { - "esphead", "uptime", "chipid", "fchipid", "idesize", "flashsize", - "corever", "bootver", "cpufreq", "freeheap", "memsketch", "memsmeter", - "lastreset" + "uptime", "chipid", "fchipid", "idesize", "flashsize", "corever", + "bootver", "cpufreq", "freeheap", "memsketch", "memsmeter", "lastreset" }; static const char* const wifiIds[] = { - "wifihead", "conx", "stassid", "staip", "stagw", "stasub", "dnss", - "host", "stamac", "autoconx", "wifiaphead", "apssid", "apip", "apbssid", - "apmac" + "conx", "stassid", "staip", "stagw", "stasub", "dnss", "host", + "stamac", "autoconx", "apssid", "apip", "apbssid", "apmac" }; #elif defined(ESP32) static const char* const deviceIds[] = { - "esphead", "uptime", "chipid", "chiprev", "idesize", "flashsize", + "uptime", "chipid", "chiprev", "idesize", "flashsize", "cpufreq", "freeheap", "memsketch", "memsmeter", "lastreset", "temp" }; static const char* const wifiIds[] = { - "wifihead", "conx", "stassid", "staip", "stagw", "stasub", "dnss", - "host", "stamac", "apssid", "wifiaphead", "apip", "apmac", "aphost", - "apbssid" + "conx", "stassid", "staip", "stagw", "stasub", "dnss", "host", + "stamac", "apssid", "apip", "apmac", "aphost", "apbssid" }; #endif appendInfoEntries(handlers, state.deviceSection, deviceIds, sizeof(deviceIds) / sizeof(deviceIds[0])); - state.deviceSection += F(""); - appendInfoEntries(handlers, state.wifiSection, wifiIds, sizeof(wifiIds) / sizeof(wifiIds[0])); - state.wifiSection += F(""); - - state.aboutSection += F("

About


"); state.aboutSection += handlers->getInfoData("aboutver"); state.aboutSection += handlers->getInfoData("aboutarduinover"); - state.aboutSection += handlers->getInfoData("aboutidfver"); + state.aboutSection += handlers->getInfoData("aboutsdkver"); state.aboutSection += handlers->getInfoData("aboutdate"); - state.aboutSection += F("
"); if (showInfoUpdate) { - state.aboutSection += HTML_PORTAL_MENU[8]; - state.aboutSection += HTML_PORTAL_MENU[9]; + state.footer += renderActionForm(F("/update"), F("get"), F("Update"), String(), F("

"), F("
\n")); } - if (showInfoErase) state.aboutSection += FPSTR(HTML_ERASEBTN); - if (showBack) state.aboutSection += FPSTR(HTML_BACKBTN); - state.aboutSection += FPSTR(HTML_HELP); + if (showInfoErase) { + state.footer += renderActionForm(F("/erase"), F("get"), F("Erase WiFi config"), F(" class='D'"), showInfoUpdate ? String() : String(F("

")), F("
\n")); + } + if (showBack) { + state.footer += renderActionForm(F("/"), F("get"), F("Back"), String(), (showInfoUpdate || showInfoErase) ? String() : String(F("

"))); + } + state.footer += FPSTR(HTML_HELP); + + state.deviceSection = renderInfoSection( +#ifdef ESP32 + F("ESP32"), +#else + F("ESP8266"), +#endif + state.deviceSection + ); + state.wifiSection = renderInfoSection(F("WiFi"), state.wifiSection, F("
")); + state.aboutSection = renderInfoSection(F("About"), state.aboutSection, F("
")); escapePercentsForTemplate(state.status); escapePercentsForTemplate(state.deviceSection); escapePercentsForTemplate(state.wifiSection); escapePercentsForTemplate(state.aboutSection); + escapePercentsForTemplate(state.footer); } void buildWiFiPageState(WiFiManagerHandlers* handlers, @@ -231,49 +537,70 @@ void buildWiFiPageState(WiFiManagerHandlers* handlers, const String& ssidPlaceholder, const String& passwordPlaceholder, bool paramsInWifi) { - reservePage(state.scanResults, includeScanResults ? 4096 : 64); - reservePage(state.formSection, 4096); - reservePage(state.backSection, 256); + reservePage(state.scanContent, includeScanResults ? 4096 : 64); + reservePage(state.staticFields, 2048); + reservePage(state.paramSection, 4096); + reservePage(state.formActions, 128); + reservePage(state.pageActions, 512); reservePage(state.status, 512); state.docTitle = F("Config ESP"); + state.ssidPlaceholder = ssidPlaceholder; + state.passwordPlaceholder = passwordPlaceholder; if (includeScanResults) { - state.scanResults += F("
"); - state.scanResults += handlers->getScanItemOut(); - state.scanResults += F("
"); + state.scanContent += handlers->getScanItemOut(); } - state.formSection += F("
"); - - state.formSection += F("

"); - - state.formSection += handlers->getStaticOut(); - state.formSection += FPSTR(HTML_FORM_WIFI_END); + state.staticFields += handlers->getStaticOut(); if (paramsInWifi) { - state.formSection += FPSTR(HTML_FORM_PARAM_HEAD); - state.formSection += handlers->getParamOut(); + state.paramSection += renderSectionBreak(handlers->getParamOut()); } - state.formSection += FPSTR(HTML_FORM_END); - state.formSection += F("
"); - if (showBack) { - state.backSection += FPSTR(HTML_BACKBTN); - } + state.formActions += renderSubmitButton(F("Save")); + state.pageActions += renderCenteredButton( + F("refresh-btn"), + F("button"), + F("return refreshScan()"), + F("Refresh"), + includeScanResults ? String() : String(F(" data-skip-initial-scan='true'")) + ); + if (showBack) state.pageActions += renderActionForm(F("/"), F("get"), F("Back"), String(), F("

")); handlers->reportStatus(state.status); - escapePercentsForTemplate(state.scanResults); - escapePercentsForTemplate(state.formSection); - escapePercentsForTemplate(state.backSection); + escapePercentsForTemplate(state.scanContent); + escapePercentsForTemplate(state.ssidPlaceholder); + escapePercentsForTemplate(state.passwordPlaceholder); + escapePercentsForTemplate(state.staticFields); + escapePercentsForTemplate(state.paramSection); + escapePercentsForTemplate(state.formActions); + escapePercentsForTemplate(state.pageActions); + escapePercentsForTemplate(state.status); +} + +void buildParamPageState(WiFiManagerHandlers* handlers, + ParamPageState& state, + bool showBack) { + reservePage(state.fields, 4096); + reservePage(state.formActions, 128); + reservePage(state.pageActions, 256); + reservePage(state.status, 512); + + state.docTitle = F("Setup"); + state.fields += handlers->getParamOut(); + state.formActions += renderSubmitButton(F("Save")); + + if (showBack) state.pageActions += renderActionForm(F("/"), F("get"), F("Back"), String(), F("

")); + + handlers->reportStatus(state.status); + + escapePercentsForTemplate(state.fields); + escapePercentsForTemplate(state.formActions); + escapePercentsForTemplate(state.pageActions); escapePercentsForTemplate(state.status); } -#if WM_ROOT_REQUEST_STATE struct RootRenderBundle { RootState state; PlaceholderRegistry registry; @@ -284,21 +611,20 @@ struct RootRenderBundle { RootRenderBundle() : registry(WM_TEMPLATE_REGISTRY_CAPACITY), menuDescriptor{}, statusDescriptor{} {} }; -#endif struct MessageRenderBundle { MessagePageState state; PlaceholderRegistry registry; TemplateContext context; DynamicTemplateDescriptor docTitleDescriptor; - DynamicTemplateDescriptor bodyClassDescriptor; - DynamicTemplateDescriptor contentDescriptor; + DynamicTemplateDescriptor messageBodyDescriptor; + DynamicTemplateDescriptor actionsDescriptor; MessageRenderBundle() : registry(WM_TEMPLATE_REGISTRY_CAPACITY), docTitleDescriptor{}, - bodyClassDescriptor{}, - contentDescriptor{} {} + messageBodyDescriptor{}, + actionsDescriptor{} {} }; struct InfoRenderBundle { @@ -310,6 +636,7 @@ struct InfoRenderBundle { DynamicTemplateDescriptor deviceDescriptor; DynamicTemplateDescriptor wifiDescriptor; DynamicTemplateDescriptor aboutDescriptor; + DynamicTemplateDescriptor footerDescriptor; InfoRenderBundle() : registry(WM_TEMPLATE_REGISTRY_CAPACITY), @@ -317,7 +644,8 @@ struct InfoRenderBundle { statusDescriptor{}, deviceDescriptor{}, wifiDescriptor{}, - aboutDescriptor{} {} + aboutDescriptor{}, + footerDescriptor{} {} }; struct WiFiRenderBundle { @@ -325,44 +653,74 @@ struct WiFiRenderBundle { PlaceholderRegistry registry; TemplateContext context; DynamicTemplateDescriptor docTitleDescriptor; - DynamicTemplateDescriptor scanResultsDescriptor; - DynamicTemplateDescriptor formDescriptor; - DynamicTemplateDescriptor backDescriptor; + DynamicTemplateDescriptor scanContentDescriptor; + DynamicTemplateDescriptor ssidPlaceholderDescriptor; + DynamicTemplateDescriptor passwordPlaceholderDescriptor; + DynamicTemplateDescriptor staticFieldsDescriptor; + DynamicTemplateDescriptor paramSectionDescriptor; + DynamicTemplateDescriptor formActionsDescriptor; + DynamicTemplateDescriptor pageActionsDescriptor; DynamicTemplateDescriptor statusDescriptor; WiFiRenderBundle() : registry(WM_TEMPLATE_REGISTRY_CAPACITY), docTitleDescriptor{}, - scanResultsDescriptor{}, - formDescriptor{}, - backDescriptor{}, + scanContentDescriptor{}, + ssidPlaceholderDescriptor{}, + passwordPlaceholderDescriptor{}, + staticFieldsDescriptor{}, + paramSectionDescriptor{}, + formActionsDescriptor{}, + pageActionsDescriptor{}, statusDescriptor{} {} }; +struct ParamRenderBundle { + ParamPageState state; + PlaceholderRegistry registry; + TemplateContext context; + DynamicTemplateDescriptor docTitleDescriptor; + DynamicTemplateDescriptor fieldsDescriptor; + DynamicTemplateDescriptor formActionsDescriptor; + DynamicTemplateDescriptor pageActionsDescriptor; + DynamicTemplateDescriptor statusDescriptor; + + ParamRenderBundle() + : registry(WM_TEMPLATE_REGISTRY_CAPACITY), + docTitleDescriptor{}, + fieldsDescriptor{}, + formActionsDescriptor{}, + pageActionsDescriptor{}, + statusDescriptor{} {} +}; + +void sendMessageTemplateResponse(WiFiManagerServer* server, + const std::shared_ptr& bundle, + const String& title, + const char* bodyClass, + const String& messageBody, + const String& actions = String()) { + buildMessagePageState(bundle->state, title, bodyClass, messageBody, actions); + + registerSharedShellPlaceholders(server, bundle->registry); + registerShellTemplate(bundle->registry, + bundle->docTitleDescriptor, + bundle->state.docTitle, + bundle->state.bodyClass, + WM_MESSAGE_CONTENT_TEMPLATE); + configureDynamicStringDescriptor(bundle->messageBodyDescriptor, bundle->state.messageBody); + configureDynamicStringDescriptor(bundle->actionsDescriptor, bundle->state.actions); + + bundle->registry.registerDynamicTemplate("%MESSAGE_BODY%", &bundle->messageBodyDescriptor); + bundle->registry.registerDynamicTemplate("%MESSAGE_ACTIONS%", &bundle->actionsDescriptor); +} + } // namespace WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {} // Rendering Methods -String WiFiManagerHandlers::getHTTPHead(String title, String classes){ - String page; - reservePage(page, 512); - page += FPSTR(HTML_HEAD_START); - page += title; - page += FPSTR(HTML_TITLE_END); - page += FPSTR(JS_SCRIPT); - page += FPSTR(CSS_STYLE); - page += FPSTR(HTML_HEAD_END_START); - page += classes; - page += FPSTR(HTML_HEAD_END_WRAP); - return page; -} - -String WiFiManagerHandlers::getHTTPEnd() { - return FPSTR(HTML_END); -} - String WiFiManagerHandlers::getMenuOut(){ return getMenuOut(nullptr); } @@ -370,183 +728,165 @@ String WiFiManagerHandlers::getMenuOut(){ String WiFiManagerHandlers::getMenuOut(String* outOpt){ String local; String &out = outOpt ? *outOpt : local; - out += HTML_PORTAL_MENU[0]; // WIFI (scan) + out += renderActionForm(F("/wifi"), F("get"), F("Configure WiFi"), String(), String(), F("
\n")); // Show PARAM (Setup) when params are on their own page if(!_wm->_paramsInWifi && _wm->_paramsCount > 0){ - out += HTML_PORTAL_MENU[3]; // PARAM + out += renderActionForm(F("/param"), F("get"), F("Setup"), String(), String(), F("
\n")); } - out += HTML_PORTAL_MENU[2]; // INFO + out += renderActionForm(F("/info"), F("get"), F("Info"), String(), String(), F("
\n")); // When captive/config portal is active, offer Close (keeps portal running) if(_wm->configPortalActive){ - out += HTML_PORTAL_MENU[4]; // CLOSE + out += renderActionForm(F("/close"), F("get"), F("Close"), String(), String(), F("
\n")); } - out += HTML_PORTAL_MENU[6]; // EXIT - out += HTML_PORTAL_MENU[9]; // SEP - out += HTML_PORTAL_MENU[8]; // UPDATE + out += renderActionForm(F("/exit"), F("get"), F("Exit"), String(), String(), F("
\n")); + out += F("

"); + out += renderActionForm(F("/update"), F("get"), F("Update"), String(), String(), F("
\n")); return out; } +void WiFiManagerHandlers::collectVisibleScanResults(std::vector& networks) { + networks.clear(); + networks.reserve(_wm->_scanResultsCache.size()); + + for (const auto& network : _wm->_scanResultsCache) { + if (network.ssid.length() == 0) { + continue; + } + + int rssiperc = _wm->getRSSIasQuality(network.rssi); + if (_wm->_minimumQuality != -1 && _wm->_minimumQuality >= rssiperc) { + continue; + } + + networks.push_back(&network); + } + + const size_t n = networks.size(); + for (size_t i = 0; i < n; i++) { + for (size_t j = i + 1; j < n; j++) { + if (networks[j]->rssi > networks[i]->rssi) { + std::swap(networks[i], networks[j]); + } + } + } + + if (_wm->_removeDuplicateAPs) { + std::vector deduped; + deduped.reserve(networks.size()); + for (const auto* network : networks) { + bool duplicate = false; + for (const auto* existing : deduped) { + if (existing->ssid == network->ssid) { + duplicate = true; + break; + } + } + if (!duplicate) { + deduped.push_back(network); + } + } + networks.swap(deduped); + } +} + String WiFiManagerHandlers::getScanItemOut(){ - String page; - int n = _wm->_numNetworks; - reservePage(page, 256 + (n > 0 ? static_cast(n) * 96 : 96)); - - // Never trigger scans from here - only use cached data - // If no cached data, show message (scan should be started elsewhere if needed) - if (n == 0) { - #ifdef WM_DEBUG_LEVEL - _wm->DEBUG_WM(F("No networks found")); - #endif - if(_wm->_scanInProgress){ - page += F("Scanning for networks...

"); - } else { - page += F("No networks found. Refresh to scan again."); - page += F("

"); - } - } - else { - #ifdef WM_DEBUG_LEVEL - _wm->DEBUG_WM(n,F("networks found")); - #endif - //sort networks - int indices[n]; - for (int i = 0; i < n; i++) { - indices[i] = i; - } - - // RSSI SORT - for (int i = 0; i < n; i++) { - for (int j = i + 1; j < n; j++) { - if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) { - std::swap(indices[i], indices[j]); - } - } - } - - // remove duplicates ( must be RSSI sorted ) - if (_wm->_removeDuplicateAPs) { - String cssid; - for (int i = 0; i < n; i++) { - if (indices[i] == -1) continue; - cssid = WiFi.SSID(indices[i]); - for (int j = i + 1; j < n; j++) { - if (cssid == WiFi.SSID(indices[j])) { - #ifdef WM_DEBUG_LEVEL - _wm->DEBUG_WM(WM_DEBUG_VERBOSE,F("DUP AP:"),WiFi.SSID(indices[j])); - #endif - indices[j] = -1; // set dup aps to index -1 - } - } - } - } - - // Build network items directly without tokens - String hiddenClass = _wm->_scanDispOptions ? "" : "h"; - String visibleClass = _wm->_scanDispOptions ? "h" : ""; - - //display networks in page - for (int i = 0; i < n; i++) { - if (indices[i] == -1) continue; // skip dups - - #ifdef WM_DEBUG_LEVEL - _wm->DEBUG_WM(WM_DEBUG_VERBOSE,F("AP: "),(String)WiFi.RSSI(indices[i]) + " " + (String)WiFi.SSID(indices[i])); - #endif - - int rssiperc = _wm->getRSSIasQuality(WiFi.RSSI(indices[i])); - uint8_t enc_type = WiFi.encryptionType(indices[i]); - - if (_wm->_minimumQuality == -1 || _wm->_minimumQuality < rssiperc) { - if(WiFi.SSID(indices[i]) == ""){ - continue; // No idea why I am seeing these, lets just skip them for now - } - - String ssid_encoded = _wm->htmlEntities(WiFi.SSID(indices[i])); - String ssid_display = _wm->htmlEntities(WiFi.SSID(indices[i]), true); - int quality = int(round(map(rssiperc,0,100,1,4))); - String lockIcon = (enc_type != WM_WIFIOPEN) ? "l" : ""; - - // Build item HTML directly - page += F("
"); - page += ssid_display; - page += F(""); - - // Add RSSI quality icon - page += F(""); - - // Add RSSI percentage (if showing percentage) - page += F("
"); - page += String(rssiperc); - page += F("%
"); - - #ifdef WM_DEBUG_LEVEL - _wm->DEBUG_WM(WM_DEBUG_DEV, F("Added network item")); - #endif - delay(0); - } else { - #ifdef WM_DEBUG_LEVEL - _wm->DEBUG_WM(WM_DEBUG_VERBOSE,F("Skipping , does not meet _minimumQuality")); - #endif - } - - } - page += FPSTR(HTML_BR); - } + String page; + std::vector networks; + collectVisibleScanResults(networks); + const int n = static_cast(networks.size()); + reservePage(page, 256 + (n > 0 ? static_cast(n) * 96 : 96)); + if (_wm->_scan.state == WiFiManager::WM_SCAN_RUNNING || _wm->_scan.state == WiFiManager::WM_SCAN_QUEUED) { + page += renderScanMessage(F("Scanning for networks...")); return page; + } + + if (!_wm->_scan.resultsValid || n == 0) { + #ifdef WM_DEBUG_LEVEL + _wm->DEBUG_WM(F("No networks found")); + #endif + if (_wm->_scan.state == WiFiManager::WM_SCAN_TIMEOUT) { + page += renderScanMessage(F("Scan timed out. Refresh to try again.")); + } else if (_wm->_scan.state == WiFiManager::WM_SCAN_FAILED) { + page += renderScanMessage(F("Scan failed. Refresh to try again.")); + } else { + page += renderScanMessage(F("No networks found. Refresh to scan again.")); + } + return page; + } + + #ifdef WM_DEBUG_LEVEL + _wm->DEBUG_WM(n,F("networks found")); + #endif + + String hiddenClass = _wm->_scanDispOptions ? "" : "h"; + String visibleClass = _wm->_scanDispOptions ? "h" : ""; + + for (int i = 0; i < n; i++) { + const auto* network = networks[static_cast(i)]; + + #ifdef WM_DEBUG_LEVEL + _wm->DEBUG_WM(WM_DEBUG_VERBOSE,F("AP: "),(String)network->rssi + " " + network->ssid); + #endif + + int rssiperc = _wm->getRSSIasQuality(network->rssi); + String ssid_encoded = _wm->htmlEntities(network->ssid); + String ssid_display = _wm->htmlEntities(network->ssid, true); + int quality = int(round(map(rssiperc,0,100,1,4))); + String lockIcon = (network->encType != WM_WIFIOPEN) ? "l" : ""; + String qualityPercent = String(rssiperc) + "%"; + + page += renderScanRow( + ssid_encoded, + ssid_display, + qualityPercent, + String(quality), + lockIcon, + hiddenClass, + visibleClass, + qualityPercent + ); + + #ifdef WM_DEBUG_LEVEL + _wm->DEBUG_WM(WM_DEBUG_DEV, F("Added network item")); + #endif + delay(0); + } + + page += F("
"); + return page; } String WiFiManagerHandlers::getIpForm(String id, String title, String value){ - // Build IP form field directly without tokens - String item = F("
"); - return item; + return renderFieldTemplate(WM_FIELD_LABEL_BEFORE_TEMPLATE, id, title, F("15"), value); } String WiFiManagerHandlers::getStaticOut(){ String page; reservePage(page, 384); + String fields; + reservePage(fields, 384); if ((_wm->_staShowStaticFields || _wm->_sta_static_ip) && _wm->_staShowStaticFields>=0) { #ifdef WM_DEBUG_LEVEL _wm->DEBUG_WM(WM_DEBUG_DEV,F("_staShowStaticFields")); #endif - page += FPSTR(HTML_FORM_STATIC_HEAD); - page += getIpForm(FPSTR(S_ip),F("Static IP"),(_wm->_sta_static_ip ? _wm->_sta_static_ip.toString() : "")); - page += getIpForm(FPSTR(S_gw),F("Static gateway"),(_wm->_sta_static_gw ? _wm->_sta_static_gw.toString() : "")); - page += getIpForm(FPSTR(S_sn),F("Subnet"),(_wm->_sta_static_sn ? _wm->_sta_static_sn.toString() : "")); + fields += getIpForm(FPSTR(S_ip),F("Static IP"),(_wm->_sta_static_ip ? _wm->_sta_static_ip.toString() : "")); + fields += getIpForm(FPSTR(S_gw),F("Static gateway"),(_wm->_sta_static_gw ? _wm->_sta_static_gw.toString() : "")); + fields += getIpForm(FPSTR(S_sn),F("Subnet"),(_wm->_sta_static_sn ? _wm->_sta_static_sn.toString() : "")); } if((_wm->_staShowDns || _wm->_sta_static_dns) && _wm->_staShowDns>=0){ - page += getIpForm(FPSTR(S_dns),F("Static DNS"),(_wm->_sta_static_dns ? _wm->_sta_static_dns.toString() : "")); + fields += getIpForm(FPSTR(S_dns),F("Static DNS"),(_wm->_sta_static_dns ? _wm->_sta_static_dns.toString() : "")); } - if(page!="") page += FPSTR(HTML_BR); + if(fields != ""){ + page += renderSectionBreak(fields); + page += F("
"); + } return page; } @@ -578,71 +918,21 @@ String WiFiManagerHandlers::getParamOut(){ if (_wm->_params[i]->getID() != NULL) { String paramId = _wm->_params[i]->getID(); - String paramLabel = _wm->_params[i]->getLabel(); - String paramValue = _wm->_params[i]->getValue(); + String paramLabel = _wm->htmlEntities(_wm->_params[i]->getLabel(), true); + String paramValue = _wm->htmlEntities(_wm->_params[i]->getValue(), true); String customHTML = _wm->_params[i]->getCustomHTML(); snprintf(valLength, 5, "%d", _wm->_params[i]->getValueLength()); + String extraAttrs = customHTML.length() > 0 ? String(F(" ")) + customHTML : String(); - // Build parameter HTML directly based on label placement switch (_wm->_params[i]->getLabelPlacement()) { case WFM_LABEL_BEFORE: - // Label before input - pitem = F("
0) { - pitem += F(" "); - pitem += customHTML; - } - pitem += F(">\n"); + pitem = renderFieldTemplate(WM_FIELD_LABEL_BEFORE_TEMPLATE, paramId, paramLabel, String(valLength), paramValue, extraAttrs); break; case WFM_LABEL_AFTER: - // Label after input - pitem = F("
0) { - pitem += F(" "); - pitem += customHTML; - } - pitem += F(">\n"); + pitem = renderFieldTemplate(WM_FIELD_LABEL_AFTER_TEMPLATE, paramId, paramLabel, String(valLength), paramValue, extraAttrs); break; default: - // WFM_NO_LABEL - no label - pitem = F("
0) { - pitem += F(" "); - pitem += customHTML; - } - pitem += F(">\n"); + pitem = renderFieldTemplate(WM_FIELD_INPUT_ONLY_TEMPLATE, paramId, paramLabel, String(valLength), paramValue, extraAttrs); break; } } else { @@ -659,100 +949,60 @@ String WiFiManagerHandlers::getParamOut(){ String WiFiManagerHandlers::getInfoData(String id){ String p; - if(id==F("esphead")){ - #ifdef ESP32 - p = F("

esp32


"); - #else - p = F("

esp8266


"); - #endif - } - else if(id==F("wifihead")){ - p = F("

WiFi


"); - } - else if(id==F("uptime")){ - p = F("
Uptime
"); - p += String(millis() / 1000 / 60); - p += F(" mins "); - p += String((millis() / 1000) % 60); - p += F(" secs
"); + if(id==F("uptime")){ + p = renderInfoRow(F("Uptime"), String(millis() / 1000 / 60) + F(" mins ") + String((millis() / 1000) % 60) + F(" secs")); } else if(id==F("chipid")){ - p = F("
Chip ID
"); #ifdef ESP8266 - p += String(ESP.getChipId(),HEX); + p = renderInfoRow(F("Chip ID"), String(ESP.getChipId(),HEX)); #elif defined(ESP32) - p += String((uint32_t)ESP.getEfuseMac(),HEX); + p = renderInfoRow(F("Chip ID"), String((uint32_t)ESP.getEfuseMac(),HEX)); #endif - p += F("
"); } #ifdef ESP32 else if(id==F("chiprev")){ - p = F("
Chip rev
"); String rev = (String)ESP.getChipRevision(); #ifdef _SOC_EFUSE_REG_H_ String revb = (String)(REG_READ(EFUSE_BLK0_RDATA3_REG) >> (EFUSE_RD_CHIP_VER_RESERVE_S)&&EFUSE_RD_CHIP_VER_RESERVE_V); - p += rev; - p += F("
"); - p += revb; + p = renderInfoRow(F("Chip rev"), rev + F("
") + revb); #else - p += rev; + p = renderInfoRow(F("Chip rev"), rev); #endif - p += F("
"); } #endif #ifdef ESP8266 else if(id==F("fchipid")){ - p = F("
Flash chip ID
"); - p += String(ESP.getFlashChipId()); - p += F("
"); + p = renderInfoRow(F("Flash chip ID"), String(ESP.getFlashChipId())); } #endif else if(id==F("idesize")){ - p = F("
Flash size
"); - p += String(ESP.getFlashChipSize()); - p += F(" bytes
"); + p = renderInfoRow(F("Flash size"), String(ESP.getFlashChipSize()) + F(" bytes")); } else if(id==F("flashsize")){ #ifdef ESP8266 - p = F("
Real flash size
"); - p += String(ESP.getFlashChipRealSize()); - p += F(" bytes
"); + p = renderInfoRow(F("Real flash size"), String(ESP.getFlashChipRealSize()) + F(" bytes")); #elif defined ESP32 - p = F("
PSRAM Size
"); - p += String(ESP.getPsramSize()); - p += F(" bytes
"); + p = renderInfoRow(F("PSRAM Size"), String(ESP.getPsramSize()) + F(" bytes")); #endif } else if(id==F("corever")){ #ifdef ESP8266 - p = F("
Core version
"); - p += String(ESP.getCoreVersion()); - p += F("
"); + p = renderInfoRow(F("Core version"), String(ESP.getCoreVersion())); #endif } #ifdef ESP8266 else if(id==F("bootver")){ - p = F("
Boot version
"); - p += String(system_get_boot_version()); - p += F("
"); + p = renderInfoRow(F("Boot version"), String(system_get_boot_version())); } #endif else if(id==F("cpufreq")){ - p = F("
CPU frequency
"); - p += String(ESP.getCpuFreqMHz()); - p += F("MHz
"); + p = renderInfoRow(F("CPU frequency"), String(ESP.getCpuFreqMHz()) + F("MHz")); } else if(id==F("freeheap")){ - p = F("
Memory - Free heap
"); - p += String(ESP.getFreeHeap()); - p += F(" bytes available
"); + p = renderInfoRow(F("Memory - Free heap"), String(ESP.getFreeHeap()) + F(" bytes available")); } else if(id==F("memsketch")){ - p = F("
Memory - Sketch size
Used / Total bytes
"); - p += String(ESP.getSketchSize()); - p += F(" / "); - p += String(ESP.getSketchSize()+ESP.getFreeSketchSpace()); - p += F("
"); + p = renderInfoRow(F("Memory - Sketch size"), String(F("Used / Total bytes
")) + String(ESP.getSketchSize()) + F(" / ") + String(ESP.getSketchSize()+ESP.getFreeSketchSpace())); } else if(id==F("memsmeter")){ p = F("
Connected to "); - str += _wm->htmlEntities(_wm->WiFi_SSID()); - str += F("
with IP "); - str += WiFi.localIP().toString(); - str += F("
"); + str = renderStatusMessage( + F("Connected"), + String(F(" to ")) + _wm->htmlEntities(_wm->WiFi_SSID(), true) + F("
with IP ") + WiFi.localIP().toString() + F(""), + F(" S") + ); } else { - // Build disconnected status HTML directly - String ssidEncoded = _wm->htmlEntities(_wm->WiFi_SSID()); - String statusClass = "D"; + String ssidEncoded = _wm->htmlEntities(_wm->WiFi_SSID(), true); + String statusClass = " D"; String statusMsg = ""; if(_wm->_lastconxresult == _wm->WL_STATION_WRONG_PASSWORD){ @@ -948,13 +1150,8 @@ void WiFiManagerHandlers::reportStatus(String &page){ else{ statusClass = ""; } - - str = F("
Not connected to "); - str += ssidEncoded; - str += statusMsg; - str += F("
"); + + str = renderStatusMessage(F("Not connected"), String(F(" to ")) + ssidEncoded + statusMsg, statusClass); } } else { @@ -1028,7 +1225,6 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) { handleRequest(request); AsyncWebServerResponse *response = nullptr; -#if WM_ROOT_REQUEST_STATE auto bundle = std::make_shared(); buildRootState(this, bundle->state); @@ -1051,24 +1247,8 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) { } response = beginTemplateResponse(request, bundle, WM_ROOT_TEMPLATE); -#else - TemplateContext ctx; - if (_wm->_serverManager && _wm->_serverManager->getPlaceholderRegistry()) { - ctx.setRegistry(_wm->_serverManager->getPlaceholderRegistry()); - } - TemplateRenderer::initializeContext(ctx, WM_ROOT_TEMPLATE); - auto ctxPtr = std::make_shared(ctx); - - response = request->beginChunkedResponse(String(FPSTR(HTTP_HEAD_CT)), - [ctxPtr](uint8_t *buffer, size_t maxLen, size_t /*index*/) -> size_t { - if (!ctxPtr) return 0; - return TemplateRenderer::renderNextChunk(*ctxPtr, buffer, maxLen); - } - ); -#endif request->send(response); - if(_wm->_preloadwifiscan) _wm->WiFi_scanNetworks(_wm->_scancachetime); } void WiFiManagerHandlers::handleWifi(AsyncWebServerRequest *request, boolean scan) { @@ -1084,20 +1264,6 @@ void WiFiManagerHandlers::handleWifi(AsyncWebServerRequest *request, boolean sca } handleRequest(request); auto bundle = std::make_shared(); - if (scan) { - bool forceRefresh = false; - if (request->hasParam("refresh")) { - forceRefresh = true; - } - - if(forceRefresh || !_wm->_lastscan || (millis()-_wm->_lastscan > _wm->_scancachetime)){ - if(!_wm->_scanInProgress){ - _wm->WiFi_scanNetworks(true); - } else { - _wm->_scanRequested = true; - } - } - } String ssidPlaceholder = _wm->WiFi_SSID(); String passwordPlaceholder = ""; @@ -1117,29 +1283,43 @@ void WiFiManagerHandlers::handleWifi(AsyncWebServerRequest *request, boolean sca _wm->_paramsInWifi && _wm->_paramsCount > 0); registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry); - configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle); - configureDynamicStringDescriptor(bundle->scanResultsDescriptor, bundle->state.scanResults); - configureDynamicStringDescriptor(bundle->formDescriptor, bundle->state.formSection); - configureDynamicStringDescriptor(bundle->backDescriptor, bundle->state.backSection); + registerShellTemplate(bundle->registry, + bundle->docTitleDescriptor, + bundle->state.docTitle, + C_wifi, + WM_WIFI_CONTENT_TEMPLATE, + WIFI_POLLING_JS); + configureDynamicStringDescriptor(bundle->scanContentDescriptor, bundle->state.scanContent); + configureDynamicStringDescriptor(bundle->ssidPlaceholderDescriptor, bundle->state.ssidPlaceholder); + configureDynamicStringDescriptor(bundle->passwordPlaceholderDescriptor, bundle->state.passwordPlaceholder); + configureDynamicStringDescriptor(bundle->staticFieldsDescriptor, bundle->state.staticFields); + configureDynamicStringDescriptor(bundle->paramSectionDescriptor, bundle->state.paramSection); + configureDynamicStringDescriptor(bundle->formActionsDescriptor, bundle->state.formActions); + configureDynamicStringDescriptor(bundle->pageActionsDescriptor, bundle->state.pageActions); configureDynamicStringDescriptor(bundle->statusDescriptor, bundle->state.status); - bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor); - bundle->registry.registerDynamicTemplate("%WIFI_SCAN_RESULTS%", &bundle->scanResultsDescriptor); - bundle->registry.registerDynamicTemplate("%WIFI_FORM_SECTION%", &bundle->formDescriptor); - bundle->registry.registerDynamicTemplate("%WIFI_BACK_SECTION%", &bundle->backDescriptor); + bundle->registry.registerDynamicTemplate("%WIFI_SCAN_CONTENT%", &bundle->scanContentDescriptor); + bundle->registry.registerDynamicTemplate("%WIFI_SSID_PLACEHOLDER%", &bundle->ssidPlaceholderDescriptor); + bundle->registry.registerDynamicTemplate("%WIFI_PASSWORD_PLACEHOLDER%", &bundle->passwordPlaceholderDescriptor); + bundle->registry.registerDynamicTemplate("%WIFI_STATIC_FIELDS%", &bundle->staticFieldsDescriptor); + bundle->registry.registerDynamicTemplate("%WIFI_PARAM_SECTION%", &bundle->paramSectionDescriptor); + bundle->registry.registerDynamicTemplate("%WIFI_FORM_ACTIONS%", &bundle->formActionsDescriptor); + bundle->registry.registerDynamicTemplate("%WIFI_PAGE_ACTIONS%", &bundle->pageActionsDescriptor); bundle->registry.registerDynamicTemplate("%WIFI_STATUS%", &bundle->statusDescriptor); #ifdef WM_DEBUG_LEVEL - size_t debugPageLength = bundle->state.scanResults.length() + bundle->state.formSection.length() - + bundle->state.backSection.length() + bundle->state.status.length(); + size_t debugPageLength = bundle->state.scanContent.length() + bundle->state.staticFields.length() + + bundle->state.paramSection.length() + + bundle->state.formActions.length() + bundle->state.pageActions.length() + + bundle->state.status.length(); _wm->DEBUG_WM(WM_DEBUG_DEV, F("Page length: "), String(debugPageLength)); _wm->DEBUG_WM(WM_DEBUG_DEV, F("_numNetworks: "), String(_wm->_numNetworks)); - _wm->DEBUG_WM(WM_DEBUG_DEV, F("_scanInProgress: "), _wm->_scanInProgress ? "true" : "false"); + _wm->DEBUG_WM(WM_DEBUG_DEV, F("_scanState: "), String(_wm->_scan.state)); _wm->DEBUG_WM(WM_DEBUG_DEV, F("_lastscan: "), String(_wm->_lastscan)); _wm->DEBUG_WM(WM_DEBUG_DEV, F("About to send response")); #endif - request->send(beginTemplateResponse(request, bundle, WM_WIFI_TEMPLATE)); + request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); #ifdef WM_DEBUG_LEVEL _wm->DEBUG_WM(WM_DEBUG_DEV, F("Response sent")); @@ -1151,19 +1331,26 @@ void WiFiManagerHandlers::handleParam(AsyncWebServerRequest *request){ _wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP Param")); #endif handleRequest(request); - String page = getHTTPHead(F("Setup"), FPSTR(C_param)); - reservePage(page, 4096); + auto bundle = std::make_shared(); + buildParamPageState(this, bundle->state, _wm->_showBack); - // Build form start directly without tokens - page += F(""); + registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry); + registerShellTemplate(bundle->registry, + bundle->docTitleDescriptor, + bundle->state.docTitle, + C_param, + WM_PARAM_CONTENT_TEMPLATE); + configureDynamicStringDescriptor(bundle->fieldsDescriptor, bundle->state.fields); + configureDynamicStringDescriptor(bundle->formActionsDescriptor, bundle->state.formActions); + configureDynamicStringDescriptor(bundle->pageActionsDescriptor, bundle->state.pageActions); + configureDynamicStringDescriptor(bundle->statusDescriptor, bundle->state.status); - page += getParamOut(); - page += FPSTR(HTML_FORM_END); - if(_wm->_showBack) page += FPSTR(HTML_BACKBTN); - reportStatus(page); - page += getHTTPEnd(); + bundle->registry.registerDynamicTemplate("%PARAM_FIELDS%", &bundle->fieldsDescriptor); + bundle->registry.registerDynamicTemplate("%PARAM_FORM_ACTIONS%", &bundle->formActionsDescriptor); + bundle->registry.registerDynamicTemplate("%PARAM_PAGE_ACTIONS%", &bundle->pageActionsDescriptor); + bundle->registry.registerDynamicTemplate("%PARAM_STATUS%", &bundle->statusDescriptor); - request->send(200, FPSTR(HTTP_HEAD_CT), page); + request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); #ifdef WM_DEBUG_LEVEL _wm->DEBUG_WM(WM_DEBUG_DEV, F("Sent param page")); @@ -1245,23 +1432,28 @@ void WiFiManagerHandlers::handleWifiSave(AsyncWebServerRequest *request) { if(_wm->_paramsInWifi) doParamSave(requestArgs); - String page; + auto bundle = std::make_shared(); + String messageBody; + String actions; + reservePage(messageBody, 1024); if(_wm->_ssid == ""){ - page = getHTTPHead(F("Settings saved"), FPSTR(C_wifi)); - reservePage(page, 1024); - page += FPSTR(HTML_PARAMSAVED); + messageBody += FPSTR(HTML_PARAMSAVED); } else { - page = getHTTPHead(F("Credentials saved"), FPSTR(C_wifi)); - reservePage(page, 1024); - page += FPSTR(HTML_SAVED); + messageBody += FPSTR(HTML_SAVED); } - if(_wm->_showBack) page += FPSTR(HTML_BACKBTN); - page += getHTTPEnd(); + if(_wm->_showBack) actions += renderActionForm(F("/"), F("get"), F("Back"), String(), F("

")); - AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(HTTP_HEAD_CT), page); + sendMessageTemplateResponse(_wm->_serverManager.get(), + bundle, + _wm->_ssid == "" ? F("Settings saved") : F("Credentials saved"), + C_wifi, + messageBody, + actions); + + AsyncWebServerResponse *response = beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE); response->addHeader(FPSTR(HTTP_HEAD_CORS), FPSTR(HTTP_HEAD_CORS_ALLOW_ALL)); request->send(response); @@ -1283,13 +1475,21 @@ void WiFiManagerHandlers::handleParamSave(AsyncWebServerRequest *request) { doParamSave(requestArgs); - String page = getHTTPHead(F("Setup saved"), FPSTR(C_param)); - reservePage(page, 1024); - page += FPSTR(HTML_PARAMSAVED); - if(_wm->_showBack) page += FPSTR(HTML_BACKBTN); - page += getHTTPEnd(); + auto bundle = std::make_shared(); + String messageBody; + String actions; + reservePage(messageBody, 1024); + messageBody += FPSTR(HTML_PARAMSAVED); + if(_wm->_showBack) actions += renderActionForm(F("/"), F("get"), F("Back"), String(), F("

")); - request->send(200, FPSTR(HTTP_HEAD_CT), page); + sendMessageTemplateResponse(_wm->_serverManager.get(), + bundle, + F("Setup saved"), + C_param, + messageBody, + actions); + + request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); #ifdef WM_DEBUG_LEVEL _wm->DEBUG_WM(WM_DEBUG_DEV, F("Sent param save page")); @@ -1359,19 +1559,24 @@ void WiFiManagerHandlers::handleInfo(AsyncWebServerRequest *request) { buildInfoPageState(this, bundle->state, _wm->_showInfoUpdate, _wm->_showInfoErase, _wm->_showBack); registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry); - configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle); + registerShellTemplate(bundle->registry, + bundle->docTitleDescriptor, + bundle->state.docTitle, + C_info, + WM_INFO_CONTENT_TEMPLATE); configureDynamicStringDescriptor(bundle->statusDescriptor, bundle->state.status); configureDynamicStringDescriptor(bundle->deviceDescriptor, bundle->state.deviceSection); configureDynamicStringDescriptor(bundle->wifiDescriptor, bundle->state.wifiSection); configureDynamicStringDescriptor(bundle->aboutDescriptor, bundle->state.aboutSection); + configureDynamicStringDescriptor(bundle->footerDescriptor, bundle->state.footer); - bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor); bundle->registry.registerDynamicTemplate("%INFO_STATUS%", &bundle->statusDescriptor); bundle->registry.registerDynamicTemplate("%INFO_DEVICE_SECTION%", &bundle->deviceDescriptor); bundle->registry.registerDynamicTemplate("%INFO_WIFI_SECTION%", &bundle->wifiDescriptor); bundle->registry.registerDynamicTemplate("%INFO_ABOUT_SECTION%", &bundle->aboutDescriptor); + bundle->registry.registerDynamicTemplate("%INFO_FOOTER%", &bundle->footerDescriptor); - request->send(beginTemplateResponse(request, bundle, WM_INFO_TEMPLATE)); + request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); #ifdef WM_DEBUG_LEVEL _wm->DEBUG_WM(WM_DEBUG_DEV, F("Sent info page")); @@ -1384,16 +1589,11 @@ void WiFiManagerHandlers::handleExit(AsyncWebServerRequest *request) { #endif handleRequest(request); auto bundle = std::make_shared(); - buildMessagePageState(bundle->state, F("Exit"), FPSTR(C_exit), String(F("Exiting"))); - - registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry); - configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle); - configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass); - configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content); - - bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor); - bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor); - bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor); + sendMessageTemplateResponse(_wm->_serverManager.get(), + bundle, + F("Exit"), + C_exit, + String(F("Exiting"))); AsyncWebServerResponse *response = beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE); response->addHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate")); @@ -1409,19 +1609,11 @@ void WiFiManagerHandlers::handleReset(AsyncWebServerRequest *request) { #endif handleRequest(request); auto bundle = std::make_shared(); - buildMessagePageState(bundle->state, - F("Reset"), - FPSTR(C_restart), - String(F("Module will reset in a few seconds."))); - - registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry); - configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle); - configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass); - configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content); - - bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor); - bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor); - bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor); + sendMessageTemplateResponse(_wm->_serverManager.get(), + bundle, + F("Reset"), + C_restart, + String(F("Module will reset in a few seconds."))); request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); @@ -1439,27 +1631,22 @@ void WiFiManagerHandlers::handleErase(AsyncWebServerRequest *request, boolean op handleRequest(request); bool ret = _wm->erase(opt); auto bundle = std::make_shared(); - String content; - reservePage(content, 256); + String messageBody; + reservePage(messageBody, 256); - if(ret) content += F("Module will reset in a few seconds."); + if(ret) messageBody += F("Module will reset in a few seconds."); else { - content += F("An error occured"); + messageBody += F("An error occured"); #ifdef WM_DEBUG_LEVEL _wm->DEBUG_WM(WM_DEBUG_ERROR, F("[ERROR] WiFi EraseConfig failed")); #endif } - buildMessagePageState(bundle->state, F("Erase"), FPSTR(C_erase), content); - - registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry); - configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle); - configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass); - configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content); - - bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor); - bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor); - bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor); + sendMessageTemplateResponse(_wm->_serverManager.get(), + bundle, + F("Erase"), + C_erase, + messageBody); request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); @@ -1480,19 +1667,11 @@ void WiFiManagerHandlers::handleClose(AsyncWebServerRequest *request){ #endif handleRequest(request); auto bundle = std::make_shared(); - buildMessagePageState(bundle->state, - F("Close"), - FPSTR(C_close), - String(F("You can close the page, portal will continue to run"))); - - registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry); - configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle); - configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass); - configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content); - - bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor); - bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor); - bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor); + sendMessageTemplateResponse(_wm->_serverManager.get(), + bundle, + F("Close"), + C_close, + String(F("You can close the page, portal will continue to run"))); request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); } @@ -1508,105 +1687,100 @@ void WiFiManagerHandlers::handleNotFound(AsyncWebServerRequest *request) { request->send(response); } -void WiFiManagerHandlers::handleWiFiStatus(AsyncWebServerRequest *request){ - #ifdef WM_DEBUG_LEVEL - _wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP WiFi status ")); - #endif - handleRequest(request); - String page; - reservePage(page, 256); - #ifdef WM_JSTEST - page = FPSTR(HTML_JS); - #endif - request->send(200, FPSTR(HTTP_HEAD_CT), page); -} - void WiFiManagerHandlers::handleWiFiScanStatus(AsyncWebServerRequest *request){ #ifdef WM_DEBUG_LEVEL _wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP WiFi scan status")); #endif handleRequest(request); - + + std::vector networks; + collectVisibleScanResults(networks); + String json = "{"; - reservePage(json, 128 + (_wm->_numNetworks > 0 ? static_cast(_wm->_numNetworks) * 96 : 64)); - json += "\"scanning\":"; - json += _wm->_scanInProgress ? "true" : "false"; + reservePage(json, 160 + (networks.size() > 0 ? networks.size() * 96 : 64)); + json += "\"state\":\""; + switch (_wm->_scan.state) { + case WiFiManager::WM_SCAN_IDLE: json += "idle"; break; + case WiFiManager::WM_SCAN_QUEUED: json += "queued"; break; + case WiFiManager::WM_SCAN_RUNNING: json += "running"; break; + case WiFiManager::WM_SCAN_COMPLETE: json += "complete"; break; + case WiFiManager::WM_SCAN_FAILED: json += "failed"; break; + case WiFiManager::WM_SCAN_TIMEOUT: json += "timeout"; break; + } + json += "\",\"scanning\":"; + json += (_wm->_scan.state == WiFiManager::WM_SCAN_RUNNING || _wm->_scan.state == WiFiManager::WM_SCAN_QUEUED) ? "true" : "false"; + json += ",\"results_valid\":"; + json += _wm->_scan.resultsValid ? "true" : "false"; json += ",\"count\":"; - json += String(_wm->_numNetworks); + json += String(networks.size()); json += ",\"lastscan\":"; json += String(_wm->_lastscan); - - if(!_wm->_scanInProgress && _wm->_numNetworks > 0){ - json += ",\"networks\":["; - - int n = _wm->_numNetworks; - int indices[n]; - for (int i = 0; i < n; i++) { - indices[i] = i; - } - - for (int i = 0; i < n; i++) { - for (int j = i + 1; j < n; j++) { - if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) { - std::swap(indices[i], indices[j]); - } - } - } - - if (_wm->_removeDuplicateAPs) { - String cssid; - for (int i = 0; i < n; i++) { - if (indices[i] == -1) continue; - cssid = WiFi.SSID(indices[i]); - for (int j = i + 1; j < n; j++) { - if (cssid == WiFi.SSID(indices[j])) { - indices[j] = -1; - } - } - } - } - - bool first = true; - for (int i = 0; i < n; i++) { - if (indices[i] == -1) continue; - - if (!first) json += ","; - first = false; - - json += "{"; - json += "\"ssid\":\"" + _wm->htmlEntities(WiFi.SSID(indices[i]), true) + "\","; - json += "\"rssi\":" + String(WiFi.RSSI(indices[i])) + ","; - json += "\"encryption\":" + String(WiFi.encryptionType(indices[i])); - json += "}"; - } - json += "]"; + json += ",\"error\":\""; + if (_wm->_scan.state == WiFiManager::WM_SCAN_TIMEOUT) { + json += "timeout"; + } else if (_wm->_scan.state == WiFiManager::WM_SCAN_FAILED) { + json += "failed"; } - - json += "}"; - + json += "\",\"networks\":["; + appendVisibleScanResultsJson(json, networks); + json += "]}"; + AsyncWebServerResponse *response = request->beginResponse(200, "application/json", json); response->addHeader(F("Cache-Control"), F("no-cache")); request->send(response); } +void WiFiManagerHandlers::appendVisibleScanResultsJson(String& json, const std::vector& networks) { + for (size_t i = 0; i < networks.size(); i++) { + const auto* network = networks[i]; + if (i > 0) { + json += ","; + } + json += "{"; + json += "\"ssid\":\""; + json += _wm->htmlEntities(network->ssid, true); + json += "\",\"rssi\":"; + json += String(network->rssi); + json += ",\"quality\":"; + json += String(_wm->getRSSIasQuality(network->rssi)); + json += ",\"enc_type\":"; + json += String(network->encType); + json += ",\"encrypted\":"; + json += (network->encType != WM_WIFIOPEN) ? "true" : "false"; + json += "}"; + } +} + +void WiFiManagerHandlers::handleWiFiScanRequest(AsyncWebServerRequest *request){ + #ifdef WM_DEBUG_LEVEL + _wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP WiFi scan request")); + #endif + handleRequest(request); + _wm->requestAsyncScan(true); + + AsyncWebServerResponse *response = request->beginResponse(202, "application/json", + "{\"accepted\":true,\"state\":\"queued\"}"); + response->addHeader(F("Cache-Control"), F("no-cache")); + request->send(response); +} + void WiFiManagerHandlers::handleUpdate(AsyncWebServerRequest *request) { #ifdef WM_DEBUG_LEVEL _wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- Handle update")); #endif if (captivePortal(request)) return; - String page = getHTTPHead(_wm->_title, FPSTR(C_update)); - reservePage(page, 2048); - // Build root main HTML directly without tokens - page += F("

"); - page += _wm->_title; - page += F("

"); - page += (_wm->configPortalActive ? _wm->_apName : (_wm->getWiFiHostname() + " - " + WiFi.localIP().toString())); - page += F("

"); + handleRequest(request); + auto bundle = std::make_shared(); + const String subtitle = _wm->configPortalActive + ? _wm->_apName + : (_wm->getWiFiHostname() + " - " + WiFi.localIP().toString()); + sendMessageTemplateResponse(_wm->_serverManager.get(), + bundle, + F("Update"), + C_update, + buildUpdatePanelContent(_wm->_title, subtitle)); - page += FPSTR(HTML_UPDATE); - page += getHTTPEnd(); - - request->send(200, FPSTR(HTTP_HEAD_CT), page); + request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); } void WiFiManagerHandlers::handleUpdating(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { @@ -1679,35 +1853,15 @@ void WiFiManagerHandlers::handleUpdateDone(AsyncWebServerRequest *request) { #ifdef WM_DEBUG_LEVEL _wm->DEBUG_WM(WM_DEBUG_VERBOSE, F("<- Handle update done")); #endif + handleRequest(request); auto bundle = std::make_shared(); - String content; - reservePage(content, 2048); - appendPortalHeader(_wm->_title, - (_wm->configPortalActive ? _wm->_apName : WiFi.localIP().toString()), - content); - - if (Update.hasError()) { - content += FPSTR(HTML_UPDATE_FAIL); - #ifdef ESP32 - content += "OTA Error: " + (String)Update.errorString(); - #else - content += "OTA Error: " + (String)Update.getError(); - #endif - } else { - content += FPSTR(HTML_UPDATE_SUCCESS); - } - - buildMessagePageState(bundle->state, F("options"), FPSTR(C_update), content); - - registerSharedShellPlaceholders(_wm->_serverManager.get(), bundle->registry); - configureDynamicStringDescriptor(bundle->docTitleDescriptor, bundle->state.docTitle); - configureDynamicStringDescriptor(bundle->bodyClassDescriptor, bundle->state.bodyClass); - configureDynamicStringDescriptor(bundle->contentDescriptor, bundle->state.content); - - bundle->registry.registerDynamicTemplate("%DOC_TITLE%", &bundle->docTitleDescriptor); - bundle->registry.registerDynamicTemplate("%BODY_CLASS%", &bundle->bodyClassDescriptor); - bundle->registry.registerDynamicTemplate("%PAGE_CONTENT%", &bundle->contentDescriptor); + const String subtitle = _wm->configPortalActive ? _wm->_apName : WiFi.localIP().toString(); + sendMessageTemplateResponse(_wm->_serverManager.get(), + bundle, + F("Update"), + C_update, + buildUpdateResultContent(_wm->_title, subtitle)); request->send(beginTemplateResponse(request, bundle, WM_PAGE_SHELL_TEMPLATE)); diff --git a/lib/WiFiManager/src/WiFiManagerServer.cpp b/lib/WiFiManager/src/WiFiManagerServer.cpp index fd17c87..23b7e75 100644 --- a/lib/WiFiManager/src/WiFiManagerServer.cpp +++ b/lib/WiFiManager/src/WiFiManagerServer.cpp @@ -222,13 +222,13 @@ void WiFiManagerServer::registerRoutes() { this->_handlers->handleErase(request, false); }); - server->on(WM_G(R_status), HTTP_GET, [this](AsyncWebServerRequest *request) { - this->_handlers->handleWiFiStatus(request); - }); - server->on(WM_G(R_scanstatus), HTTP_GET, [this](AsyncWebServerRequest *request) { this->_handlers->handleWiFiScanStatus(request); }); + + server->on(WM_G(R_scan), HTTP_POST, [this](AsyncWebServerRequest *request) { + this->_handlers->handleWiFiScanRequest(request); + }); // OTA Update routes server->on(WM_G(R_update), HTTP_GET, [this](AsyncWebServerRequest *request) { diff --git a/test/test_wifimanager/test_main.cpp b/test/test_wifimanager/test_main.cpp index 4e0c17c..489167c 100644 --- a/test/test_wifimanager/test_main.cpp +++ b/test/test_wifimanager/test_main.cpp @@ -108,6 +108,20 @@ TestCase tests[] = { TEST_ENTRY(test_async_scan_behavior), TEST_ENTRY(test_scan_status_checking), TEST_ENTRY(test_scan_completion_wait), + TEST_ENTRY(test_scan_cancels_when_connect_pending), + TEST_ENTRY(test_scan_cancels_when_lifecycle_blocked), + TEST_ENTRY(test_scan_generation_invalidated_on_reset), + + // Template rendering tests + TEST_ENTRY(test_page_shell_supports_page_assets), + TEST_ENTRY(test_root_template_renders_core_placeholders), + TEST_ENTRY(test_wifi_template_renders_scan_container_and_form_fields), + TEST_ENTRY(test_message_template_renders_body_and_actions), + TEST_ENTRY(test_param_template_renders_shared_actions), + TEST_ENTRY(test_info_template_renders_sections_and_footer), + TEST_ENTRY(test_fragment_action_form_renders_shared_button_markup), + TEST_ENTRY(test_fragment_info_row_and_section_render), + TEST_ENTRY(test_fragment_centered_button_supports_extra_attrs), // State transition tests TEST_ENTRY(test_portal_to_connected_transition), diff --git a/test/test_wifimanager/test_main.h b/test/test_wifimanager/test_main.h index 4e03c83..43ae1b4 100644 --- a/test/test_wifimanager/test_main.h +++ b/test/test_wifimanager/test_main.h @@ -118,6 +118,20 @@ void test_wifi_scan_initiates(); void test_async_scan_behavior(); void test_scan_status_checking(); void test_scan_completion_wait(); +void test_scan_cancels_when_connect_pending(); +void test_scan_cancels_when_lifecycle_blocked(); +void test_scan_generation_invalidated_on_reset(); + +// Template rendering tests +void test_page_shell_supports_page_assets(); +void test_root_template_renders_core_placeholders(); +void test_wifi_template_renders_scan_container_and_form_fields(); +void test_message_template_renders_body_and_actions(); +void test_param_template_renders_shared_actions(); +void test_info_template_renders_sections_and_footer(); +void test_fragment_action_form_renders_shared_button_markup(); +void test_fragment_info_row_and_section_render(); +void test_fragment_centered_button_supports_extra_attrs(); // State transition tests void test_portal_to_connected_transition(); diff --git a/test/test_wifimanager/tests/test_configuration.cpp b/test/test_wifimanager/tests/test_configuration.cpp index 4923418..ac0d550 100644 --- a/test/test_wifimanager/tests/test_configuration.cpp +++ b/test/test_wifimanager/tests/test_configuration.cpp @@ -65,17 +65,18 @@ void test_set_and_get_hostname() { WiFiManager wm; - // Test with char* - wm.setHostname("test-hostname"); - String hostname1 = wm.getWiFiHostname(); - // Verify hostname was set (may be empty initially, but getter doesn't crash) - TEST_ASSERT_TRUE_MESSAGE(true, "setHostname(char*) and getWiFiHostname() executed"); - - // Test with String - wm.setHostname(String("test-hostname-2")); - String hostname2 = wm.getWiFiHostname(); - // Verify getter works - TEST_ASSERT_TRUE_MESSAGE(true, "setHostname(String) and getWiFiHostname() executed"); + TEST_ASSERT_TRUE_MESSAGE(wm.setHostname("test-hostname"), "Valid char* hostname should be accepted"); + TEST_ASSERT_TRUE_MESSAGE(wm.setHostname(String(" trimmed-hostname ")), "Valid String hostname should be trimmed and accepted"); + TEST_ASSERT_TRUE_MESSAGE(wm.setHostname(""), "Empty hostname should clear the configured hostname"); + + String tooLongHostname = "123456789012345678901234567890123"; + TEST_ASSERT_FALSE_MESSAGE(wm.setHostname(tooLongHostname), "Hostnames longer than 32 chars should be rejected"); + TEST_ASSERT_FALSE_MESSAGE(wm.setHostname("bad host"), "Hostnames with spaces should be rejected"); + TEST_ASSERT_FALSE_MESSAGE(wm.setHostname("-leading"), "Hostnames cannot start with a hyphen"); + TEST_ASSERT_FALSE_MESSAGE(wm.setHostname("trailing-"), "Hostnames cannot end with a hyphen"); + + (void)wm.getWiFiHostname(); + TEST_ASSERT_TRUE_MESSAGE(true, "getWiFiHostname() executed after hostname validation checks"); Serial.println("[TEST] setHostname() and getWiFiHostname() test completed successfully"); } diff --git a/test/test_wifimanager/tests/test_template_rendering.cpp b/test/test_wifimanager/tests/test_template_rendering.cpp new file mode 100644 index 0000000..de068d3 --- /dev/null +++ b/test/test_wifimanager/tests/test_template_rendering.cpp @@ -0,0 +1,251 @@ +#include +#include +#include +#include "templates/Fragments.h" +#include "templates/Info.h" +#include "templates/Param.h" +#include "templates/PageShell.h" +#include "templates/Root.h" +#include "templates/WiFi.h" +#include "templates/Message.h" +#include "../test_main.h" + +namespace { + +String renderTemplate(const char* templateData, PlaceholderRegistry& registry) { + TemplateContext context; + context.setRegistry(®istry); + TemplateRenderer::initializeContext(context, templateData); + + uint8_t buffer[128]; + String output; + while (true) { + size_t written = TemplateRenderer::renderNextChunk(context, buffer, sizeof(buffer)); + if (written == 0) { + break; + } + output.concat(reinterpret_cast(buffer), written); + } + return output; +} + +const char kEmpty[] PROGMEM = ""; +const char kDocTitle[] PROGMEM = "Config ESP"; +const char kBodyClass[] PROGMEM = "wifi"; +const char kPageScripts[] PROGMEM = ""; +const char kPageStyles[] PROGMEM = ""; +const char kPageContent[] PROGMEM = "
Body
"; +const char kScanContent[] PROGMEM = "Scanning for networks...

"; +const char kSsidPlaceholder[] PROGMEM = "Office"; +const char kPasswordPlaceholder[] PROGMEM = "********"; +const char kStaticFields[] PROGMEM = "

"; +const char kParamSection[] PROGMEM = "

"; +const char kFormActions[] PROGMEM = "

"; +const char kPageActions[] PROGMEM = "


"; +const char kMessageActions[] PROGMEM = "

"; +const char kStatus[] PROGMEM = "
Status
"; +const char kMessageBody[] PROGMEM = "
Saved
"; +const char kInfoSection[] PROGMEM = "

About


Build date
today
"; +const char kInfoFooter[] PROGMEM = "

"; +const char kPageTitle[] PROGMEM = "WiFiManager"; +const char kSubtitle[] PROGMEM = "Setup"; +const char kMenu[] PROGMEM = "
"; +const char kActionPrefix[] PROGMEM = "

"; +const char kActionPath[] PROGMEM = "/update"; +const char kActionMethod[] PROGMEM = "get"; +const char kActionLabel[] PROGMEM = "Update"; +const char kActionClassAttr[] PROGMEM = " class='D'"; +const char kActionSuffix[] PROGMEM = "
\n"; +const char kButtonId[] PROGMEM = "refresh-btn"; +const char kButtonType[] PROGMEM = "button"; +const char kButtonOnClick[] PROGMEM = "return refreshScan()"; +const char kButtonExtraAttrs[] PROGMEM = " data-skip-initial-scan='true'"; +const char kInfoLabel[] PROGMEM = "SDK version"; +const char kInfoValue[] PROGMEM = "3.1.2"; +const char kSectionTitle[] PROGMEM = "Device"; +const char kSectionRows[] PROGMEM = "
Chip ID
abc
"; + +} // namespace + +void test_page_shell_supports_page_assets() { + Serial.println("[TEST] Testing page shell page-scoped assets..."); + + PlaceholderRegistry registry(12); + registry.registerProgmemData("%SCRIPTS%", kEmpty); + registry.registerProgmemData("%PAGE_SCRIPTS%", kPageScripts); + registry.registerProgmemData("%STYLES%", kEmpty); + registry.registerProgmemData("%PAGE_STYLES%", kPageStyles); + registry.registerProgmemData("%DOC_TITLE%", kDocTitle); + registry.registerProgmemData("%BODY_CLASS%", kBodyClass); + registry.registerProgmemData("%PAGE_CONTENT%", kPageContent); + + String output = renderTemplate(WM_PAGE_SHELL_TEMPLATE, registry); + + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("wifi page")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(".wifi{color:#000;}")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("
Body
")); + + Serial.println("[TEST] Page shell page-scoped assets test completed successfully"); +} + +void test_wifi_template_renders_scan_container_and_form_fields() { + Serial.println("[TEST] Testing WiFi page template rendering..."); + + PlaceholderRegistry registry(16); + registry.registerProgmemData("%WIFI_SCAN_CONTENT%", kScanContent); + registry.registerProgmemData("%WIFI_SSID_PLACEHOLDER%", kSsidPlaceholder); + registry.registerProgmemData("%WIFI_PASSWORD_PLACEHOLDER%", kPasswordPlaceholder); + registry.registerProgmemData("%WIFI_STATIC_FIELDS%", kStaticFields); + registry.registerProgmemData("%WIFI_PARAM_SECTION%", kParamSection); + registry.registerProgmemData("%WIFI_FORM_ACTIONS%", kFormActions); + registry.registerProgmemData("%WIFI_PAGE_ACTIONS%", kPageActions); + registry.registerProgmemData("%WIFI_STATUS%", kStatus); + + String output = renderTemplate(WM_WIFI_CONTENT_TEMPLATE, registry); + + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='scan-results'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Scanning for networks...")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("placeholder='Office'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='refresh-btn'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Token")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Save")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back")); + + Serial.println("[TEST] WiFi page template rendering test completed successfully"); +} + +void test_message_template_renders_body_and_actions() { + Serial.println("[TEST] Testing message template rendering..."); + + PlaceholderRegistry registry(8); + registry.registerProgmemData("%MESSAGE_BODY%", kMessageBody); + registry.registerProgmemData("%MESSAGE_ACTIONS%", kMessageActions); + + String output = renderTemplate(WM_MESSAGE_CONTENT_TEMPLATE, registry); + + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Saved")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back")); + + Serial.println("[TEST] Message template rendering test completed successfully"); +} + +void test_param_template_renders_shared_actions() { + Serial.println("[TEST] Testing param page template rendering..."); + + PlaceholderRegistry registry(8); + registry.registerProgmemData("%PARAM_FIELDS%", kParamSection); + registry.registerProgmemData("%PARAM_FORM_ACTIONS%", kFormActions); + registry.registerProgmemData("%PARAM_PAGE_ACTIONS%", kMessageActions); + registry.registerProgmemData("%PARAM_STATUS%", kStatus); + + String output = renderTemplate(WM_PARAM_CONTENT_TEMPLATE, registry); + + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Token")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Save")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status")); + + Serial.println("[TEST] Param page template rendering test completed successfully"); +} + +void test_info_template_renders_sections_and_footer() { + Serial.println("[TEST] Testing info page template rendering..."); + + PlaceholderRegistry registry(8); + registry.registerProgmemData("%INFO_STATUS%", kStatus); + registry.registerProgmemData("%INFO_DEVICE_SECTION%", kInfoSection); + registry.registerProgmemData("%INFO_WIFI_SECTION%", kInfoSection); + registry.registerProgmemData("%INFO_ABOUT_SECTION%", kInfoSection); + registry.registerProgmemData("%INFO_FOOTER%", kInfoFooter); + + String output = renderTemplate(WM_INFO_CONTENT_TEMPLATE, registry); + + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Build date")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Update")); + + Serial.println("[TEST] Info page template rendering test completed successfully"); +} + +void test_fragment_action_form_renders_shared_button_markup() { + Serial.println("[TEST] Testing shared action fragment rendering..."); + + PlaceholderRegistry registry(8); + registry.registerProgmemData("%ACTION_PREFIX%", kActionPrefix); + registry.registerProgmemData("%ACTION%", kActionPath); + registry.registerProgmemData("%METHOD%", kActionMethod); + registry.registerProgmemData("%BUTTON_LABEL%", kActionLabel); + registry.registerProgmemData("%BUTTON_CLASS_ATTR%", kActionClassAttr); + registry.registerProgmemData("%ACTION_SUFFIX%", kActionSuffix); + + String output = renderTemplate(WM_ACTION_FORM_TEMPLATE, registry); + + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("action='/update'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("class='D'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(">Update")); + + Serial.println("[TEST] Shared action fragment rendering test completed successfully"); +} + +void test_fragment_info_row_and_section_render() { + Serial.println("[TEST] Testing shared info fragment rendering..."); + + PlaceholderRegistry rowRegistry(4); + rowRegistry.registerProgmemData("%INFO_LABEL%", kInfoLabel); + rowRegistry.registerProgmemData("%INFO_VALUE%", kInfoValue); + + String rowOutput = renderTemplate(WM_INFO_ROW_TEMPLATE, rowRegistry); + TEST_ASSERT_NOT_EQUAL(-1, rowOutput.indexOf("
SDK version
3.1.2
")); + + PlaceholderRegistry sectionRegistry(6); + sectionRegistry.registerProgmemData("%SECTION_PREFIX%", kEmpty); + sectionRegistry.registerProgmemData("%SECTION_TITLE%", kSectionTitle); + sectionRegistry.registerProgmemData("%SECTION_ROWS%", kSectionRows); + sectionRegistry.registerProgmemData("%SECTION_SUFFIX%", kActionSuffix); + + String sectionOutput = renderTemplate(WM_INFO_SECTION_TEMPLATE, sectionRegistry); + TEST_ASSERT_NOT_EQUAL(-1, sectionOutput.indexOf("

Device

")); + TEST_ASSERT_NOT_EQUAL(-1, sectionOutput.indexOf("
Chip ID
abc
")); + + Serial.println("[TEST] Shared info fragment rendering test completed successfully"); +} + +void test_fragment_centered_button_supports_extra_attrs() { + Serial.println("[TEST] Testing centered button fragment extra attrs..."); + + PlaceholderRegistry registry(8); + registry.registerProgmemData("%BUTTON_ID%", kButtonId); + registry.registerProgmemData("%BUTTON_TYPE%", kButtonType); + registry.registerProgmemData("%BUTTON_ONCLICK%", kButtonOnClick); + registry.registerProgmemData("%BUTTON_LABEL%", kActionLabel); + registry.registerProgmemData("%BUTTON_EXTRA_ATTRS%", kButtonExtraAttrs); + + String output = renderTemplate(WM_CENTERED_BUTTON_TEMPLATE, registry); + + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='refresh-btn'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("data-skip-initial-scan='true'")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(">Update")); + + Serial.println("[TEST] Centered button fragment extra attrs test completed successfully"); +} + +void test_root_template_renders_core_placeholders() { + Serial.println("[TEST] Testing root template rendering..."); + + PlaceholderRegistry registry(8); + registry.registerProgmemData("%SCRIPTS%", kEmpty); + registry.registerProgmemData("%STYLES%", kEmpty); + registry.registerProgmemData("%PAGE_TITLE%", kPageTitle); + registry.registerProgmemData("%SUBTITLE%", kSubtitle); + registry.registerProgmemData("%MENU%", kMenu); + registry.registerProgmemData("%STATUS%", kStatus); + + String output = renderTemplate(WM_ROOT_TEMPLATE, registry); + + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("WiFiManager")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Setup")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Configure WiFi")); + TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status")); + + Serial.println("[TEST] Root template rendering test completed successfully"); +} diff --git a/test/test_wifimanager/tests/test_wifi_scanning.cpp b/test/test_wifimanager/tests/test_wifi_scanning.cpp index 48c2347..6b70848 100644 --- a/test/test_wifimanager/tests/test_wifi_scanning.cpp +++ b/test/test_wifimanager/tests/test_wifi_scanning.cpp @@ -3,88 +3,150 @@ #include #include "../test_main.h" -// Test WiFi scan initiates void test_wifi_scan_initiates() { - Serial.println("[TEST] Testing WiFi scan initiation..."); - + Serial.println("[TEST] Testing async scan request queues..."); + WiFiManager wm; - - // Initiate scan (non-blocking) - unsigned long start = millis(); - (void)WiFi.scanNetworks(true); // true = async - unsigned long elapsed = millis() - start; - - // Scan initiation should return quickly (non-blocking) - TEST_ASSERT_LESS_THAN(100, elapsed); - - // Return value should be number of networks (if scan completed) or -1 (if async) - // For async scan, it returns -1 immediately - TEST_ASSERT_TRUE_MESSAGE(true, "WiFi scan initiated (return value may be -1 for async)"); - - Serial.println("[TEST] WiFi scan initiation test completed successfully"); + wm.requestAsyncScan(true); + + TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_QUEUED, wm.getScanState()); + TEST_ASSERT_TRUE_MESSAGE(wm.getScanSnapshot().schedulePending, + "Explicit refresh should mark scan scheduling as pending"); + TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_SCHEDULE_USER_REFRESH, + wm.getScanSnapshot().scheduledReason); + TEST_ASSERT_FALSE_MESSAGE(wm.hasValidScanResults(), + "Queueing a scan should invalidate previous cached results"); + + Serial.println("[TEST] Async scan request queues test completed successfully"); } -// Test async scan behavior void test_async_scan_behavior() { - Serial.println("[TEST] Testing async scan behavior..."); - + Serial.println("[TEST] Testing repeated scan requests coalesce..."); + WiFiManager wm; - - // Initiate async scan - unsigned long start = millis(); - (void)WiFi.scanNetworks(true); // true = async - unsigned long elapsed = millis() - start; - - // Should return immediately (non-blocking) - TEST_ASSERT_LESS_THAN(50, elapsed); - - // For async scan, result is -1 immediately - // Scan is in progress, status can be checked later - TEST_ASSERT_TRUE_MESSAGE(true, "Async scan returns immediately (non-blocking)"); - - Serial.println("[TEST] Async scan behavior test completed successfully"); + wm.requestAsyncScan(true); + wm.requestAsyncScan(true); + wm.requestAsyncScan(true); + + TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_QUEUED, wm.getScanState()); + TEST_ASSERT_TRUE_MESSAGE(wm.getScanSnapshot().schedulePending, + "Repeated refresh clicks should still leave one pending schedule"); + TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_SCHEDULE_USER_REFRESH, + wm.getScanSnapshot().scheduledReason); + TEST_ASSERT_FALSE_MESSAGE(wm.hasValidScanResults(), + "Repeated refresh requests should keep a single queued scan lifecycle"); + + Serial.println("[TEST] Repeated scan requests coalesce test completed successfully"); } -// Test scan status checking (non-blocking) void test_scan_status_checking() { - Serial.println("[TEST] Testing scan status checking..."); - + Serial.println("[TEST] Testing cached scan snapshot state..."); + WiFiManager wm; - - // Initiate scan - WiFi.scanNetworks(true); // async - - // Check scan status (should be able to check without blocking) - (void)WiFi.scanComplete(); - - // Status may be -1 (scanning), -2 (not started), or >= 0 (number of networks) - // Just verify we can check status without blocking - TEST_ASSERT_TRUE_MESSAGE(true, "Scan status can be checked without blocking"); - - Serial.println("[TEST] Scan status checking test completed successfully"); + +#ifdef UNIT_TEST + std::vector results = { + { "Office", -48, 1 }, + { "Guest", -70, 0 } + }; + wm.wmTestInjectScanResults(results); + + TEST_ASSERT_TRUE_MESSAGE(wm.hasValidScanResults(), + "Injected scan snapshot should be marked valid"); + TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_COMPLETE, wm.getScanState()); + TEST_ASSERT_EQUAL(static_cast(2), wm.getScanResults().size()); + TEST_ASSERT_EQUAL_STRING("Office", wm.getScanResults()[0].ssid.c_str()); +#else + TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable"); +#endif + + Serial.println("[TEST] Cached scan snapshot state test completed successfully"); } -// Test scan completion wait void test_scan_completion_wait() { - Serial.println("[TEST] Testing scan completion wait..."); - + Serial.println("[TEST] Testing scan timeout transition..."); + WiFiManager wm; - - // Initiate scan - WiFi.scanNetworks(true); // async - - // Wait for scan to complete (with timeout) - unsigned long start = millis(); - int scanStatus = -1; - while (scanStatus < 0 && (millis() - start < 10000)) { // 10 second timeout - delay(100); - scanStatus = WiFi.scanComplete(); - } - - // After wait, status should be >= 0 (completed) or still -1 (timeout) - // Just verify we can wait for completion - TEST_ASSERT_TRUE_MESSAGE(true, "Scan completion can be waited for"); - - Serial.println("[TEST] Scan completion wait test completed successfully"); + +#ifdef UNIT_TEST + wm.wmTestSetPortalActive(true); + wm.wmTestForceScanState(WiFiManager::WM_SCAN_RUNNING); + wm.wmTestSetScanStartedAt(millis() - 20000); + wm.wmTestSetScanTimeoutMs(1000); + wm.process(); + + TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_TIMEOUT, wm.getScanState()); + TEST_ASSERT_FALSE_MESSAGE(wm.hasValidScanResults(), + "Timed out scans should not leave cached results marked valid"); +#else + TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable"); +#endif + + Serial.println("[TEST] Scan timeout transition test completed successfully"); +} + +void test_scan_cancels_when_connect_pending() { + Serial.println("[TEST] Testing scan cancellation during connect..."); + + WiFiManager wm; + +#ifdef UNIT_TEST + wm.wmTestSetPortalActive(true); + wm.wmTestForceScanState(WiFiManager::WM_SCAN_RUNNING); + wm.wmTestSetScanStartedAt(millis()); + wm.wmTestSetConnectPending(true); + wm.process(); + + TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_IDLE, wm.getScanState()); + TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().schedulePending, + "Connect flow should cancel any in-flight async scan"); +#else + TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable"); +#endif + + Serial.println("[TEST] Scan cancellation during connect test completed successfully"); +} + +void test_scan_cancels_when_lifecycle_blocked() { + Serial.println("[TEST] Testing scan cancellation during lifecycle block..."); + + WiFiManager wm; + +#ifdef UNIT_TEST + wm.wmTestSetPortalActive(true); + wm.requestAsyncScan(true); + wm.wmTestSetScanLifecycleBlocked(true); + wm.process(); + + TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_IDLE, wm.getScanState()); + TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().schedulePending, + "Lifecycle blocking should leave scan engine idle"); +#else + TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable"); +#endif + + Serial.println("[TEST] Scan cancellation during lifecycle block test completed successfully"); +} + +void test_scan_generation_invalidated_on_reset() { + Serial.println("[TEST] Testing scan generation invalidation on reset..."); + + WiFiManager wm; + +#ifdef UNIT_TEST + wm.wmTestSetScanGenerations(4, 4, 4); + wm.wmTestSetScanCompletionPending(3); + wm.wmTestClearScanResults(); + + TEST_ASSERT_EQUAL(WiFiManager::WM_SCAN_IDLE, wm.getScanState()); + TEST_ASSERT_TRUE_MESSAGE(wm.getScanSnapshot().generation > 4, + "Resetting scan state should invalidate older completion generations"); + TEST_ASSERT_FALSE_MESSAGE(wm.getScanSnapshot().completionPending, + "Resetting scan state should clear pending completion callbacks"); +#else + TEST_ASSERT_TRUE_MESSAGE(true, "UNIT_TEST helpers unavailable"); +#endif + + Serial.println("[TEST] Scan generation invalidation on reset test completed successfully"); }