mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
Merge branch 'device-framework' of github.com:alexhopeoconnor/WiFiManager into device-framework
This commit is contained in:
@@ -776,8 +776,6 @@ protected:
|
|||||||
String WiFi_psk(bool persistent = true) const;
|
String WiFi_psk(bool persistent = true) const;
|
||||||
bool WiFi_scanNetworks();
|
bool WiFi_scanNetworks();
|
||||||
bool WiFi_scanNetworks(bool force); // Always async - returns false if scan started but not complete
|
bool WiFi_scanNetworks(bool force); // Always async - returns false if scan started but not complete
|
||||||
bool WiFi_scanNetworks(bool force,bool async); // async parameter ignored - always async
|
|
||||||
bool WiFi_scanNetworks(unsigned int cachetime,bool async); // async parameter ignored - always async
|
|
||||||
bool WiFi_scanNetworks(unsigned int cachetime);
|
bool WiFi_scanNetworks(unsigned int cachetime);
|
||||||
void WiFi_scanComplete(int networksFound);
|
void WiFi_scanComplete(int networksFound);
|
||||||
bool WiFiSetCountry();
|
bool WiFiSetCountry();
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/**
|
||||||
|
* WiFiPollingJS.h
|
||||||
|
* JavaScript code for WiFi network scanning with AJAX polling
|
||||||
|
* This is embedded in the WiFi configuration page for async WiFi scanning
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _WIFI_POLLING_JS_H_
|
||||||
|
#define _WIFI_POLLING_JS_H_
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
const char PROGMEM WIFI_POLLING_JS[] = R"rawliteral(
|
||||||
|
<script>
|
||||||
|
let scanPollInterval = null;
|
||||||
|
let isPolling = false;
|
||||||
|
|
||||||
|
function refreshScan() {
|
||||||
|
// Trigger scan via refresh parameter
|
||||||
|
fetch('/wifi?refresh=1', {method: 'GET'}).then(() => {
|
||||||
|
startPolling();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
if(isPolling) return;
|
||||||
|
isPolling = true;
|
||||||
|
document.getElementById('refresh-btn').disabled = true;
|
||||||
|
document.getElementById('refresh-btn').textContent = 'Scanning...';
|
||||||
|
updateScanStatus();
|
||||||
|
scanPollInterval = setInterval(updateScanStatus, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if(scanPollInterval) {
|
||||||
|
clearInterval(scanPollInterval);
|
||||||
|
scanPollInterval = null;
|
||||||
|
}
|
||||||
|
isPolling = false;
|
||||||
|
document.getElementById('refresh-btn').disabled = false;
|
||||||
|
document.getElementById('refresh-btn').textContent = '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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error polling scan status:', error);
|
||||||
|
stopPolling();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNetworkList(data) {
|
||||||
|
let html = '';
|
||||||
|
if(data.count === 0) {
|
||||||
|
html = 'No networks found. Refresh to scan again.<br/><br/>';
|
||||||
|
} 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 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>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('scan-results').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
var map = {};
|
||||||
|
map['&'] = '&';
|
||||||
|
map['<'] = '<';
|
||||||
|
map['>'] = '>';
|
||||||
|
map['"'] = '"';
|
||||||
|
map["'"] = ''';
|
||||||
|
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() {
|
||||||
|
fetch('/wifistatus')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if(data.scanning) {
|
||||||
|
startPolling();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
)rawliteral";
|
||||||
|
|
||||||
|
#endif // _WIFI_POLLING_JS_H_
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ const char R_exit[] PROGMEM = "/exit";
|
|||||||
const char R_close[] PROGMEM = "/close";
|
const char R_close[] PROGMEM = "/close";
|
||||||
const char R_erase[] PROGMEM = "/erase";
|
const char R_erase[] PROGMEM = "/erase";
|
||||||
const char R_status[] PROGMEM = "/status";
|
const char R_status[] PROGMEM = "/status";
|
||||||
const char R_scanstatus[] PROGMEM = "/wifi/scanstatus";
|
const char R_scanstatus[] PROGMEM = "/wifistatus";
|
||||||
const char R_update[] PROGMEM = "/update";
|
const char R_update[] PROGMEM = "/update";
|
||||||
const char R_updatedone[] PROGMEM = "/u";
|
const char R_updatedone[] PROGMEM = "/u";
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,13 @@ const char HTTP_HEAD_START[] PROGMEM = "<!DOCTYPE html>"
|
|||||||
|
|
||||||
const char HTTP_SCRIPT[] PROGMEM = "<script>function c(l){"
|
const char HTTP_SCRIPT[] PROGMEM = "<script>function c(l){"
|
||||||
"document.getElementById('s').value=l.getAttribute('data-ssid')||l.innerText||l.textContent;"
|
"document.getElementById('s').value=l.getAttribute('data-ssid')||l.innerText||l.textContent;"
|
||||||
"p = l.nextElementSibling.classList.contains('l');"
|
"var parent = l.parentElement;"
|
||||||
"document.getElementById('p').disabled = !p;"
|
"var isEncrypted = parent.querySelector('.l') !== null || (l.nextElementSibling && l.nextElementSibling.classList.contains('l'));"
|
||||||
"if(p)document.getElementById('p').focus();};"
|
"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';}"
|
"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
|
"</script>"; // @todo add button states, disable on click , show ack , spinner etc
|
||||||
|
|
||||||
const char HTTP_HEAD_END[] PROGMEM = "</head><body class='{c}'><div class='wrap'>"; // {c} = _bodyclass
|
const char HTTP_HEAD_END[] PROGMEM = "</head><body class='{c}'><div class='wrap'>"; // {c} = _bodyclass
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "WiFiManager.h"
|
#include "WiFiManager.h"
|
||||||
|
#include "templates/WiFiPollingJS.h"
|
||||||
|
|
||||||
#if defined(ESP8266) || defined(ESP32)
|
#if defined(ESP8266) || defined(ESP32)
|
||||||
|
|
||||||
@@ -1381,7 +1382,14 @@ void WiFiManager::handleRoot(AsyncWebServerRequest *request) {
|
|||||||
void WiFiManager::handleWifi(AsyncWebServerRequest *request, boolean scan) {
|
void WiFiManager::handleWifi(AsyncWebServerRequest *request, boolean scan) {
|
||||||
#ifdef WM_DEBUG_LEVEL
|
#ifdef WM_DEBUG_LEVEL
|
||||||
DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP Wifi"));
|
DEBUG_WM(WM_DEBUG_VERBOSE, F("<- HTTP Wifi"));
|
||||||
|
DEBUG_WM(WM_DEBUG_DEV, F("handleWifi called, scan="), scan ? "true" : "false");
|
||||||
#endif
|
#endif
|
||||||
|
if (captivePortal(request)) {
|
||||||
|
#ifdef WM_DEBUG_LEVEL
|
||||||
|
DEBUG_WM(WM_DEBUG_DEV, F("Captive portal redirect"));
|
||||||
|
#endif
|
||||||
|
return; // If captive portal redirect instead of displaying the page
|
||||||
|
}
|
||||||
handleRequest(request);
|
handleRequest(request);
|
||||||
String page = getHTTPHead(FPSTR(S_titlewifi), FPSTR(C_wifi)); // @token titlewifi
|
String page = getHTTPHead(FPSTR(S_titlewifi), FPSTR(C_wifi)); // @token titlewifi
|
||||||
if (scan) {
|
if (scan) {
|
||||||
@@ -1440,112 +1448,29 @@ void WiFiManager::handleWifi(AsyncWebServerRequest *request, boolean scan) {
|
|||||||
if(_showBack) page += FPSTR(HTTP_BACKBTN);
|
if(_showBack) page += FPSTR(HTTP_BACKBTN);
|
||||||
reportStatus(page);
|
reportStatus(page);
|
||||||
|
|
||||||
// Add JavaScript for AJAX polling
|
// TODO: Known issue - Adding WIFI_POLLING_JS causes silent failure (empty response)
|
||||||
page += F("<script>");
|
// This is likely due to:
|
||||||
page += F("let scanPollInterval = null;");
|
// 1. String size limit: Page is already ~9-10KB, adding ~2.5KB JS may exceed ESPAsyncWebServer limits
|
||||||
page += F("let isPolling = false;");
|
// 2. Memory fragmentation: Large String append operations on ESP8266 can cause heap fragmentation
|
||||||
page += F("");
|
// 3. PROGMEM access: FPSTR() on large PROGMEM strings may have timing issues with async operations
|
||||||
page += F("function refreshScan() {");
|
// 4. Response size: ESPAsyncWebServer may silently fail or truncate responses exceeding ~12KB
|
||||||
page += F(" // Trigger scan via refresh parameter");
|
// Workaround: Temporarily disabled until we can optimize the page size or use chunked responses
|
||||||
page += F(" fetch('/wifi?refresh=1', {method: 'GET'}).then(() => {");
|
// page += FPSTR(WIFI_POLLING_JS);
|
||||||
page += F(" startPolling();");
|
|
||||||
page += F(" });");
|
|
||||||
page += F("}");
|
|
||||||
page += F("");
|
|
||||||
page += F("function startPolling() {");
|
|
||||||
page += F(" if(isPolling) return;");
|
|
||||||
page += F(" isPolling = true;");
|
|
||||||
page += F(" document.getElementById('refresh-btn').disabled = true;");
|
|
||||||
page += F(" document.getElementById('refresh-btn').textContent = 'Scanning...';");
|
|
||||||
page += F(" updateScanStatus();");
|
|
||||||
page += F(" scanPollInterval = setInterval(updateScanStatus, 1000);");
|
|
||||||
page += F("}");
|
|
||||||
page += F("");
|
|
||||||
page += F("function stopPolling() {");
|
|
||||||
page += F(" if(scanPollInterval) {");
|
|
||||||
page += F(" clearInterval(scanPollInterval);");
|
|
||||||
page += F(" scanPollInterval = null;");
|
|
||||||
page += F(" }");
|
|
||||||
page += F(" isPolling = false;");
|
|
||||||
page += F(" document.getElementById('refresh-btn').disabled = false;");
|
|
||||||
page += F(" document.getElementById('refresh-btn').textContent = 'Refresh';");
|
|
||||||
page += F("}");
|
|
||||||
page += F("");
|
|
||||||
page += F("function updateScanStatus() {");
|
|
||||||
page += F(" fetch('/wifi/scanstatus')");
|
|
||||||
page += F(" .then(response => response.json())");
|
|
||||||
page += F(" .then(data => {");
|
|
||||||
page += F(" if(data.scanning) {");
|
|
||||||
page += F(" // Still scanning, show status");
|
|
||||||
page += F(" document.getElementById('scan-results').innerHTML = 'Scanning for networks...<br/><br/>';");
|
|
||||||
page += F(" } else {");
|
|
||||||
page += F(" // Scan complete, update network list");
|
|
||||||
page += F(" stopPolling();");
|
|
||||||
page += F(" updateNetworkList(data);");
|
|
||||||
page += F(" }");
|
|
||||||
page += F(" })");
|
|
||||||
page += F(" .catch(error => {");
|
|
||||||
page += F(" console.error('Error polling scan status:', error);");
|
|
||||||
page += F(" stopPolling();");
|
|
||||||
page += F(" });");
|
|
||||||
page += F("}");
|
|
||||||
page += F("");
|
|
||||||
page += F("function updateNetworkList(data) {");
|
|
||||||
page += F(" let html = '';");
|
|
||||||
page += F(" if(data.count === 0) {");
|
|
||||||
page += F(" html = 'No networks found. Refresh to scan again.<br/><br/>';");
|
|
||||||
page += F(" } else if(data.networks && data.networks.length > 0) {");
|
|
||||||
page += F(" data.networks.forEach(function(network) {");
|
|
||||||
page += F(" // Match existing HTML structure from getScanItemOut()");
|
|
||||||
page += F(" let qualityIcon = getQualityIcon(network.quality);");
|
|
||||||
page += F(" let qualityPercent = network.quality + '%';");
|
|
||||||
page += F(" let encrypted = network.enc_type !== 0 ? '<span class=\"l\">🔒</span>' : '';");
|
|
||||||
page += F(" html += '<div><a href=\"#p\" onclick=\"c(this)\" data-ssid=\"' + escapeHtml(network.ssid) + '\">' + escapeHtml(network.ssid) + '</a>';");
|
|
||||||
page += F(" html += '<div role=\"img\" aria-label=\"' + qualityPercent + '\" title=\"' + qualityPercent + '\" class=\"q q-' + network.quality + '\"></div>';");
|
|
||||||
page += F(" html += '<div class=\"q\">' + qualityPercent + '</div>';");
|
|
||||||
page += F(" html += encrypted;");
|
|
||||||
page += F(" html += '</div>';");
|
|
||||||
page += F(" });");
|
|
||||||
page += F(" }");
|
|
||||||
page += F(" document.getElementById('scan-results').innerHTML = html;");
|
|
||||||
page += F("}");
|
|
||||||
page += F("");
|
|
||||||
page += F("function escapeHtml(text) {");
|
|
||||||
page += F(" const map = {");
|
|
||||||
page += F(" '&': '&',");
|
|
||||||
page += F(" '<': '<',");
|
|
||||||
page += F(" '>': '>',");
|
|
||||||
page += F(" '\"': '"',");
|
|
||||||
page += F(" \"'\": '''");
|
|
||||||
page += F(" };");
|
|
||||||
page += F(" return text.replace(/[&<>\"']/g, m => map[m]);");
|
|
||||||
page += F("}");
|
|
||||||
page += F("");
|
|
||||||
page += F("function getQualityIcon(quality) {");
|
|
||||||
page += F(" if(quality >= 75) return '▂▄▆█';");
|
|
||||||
page += F(" if(quality >= 50) return '▂▄▆▁';");
|
|
||||||
page += F(" if(quality >= 25) return '▂▄▁▁';");
|
|
||||||
page += F(" return '▂▁▁▁';");
|
|
||||||
page += F("}");
|
|
||||||
page += F("");
|
|
||||||
page += F("// Check if scan is in progress on page load");
|
|
||||||
page += F("window.addEventListener('load', function() {");
|
|
||||||
page += F(" fetch('/wifi/scanstatus')");
|
|
||||||
page += F(" .then(response => response.json())");
|
|
||||||
page += F(" .then(data => {");
|
|
||||||
page += F(" if(data.scanning) {");
|
|
||||||
page += F(" startPolling();");
|
|
||||||
page += F(" }");
|
|
||||||
page += F(" });");
|
|
||||||
page += F("});");
|
|
||||||
page += F("</script>");
|
|
||||||
|
|
||||||
page += getHTTPEnd();
|
page += getHTTPEnd();
|
||||||
|
|
||||||
|
#ifdef WM_DEBUG_LEVEL
|
||||||
|
DEBUG_WM(WM_DEBUG_DEV, F("Page length: "), String(page.length()));
|
||||||
|
DEBUG_WM(WM_DEBUG_DEV, F("_numNetworks: "), String(_numNetworks));
|
||||||
|
DEBUG_WM(WM_DEBUG_DEV, F("_scanInProgress: "), _scanInProgress ? "true" : "false");
|
||||||
|
DEBUG_WM(WM_DEBUG_DEV, F("_lastscan: "), String(_lastscan));
|
||||||
|
DEBUG_WM(WM_DEBUG_DEV, F("About to send response"));
|
||||||
|
#endif
|
||||||
|
|
||||||
request->send(200, FPSTR(HTTP_HEAD_CT), page);
|
request->send(200, FPSTR(HTTP_HEAD_CT), page);
|
||||||
|
|
||||||
#ifdef WM_DEBUG_LEVEL
|
#ifdef WM_DEBUG_LEVEL
|
||||||
DEBUG_WM(WM_DEBUG_DEV, F("Sent config page"));
|
DEBUG_WM(WM_DEBUG_DEV, F("Response sent"));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1615,17 +1540,9 @@ bool WiFiManager::WiFi_scanNetworks(){
|
|||||||
return WiFi_scanNetworks(false);
|
return WiFi_scanNetworks(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime,bool async){
|
|
||||||
// async parameter ignored - always async now
|
|
||||||
return WiFi_scanNetworks(millis()-_lastscan > cachetime);
|
|
||||||
}
|
|
||||||
bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime){
|
bool WiFiManager::WiFi_scanNetworks(unsigned int cachetime){
|
||||||
return WiFi_scanNetworks(millis()-_lastscan > cachetime);
|
return WiFi_scanNetworks(millis()-_lastscan > cachetime);
|
||||||
}
|
}
|
||||||
bool WiFiManager::WiFi_scanNetworks(bool force,bool async){
|
|
||||||
// async parameter ignored - always async now
|
|
||||||
return WiFi_scanNetworks(force);
|
|
||||||
}
|
|
||||||
bool WiFiManager::WiFi_scanNetworks(bool force){
|
bool WiFiManager::WiFi_scanNetworks(bool force){
|
||||||
#ifdef WM_DEBUG_LEVEL
|
#ifdef WM_DEBUG_LEVEL
|
||||||
// DEBUG_WM(WM_DEBUG_DEV,_numNetworks,(millis()-_lastscan ));
|
// DEBUG_WM(WM_DEBUG_DEV,_numNetworks,(millis()-_lastscan ));
|
||||||
@@ -2742,8 +2659,21 @@ boolean WiFiManager::captivePortal(AsyncWebServerRequest *request) {
|
|||||||
#ifdef WM_DEBUG_LEVEL
|
#ifdef WM_DEBUG_LEVEL
|
||||||
DEBUG_WM(WM_DEBUG_VERBOSE, F("<- Request redirected to captive portal"));
|
DEBUG_WM(WM_DEBUG_VERBOSE, F("<- Request redirected to captive portal"));
|
||||||
DEBUG_WM(WM_DEBUG_DEV, "serverLoc " + serverLoc);
|
DEBUG_WM(WM_DEBUG_DEV, "serverLoc " + serverLoc);
|
||||||
|
DEBUG_WM(WM_DEBUG_DEV, "Original URL " + request->url());
|
||||||
#endif
|
#endif
|
||||||
request->redirect((String)F("http://") + serverLoc);
|
// Preserve the original path in the redirect
|
||||||
|
String redirectUrl = (String)F("http://") + serverLoc + request->url();
|
||||||
|
if (request->params() > 0) {
|
||||||
|
redirectUrl += F("?");
|
||||||
|
for (size_t i = 0; i < request->params(); i++) {
|
||||||
|
if (i > 0) redirectUrl += F("&");
|
||||||
|
redirectUrl += request->getParam(i)->name() + F("=") + request->getParam(i)->value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef WM_DEBUG_LEVEL
|
||||||
|
DEBUG_WM(WM_DEBUG_DEV, "Redirect URL " + redirectUrl);
|
||||||
|
#endif
|
||||||
|
request->redirect(redirectUrl);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user