mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
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.
This commit is contained in:
@@ -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<WiFiScanNetwork>& 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<WiFiScanNetwork> _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<WiFiScanNetwork>& results) {
|
||||
_scanResultsCache = results;
|
||||
_numNetworks = static_cast<int>(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);
|
||||
|
||||
@@ -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<const WiFiManager::WiFiScanNetwork*>& networks);
|
||||
void appendVisibleScanResultsJson(String& json, const std::vector<const WiFiManager::WiFiScanNetwork*>& networks);
|
||||
};
|
||||
|
||||
#endif // defined(ESP8266) || defined(ESP32)
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 <Arduino.h>
|
||||
|
||||
const char WM_ACTION_FORM_TEMPLATE[] PROGMEM =
|
||||
"%ACTION_PREFIX%<form action='%ACTION%' method='%METHOD%'><button%BUTTON_CLASS_ATTR%>%BUTTON_LABEL%</button></form>%ACTION_SUFFIX%";
|
||||
|
||||
const char WM_SUBMIT_BUTTON_TEMPLATE[] PROGMEM =
|
||||
"<br/><br/><button type='submit'>%BUTTON_LABEL%</button>";
|
||||
|
||||
const char WM_CENTERED_BUTTON_TEMPLATE[] PROGMEM =
|
||||
"<br/><div class='c'><button id='%BUTTON_ID%' type='%BUTTON_TYPE%' onclick='%BUTTON_ONCLICK%'%BUTTON_EXTRA_ATTRS%>%BUTTON_LABEL%</button></div>";
|
||||
|
||||
const char WM_SECTION_BREAK_TEMPLATE[] PROGMEM =
|
||||
"<hr><br/>%SECTION_CONTENT%";
|
||||
|
||||
const char WM_SCAN_MESSAGE_TEMPLATE[] PROGMEM =
|
||||
"%SCAN_MESSAGE%<br/><br/>";
|
||||
|
||||
const char WM_SCAN_RESULT_ROW_TEMPLATE[] PROGMEM =
|
||||
"<div><a href='#p' onclick='c(this)' data-ssid='%SSID_ATTR%'>%SSID_TEXT%</a>"
|
||||
"<div role='img' aria-label='%QUALITY_LABEL%' title='%QUALITY_LABEL%' class='q q-%QUALITY_ICON% %LOCK_CLASS% %ICON_VISIBILITY_CLASS%'></div>"
|
||||
"<div class='q %VALUE_VISIBILITY_CLASS%'>%QUALITY_VALUE%</div></div>";
|
||||
|
||||
const char WM_FIELD_LABEL_BEFORE_TEMPLATE[] PROGMEM =
|
||||
"<label for='%FIELD_ID%'>%FIELD_LABEL%</label><br/><input id='%FIELD_ID%' name='%FIELD_NAME%' maxlength='%FIELD_MAXLENGTH%' value='%FIELD_VALUE%'%FIELD_EXTRA_ATTRS%>";
|
||||
|
||||
const char WM_FIELD_LABEL_AFTER_TEMPLATE[] PROGMEM =
|
||||
"<br/><input id='%FIELD_ID%' name='%FIELD_NAME%' maxlength='%FIELD_MAXLENGTH%' value='%FIELD_VALUE%'%FIELD_EXTRA_ATTRS%>"
|
||||
"<label for='%FIELD_ID%'>%FIELD_LABEL%</label>";
|
||||
|
||||
const char WM_FIELD_INPUT_ONLY_TEMPLATE[] PROGMEM =
|
||||
"<br/><input id='%FIELD_ID%' name='%FIELD_NAME%' maxlength='%FIELD_MAXLENGTH%' value='%FIELD_VALUE%'%FIELD_EXTRA_ATTRS%>";
|
||||
|
||||
const char WM_INFO_ROW_TEMPLATE[] PROGMEM =
|
||||
"<dt>%INFO_LABEL%</dt><dd>%INFO_VALUE%</dd>";
|
||||
|
||||
const char WM_INFO_SECTION_TEMPLATE[] PROGMEM =
|
||||
"%SECTION_PREFIX%<h3>%SECTION_TITLE%</h3><hr><dl>%SECTION_ROWS%</dl>%SECTION_SUFFIX%";
|
||||
|
||||
const char WM_STATUS_MESSAGE_TEMPLATE[] PROGMEM =
|
||||
"<div class='msg%STATUS_CLASS_SUFFIX%'><strong>%STATUS_TITLE%</strong>%STATUS_BODY%</div>";
|
||||
|
||||
const char WM_PAGE_HEADING_TEMPLATE[] PROGMEM =
|
||||
"<h1>%HEADER_TITLE%</h1><h3>%HEADER_SUBTITLE%</h3>";
|
||||
|
||||
#endif // _WM_FRAGMENTS_TEMPLATE_H_
|
||||
@@ -9,39 +9,6 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// HTML document structure
|
||||
const char HTML_HEAD_START[] PROGMEM =
|
||||
"<!DOCTYPE html>"
|
||||
"<html lang='en'><head>"
|
||||
"<meta name='format-detection' content='telephone=no'>"
|
||||
"<meta charset='UTF-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
||||
"<title>";
|
||||
|
||||
const char HTML_TITLE_END[] PROGMEM = "</title>";
|
||||
|
||||
const char HTML_HEAD_END_START[] PROGMEM = "</head><body class='";
|
||||
|
||||
const char HTML_HEAD_END_WRAP[] PROGMEM = "'><div class='wrap'>";
|
||||
|
||||
const char HTML_END[] PROGMEM = "</div></body></html>";
|
||||
|
||||
// Form elements
|
||||
const char HTML_FORM_END[] PROGMEM = "<br/><br/><button type='submit'>Save</button></form>";
|
||||
|
||||
const char HTML_FORM_STATIC_HEAD[] PROGMEM = "<hr><br/>";
|
||||
|
||||
const char HTML_FORM_PARAM_HEAD[] PROGMEM = "<hr><br/>";
|
||||
|
||||
const char HTML_FORM_WIFI_END[] PROGMEM = "";
|
||||
|
||||
const char HTML_BR[] PROGMEM = "<br/>";
|
||||
|
||||
// Buttons
|
||||
const char HTML_BACKBTN[] PROGMEM = "<hr><br/><form action='/' method='get'><button>Back</button></form>";
|
||||
|
||||
const char HTML_ERASEBTN[] PROGMEM = "<br/><form action='/erase' method='get'><button class='D'>Erase WiFi config</button></form>";
|
||||
|
||||
// Status messages
|
||||
const char HTML_STATUS_OFFPW[] PROGMEM = "<br/>Authentication failure";
|
||||
|
||||
@@ -62,22 +29,6 @@ const char HTML_UPDATE_FAIL[] PROGMEM = "<div class='msg D'><strong>Update faile
|
||||
|
||||
const char HTML_UPDATE_SUCCESS[] PROGMEM = "<div class='msg S'><strong>Update successful. </strong> <br/> Device rebooting now...</div>";
|
||||
|
||||
// Portal menu
|
||||
const char * const HTML_PORTAL_MENU[] PROGMEM = {
|
||||
"<form action='/wifi' method='get'><button>Configure WiFi</button></form><br/>\n", // MENU_WIFI
|
||||
"<form action='/0wifi' method='get'><button>Configure WiFi (No scan)</button></form><br/>\n", // MENU_WIFINOSCAN
|
||||
"<form action='/info' method='get'><button>Info</button></form><br/>\n", // MENU_INFO
|
||||
"<form action='/param' method='get'><button>Setup</button></form><br/>\n",//MENU_PARAM
|
||||
"<form action='/close' method='get'><button>Close</button></form><br/>\n", // MENU_CLOSE
|
||||
"<form action='/restart' method='get'><button>Restart</button></form><br/>\n",// MENU_RESTART
|
||||
"<form action='/exit' method='get'><button>Exit</button></form><br/>\n", // MENU_EXIT
|
||||
"<form action='/erase' method='get'><button class='D'>Erase</button></form><br/>\n", // MENU_ERASE
|
||||
"<form action='/update' method='get'><button>Update</button></form><br/>\n",// MENU_UPDATE
|
||||
"<hr><br/>" // 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 =
|
||||
"<tr><td><a href='/'>/</a></td>"
|
||||
"<td>Menu page.</td></tr>"
|
||||
"<tr><td><a href='/wifi'>/wifi</a></td>"
|
||||
"<td>Show WiFi scan results and enter WiFi configuration.(/0wifi noscan)</td></tr>"
|
||||
"<td>Show WiFi scan results and enter WiFi configuration.</td></tr>"
|
||||
"<tr><td><a href='/0wifi'>/0wifi</a></td>"
|
||||
"<td>Show WiFi configuration without loading scan results until refresh is requested.</td></tr>"
|
||||
"<tr><td><a href='/wifisave'>/wifisave</a></td>"
|
||||
"<td>Save WiFi configuration information and configure device. Needs variables supplied.</td></tr>"
|
||||
"<tr><td><a href='/param'>/param</a></td>"
|
||||
"<td>Parameter page</td></tr>"
|
||||
"<tr><td><a href='/info'>/info</a></td>"
|
||||
"<td>Information page</td></tr>"
|
||||
"<tr><td><a href='/u'>/u</a></td>"
|
||||
"<td>OTA Update</td></tr>"
|
||||
"<tr><td><a href='/update'>/update</a></td>"
|
||||
"<td>OTA update page. Firmware upload posts to /u.</td></tr>"
|
||||
"<tr><td><a href='/close'>/close</a></td>"
|
||||
"<td>Close the captiveportal popup, config portal will remain active</td></tr>"
|
||||
"<tr><td>/exit</td>"
|
||||
@@ -110,25 +63,5 @@ const char HTML_HELP[] PROGMEM =
|
||||
const char HTML_HELP[] PROGMEM = "";
|
||||
#endif
|
||||
|
||||
#ifdef WM_JSTEST
|
||||
const char HTML_JS[] PROGMEM =
|
||||
"<script>function postAjax(url, data, success) {"
|
||||
" var params = typeof data == 'string' ? data : Object.keys(data).map("
|
||||
" function(k){ return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]) }"
|
||||
" ).join('&');"
|
||||
" var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject(\"Microsoft.XMLHTTP\");"
|
||||
" xhr.open('POST', url);"
|
||||
" xhr.onreadystatechange = function() {"
|
||||
" if (xhr.readyState>3 && xhr.status==200) { success(xhr.responseText); }"
|
||||
" };"
|
||||
" xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');"
|
||||
" xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');"
|
||||
" xhr.send(params);"
|
||||
" return xhr;}"
|
||||
"postAjax('/status', 'p1=1&p2=Hello+World', function(data){ console.log(data); });"
|
||||
"postAjax('/status', { p1: 1, p2: 'Hello World' }, function(data){ console.log(data); });"
|
||||
"</script>";
|
||||
#endif
|
||||
|
||||
#endif // _HTML_TEMPLATES_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 <Arduino.h>
|
||||
|
||||
const char WM_INFO_TEMPLATE[] PROGMEM =
|
||||
"<!DOCTYPE html>"
|
||||
"<html lang='en'>"
|
||||
"<head>"
|
||||
"<meta name='format-detection' content='telephone=no'>"
|
||||
"<meta charset='UTF-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
||||
"<title>%DOC_TITLE%</title>"
|
||||
"%SCRIPTS%"
|
||||
"%STYLES%"
|
||||
"</head>"
|
||||
"<body class='info'>"
|
||||
"<div class='wrap'>"
|
||||
const char WM_INFO_CONTENT_TEMPLATE[] PROGMEM =
|
||||
"%INFO_STATUS%"
|
||||
"%INFO_DEVICE_SECTION%"
|
||||
"%INFO_WIFI_SECTION%"
|
||||
"%INFO_ABOUT_SECTION%"
|
||||
"</div>"
|
||||
"</body>"
|
||||
"</html>";
|
||||
"<section class='info-device'>%INFO_DEVICE_SECTION%</section>"
|
||||
"<section class='info-wifi'>%INFO_WIFI_SECTION%</section>"
|
||||
"<section class='info-about'>%INFO_ABOUT_SECTION%</section>"
|
||||
"%INFO_FOOTER%";
|
||||
|
||||
#endif // _WM_INFO_TEMPLATE_H_
|
||||
|
||||
@@ -9,16 +9,40 @@
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char JS_SCRIPT[] PROGMEM = "<script>function c(l){"
|
||||
"document.getElementById('s').value=l.getAttribute('data-ssid')||l.innerText||l.textContent;"
|
||||
"var parent = l.parentElement;"
|
||||
"var isEncrypted = parent.querySelector('.l') !== null || (l.nextElementSibling && l.nextElementSibling.classList.contains('l'));"
|
||||
"var pField = document.getElementById('p');"
|
||||
"if(isEncrypted){pField.removeAttribute('disabled');pField.focus();}else{pField.setAttribute('disabled','');};"
|
||||
"};"
|
||||
"function f() {var x = document.getElementById('p');x.type==='password'?x.type='text':x.type='password';}"
|
||||
"window.addEventListener('load',function(){var pField = document.getElementById('p');pField.removeAttribute('disabled');});"
|
||||
"</script>"; // @todo add button states, disable on click , show ack , spinner etc
|
||||
const char JS_SCRIPT[] PROGMEM = R"rawliteral(
|
||||
<script>
|
||||
function c(l){
|
||||
var ssidField = document.getElementById('s');
|
||||
if (!ssidField) return;
|
||||
ssidField.value = l.getAttribute('data-ssid') || l.innerText || l.textContent;
|
||||
|
||||
var parent = l.parentElement;
|
||||
var isEncrypted = parent.querySelector('.l') !== null || (l.nextElementSibling && l.nextElementSibling.classList.contains('l'));
|
||||
var pField = document.getElementById('p');
|
||||
if (!pField) return;
|
||||
|
||||
if (isEncrypted) {
|
||||
pField.removeAttribute('disabled');
|
||||
pField.focus();
|
||||
} else {
|
||||
pField.setAttribute('disabled', '');
|
||||
}
|
||||
}
|
||||
|
||||
function f() {
|
||||
var x = document.getElementById('p');
|
||||
if (!x) return;
|
||||
x.type = x.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
var pField = document.getElementById('p');
|
||||
if (pField) {
|
||||
pField.removeAttribute('disabled');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
)rawliteral";
|
||||
|
||||
#endif // _JS_TEMPLATES_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 <Arduino.h>
|
||||
|
||||
const char WM_MESSAGE_CONTENT_TEMPLATE[] PROGMEM =
|
||||
"%MESSAGE_BODY%"
|
||||
"%MESSAGE_ACTIONS%";
|
||||
|
||||
#endif // _WM_MESSAGE_TEMPLATE_H_
|
||||
@@ -17,7 +17,9 @@ const char WM_PAGE_SHELL_TEMPLATE[] PROGMEM =
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
||||
"<title>%DOC_TITLE%</title>"
|
||||
"%SCRIPTS%"
|
||||
"%PAGE_SCRIPTS%"
|
||||
"%STYLES%"
|
||||
"%PAGE_STYLES%"
|
||||
"</head>"
|
||||
"<body class='%BODY_CLASS%'>"
|
||||
"<div class='wrap'>"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Param.h
|
||||
* Streamed setup page content template for WiFiManager.
|
||||
*/
|
||||
|
||||
#ifndef _WM_PARAM_TEMPLATE_H_
|
||||
#define _WM_PARAM_TEMPLATE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char WM_PARAM_CONTENT_TEMPLATE[] PROGMEM =
|
||||
"<form method='POST' action='paramsave'>"
|
||||
"%PARAM_FIELDS%"
|
||||
"%PARAM_FORM_ACTIONS%</form>"
|
||||
"%PARAM_PAGE_ACTIONS%"
|
||||
"%PARAM_STATUS%";
|
||||
|
||||
#endif // _WM_PARAM_TEMPLATE_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 <Arduino.h>
|
||||
|
||||
const char WM_WIFI_TEMPLATE[] PROGMEM =
|
||||
"<!DOCTYPE html>"
|
||||
"<html lang='en'>"
|
||||
"<head>"
|
||||
"<meta name='format-detection' content='telephone=no'>"
|
||||
"<meta charset='UTF-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
||||
"<title>%DOC_TITLE%</title>"
|
||||
"%SCRIPTS%"
|
||||
"%STYLES%"
|
||||
"</head>"
|
||||
"<body class='wifi'>"
|
||||
"<div class='wrap'>"
|
||||
"%WIFI_SCAN_RESULTS%"
|
||||
"%WIFI_FORM_SECTION%"
|
||||
"%WIFI_BACK_SECTION%"
|
||||
"%WIFI_STATUS%"
|
||||
"</div>"
|
||||
"</body>"
|
||||
"</html>";
|
||||
const char WM_WIFI_CONTENT_TEMPLATE[] PROGMEM =
|
||||
"<div id='scan-results'>%WIFI_SCAN_CONTENT%</div>"
|
||||
"<form method='POST' action='wifisave'>"
|
||||
"<label for='s'>SSID</label>"
|
||||
"<input id='s' name='s' maxlength='32' autocorrect='off' autocapitalize='none' placeholder='%WIFI_SSID_PLACEHOLDER%'>"
|
||||
"<br/>"
|
||||
"<label for='p'>Password</label>"
|
||||
"<input id='p' name='p' maxlength='64' type='password' placeholder='%WIFI_PASSWORD_PLACEHOLDER%'>"
|
||||
"<input type='checkbox' id='showpass' onclick='f()'> <label for='showpass'>Show Password</label><br/>"
|
||||
"%WIFI_STATIC_FIELDS%"
|
||||
"%WIFI_PARAM_SECTION%"
|
||||
"%WIFI_FORM_ACTIONS%</form>"
|
||||
"%WIFI_PAGE_ACTIONS%"
|
||||
"%WIFI_STATUS%";
|
||||
|
||||
#endif // _WM_WIFI_TEMPLATE_H_
|
||||
|
||||
@@ -12,49 +12,81 @@
|
||||
const char PROGMEM WIFI_POLLING_JS[] = R"rawliteral(
|
||||
<script>
|
||||
let scanPollInterval = null;
|
||||
let isPolling = false;
|
||||
let scanBusy = false;
|
||||
|
||||
function refreshScan() {
|
||||
// Trigger scan via refresh parameter
|
||||
fetch('/wifi?refresh=1', {method: 'GET'}).then(() => {
|
||||
function setRefreshButtonState(disabled, label) {
|
||||
const btn = document.getElementById('refresh-btn');
|
||||
if (!btn) return;
|
||||
btn.disabled = disabled;
|
||||
btn.textContent = label;
|
||||
}
|
||||
|
||||
function showScanMessage(message) {
|
||||
const scanResults = document.getElementById('scan-results');
|
||||
if (!scanResults) return;
|
||||
scanResults.innerHTML = message + '<br/><br/>';
|
||||
}
|
||||
|
||||
async function refreshScan() {
|
||||
if (scanBusy) return false;
|
||||
scanBusy = true;
|
||||
setRefreshButtonState(true, 'Scanning...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/wifi/scan', { method: 'POST' });
|
||||
if (!response.ok) throw new Error('scan request failed');
|
||||
startPolling();
|
||||
});
|
||||
} catch (error) {
|
||||
scanBusy = false;
|
||||
setRefreshButtonState(false, 'Refresh');
|
||||
showScanMessage('Could not start scan. Try again.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if(isPolling) return;
|
||||
isPolling = true;
|
||||
document.getElementById('refresh-btn').disabled = true;
|
||||
document.getElementById('refresh-btn').textContent = 'Scanning...';
|
||||
if (scanPollInterval) return;
|
||||
updateScanStatus();
|
||||
scanPollInterval = setInterval(updateScanStatus, 1000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if(scanPollInterval) {
|
||||
if (scanPollInterval) {
|
||||
clearInterval(scanPollInterval);
|
||||
scanPollInterval = null;
|
||||
}
|
||||
isPolling = false;
|
||||
document.getElementById('refresh-btn').disabled = false;
|
||||
document.getElementById('refresh-btn').textContent = 'Refresh';
|
||||
scanBusy = false;
|
||||
setRefreshButtonState(false, 'Refresh');
|
||||
}
|
||||
|
||||
function updateScanStatus() {
|
||||
fetch('/wifistatus')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if(data.scanning) {
|
||||
// Still scanning, show status
|
||||
document.getElementById('scan-results').innerHTML = 'Scanning for networks...<br/><br/>';
|
||||
} else {
|
||||
// Scan complete, update network list
|
||||
stopPolling();
|
||||
updateNetworkList(data);
|
||||
if (data.scanning) {
|
||||
showScanMessage('Scanning for networks...');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.error === 'timeout') {
|
||||
showScanMessage('Scan timed out. Refresh to try again.');
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.error === 'failed') {
|
||||
showScanMessage('Scan failed. Refresh to try again.');
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
stopPolling();
|
||||
updateNetworkList(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error polling scan status:', error);
|
||||
showScanMessage('Scan status unavailable.');
|
||||
stopPolling();
|
||||
});
|
||||
}
|
||||
@@ -66,14 +98,11 @@ function updateNetworkList(data) {
|
||||
} else if(data.networks && data.networks.length > 0) {
|
||||
data.networks.forEach(function(network) {
|
||||
let qualityPercent = network.quality + '%';
|
||||
// Map quality 0-100 to icon level 1-4
|
||||
let qualityIcon = Math.round((network.quality / 100) * 3) + 1;
|
||||
if(qualityIcon < 1) qualityIcon = 1;
|
||||
if(qualityIcon > 4) qualityIcon = 4;
|
||||
// Encryption class: 'l' if encrypted, empty if open
|
||||
let encClass = network.enc_type !== 0 ? 'l' : '';
|
||||
let encClass = network.encrypted ? 'l' : '';
|
||||
let ssidEscaped = escapeHtml(network.ssid);
|
||||
// Match original template structure: icon div with quality + encryption class, then percentage div
|
||||
html += '<div><a href="#p" onclick="c(this)" data-ssid="' + ssidEscaped + '">' + ssidEscaped + '</a>';
|
||||
html += '<div role="img" aria-label="' + qualityPercent + '" title="' + qualityPercent + '" class="q q-' + qualityIcon + ' ' + encClass + '"></div>';
|
||||
html += '<div class="q">' + qualityPercent + '</div>';
|
||||
@@ -93,21 +122,43 @@ function escapeHtml(text) {
|
||||
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||
}
|
||||
|
||||
function getQualityIcon(quality) {
|
||||
if(quality >= 75) return '▂▄▆█';
|
||||
if(quality >= 50) return '▂▄▆▁';
|
||||
if(quality >= 25) return '▂▄▁▁';
|
||||
return '▂▁▁▁';
|
||||
}
|
||||
|
||||
// Check if scan is in progress on page load
|
||||
window.addEventListener('load', function() {
|
||||
const refreshButton = document.getElementById('refresh-btn');
|
||||
const skipInitialScan = refreshButton && refreshButton.dataset.skipInitialScan === 'true';
|
||||
const pField = document.getElementById('p');
|
||||
if (pField) {
|
||||
pField.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
if (skipInitialScan) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/wifistatus')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if(data.scanning) {
|
||||
if (data.scanning) {
|
||||
scanBusy = true;
|
||||
setRefreshButtonState(true, 'Scanning...');
|
||||
startPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.error === 'timeout') {
|
||||
showScanMessage('Scan timed out. Refresh to try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.error === 'failed') {
|
||||
showScanMessage('Scan failed. Refresh to try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
updateNetworkList(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading initial scan status:', error);
|
||||
showScanMessage('Scan status unavailable.');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
+401
-123
@@ -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<size_t>(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<int>(_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;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <TemplateEngine.h>
|
||||
#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<const char*>(buffer), written);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
const char kEmpty[] PROGMEM = "";
|
||||
const char kDocTitle[] PROGMEM = "Config ESP";
|
||||
const char kBodyClass[] PROGMEM = "wifi";
|
||||
const char kPageScripts[] PROGMEM = "<script>console.log('wifi page');</script>";
|
||||
const char kPageStyles[] PROGMEM = "<style>.wifi{color:#000;}</style>";
|
||||
const char kPageContent[] PROGMEM = "<div>Body</div>";
|
||||
const char kScanContent[] PROGMEM = "Scanning for networks...<br/><br/>";
|
||||
const char kSsidPlaceholder[] PROGMEM = "Office";
|
||||
const char kPasswordPlaceholder[] PROGMEM = "********";
|
||||
const char kStaticFields[] PROGMEM = "<hr><br/><label for='ip'>Static IP</label>";
|
||||
const char kParamSection[] PROGMEM = "<hr><br/><label for='param_0'>Token</label>";
|
||||
const char kFormActions[] PROGMEM = "<br/><br/><button type='submit'>Save</button>";
|
||||
const char kPageActions[] PROGMEM = "<br/><div class='c'><button id='refresh-btn' type='button' onclick='return refreshScan()'>Refresh</button></div><hr><br/><form action='/' method='get'><button>Back</button></form>";
|
||||
const char kMessageActions[] PROGMEM = "<hr><br/><form action='/' method='get'><button>Back</button></form>";
|
||||
const char kStatus[] PROGMEM = "<div class='msg'>Status</div>";
|
||||
const char kMessageBody[] PROGMEM = "<div class='msg S'>Saved</div>";
|
||||
const char kInfoSection[] PROGMEM = "<h3>About</h3><hr><dl><dt>Build date</dt><dd>today</dd></dl>";
|
||||
const char kInfoFooter[] PROGMEM = "<hr><br/><form action='/update' method='get'><button>Update</button></form>";
|
||||
const char kPageTitle[] PROGMEM = "WiFiManager";
|
||||
const char kSubtitle[] PROGMEM = "Setup";
|
||||
const char kMenu[] PROGMEM = "<form><button>Configure WiFi</button></form>";
|
||||
const char kActionPrefix[] PROGMEM = "<hr><br/>";
|
||||
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 = "<br/>\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 = "<dt>Chip ID</dt><dd>abc</dd>";
|
||||
|
||||
} // 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("<div>Body</div>"));
|
||||
|
||||
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</button>"));
|
||||
|
||||
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("<dt>SDK version</dt><dd>3.1.2</dd>"));
|
||||
|
||||
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("<h3>Device</h3>"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, sectionOutput.indexOf("<dt>Chip ID</dt><dd>abc</dd>"));
|
||||
|
||||
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</button>"));
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -3,88 +3,150 @@
|
||||
#include <WiFiManager.h>
|
||||
#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<WiFiManager::WiFiScanNetwork> 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<size_t>(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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user