mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
Portal: request-scoped shell bundle, SPA connect poll fix, test tidy
- Use per-request PortalShellRenderBundle and one root shell template; drop server-side template registry and simplify WiFiManagerServer. - PortalAppJS: centralize chrome/form binding; fix WiFi connect polling by stopping previous timers on view change and before new polls. - processPortalConnect honors _cleanConnect like connectWifi. - Remove brittle full-JSON API tests; trim test harness surface. - README: small doc touch-up for the shell/portal flow.
This commit is contained in:
@@ -31,10 +31,11 @@ This fork currently includes the following architectural improvements:
|
||||
- **Data-first JSON generation** for portal APIs, including info/device/about data, instead of HTML-to-JSON parsing.
|
||||
- **Capability-driven UI flags** in bootstrap/API payloads so features like info, update, erase, and action visibility can be controlled by backend state.
|
||||
- **SPA-native feedback UX** using in-DOM dialog/toast behavior rather than page-based action flows.
|
||||
- A **single shell render contract** with default shell placeholders (`%PAGE_TITLE%`, `%STYLES%`) plus embedded runtime payloads (`%BOOTSTRAP_JSON%`, `%PORTAL_APP_JS%`).
|
||||
- **Request-scoped shell rendering**: the root portal page is built for each `GET /` from `WM_ROOT_SHELL_TEMPLATE` using a fresh placeholder registry. Shell inputs are only `%PAGE_TITLE%`, `%STYLES%`, `%BOOTSTRAP_JSON%`, and `%PORTAL_APP_JS%` — filled in `WiFiManagerHandlers` from WiFiManager state and embedded assets (not from a server-wide template registry).
|
||||
- **Customization via WiFiManager APIs** (`setTitle`, `setShowInfo`, `setParamsPage`, bootstrap-facing flags, etc.), not by exposing placeholder-registry mutation to consumers.
|
||||
- A clearer separation between:
|
||||
- shell rendering
|
||||
- API responses
|
||||
- shell rendering (handlers + SPA bootstrap)
|
||||
- JSON API responses
|
||||
- captive portal behavior
|
||||
- OTA handling
|
||||
- Updated tests focused on the **shell contract**, **bootstrap payloads**, and **API JSON shapes** rather than removed legacy portal pages.
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
#include <memory>
|
||||
#include "WiFiManager.h"
|
||||
|
||||
#ifndef WM_TEMPLATE_REGISTRY_CAPACITY
|
||||
#define WM_TEMPLATE_REGISTRY_CAPACITY 16
|
||||
#endif
|
||||
|
||||
// Portal UI customization is driven by WiFiManager state and helpers such as setTitle(), setShowInfo(),
|
||||
// setParamsPage(), setShowInfoUpdate(), etc., surfaced through bootstrap/API JSON — not by mutating template
|
||||
// placeholder registries.
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// FORM FIELD NAMES (for IP configuration forms)
|
||||
|
||||
@@ -79,6 +87,7 @@ class WiFiManagerHandlers {
|
||||
String buildApiWifiConnectStatusJson();
|
||||
|
||||
/** Fixed JSON bodies for POST action endpoints (single source for handlers + tests). */
|
||||
static String jsonApiWifiScanAccepted();
|
||||
static String jsonApiDeviceRestartScheduled();
|
||||
static String jsonApiParamsSaveOk();
|
||||
static String jsonApiPortalCloseOk();
|
||||
|
||||
@@ -37,12 +37,6 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <TemplateEngine.h>
|
||||
|
||||
#ifndef WM_TEMPLATE_REGISTRY_CAPACITY
|
||||
#define WM_TEMPLATE_REGISTRY_CAPACITY 16
|
||||
#endif
|
||||
|
||||
class WiFiManager;
|
||||
class WiFiManagerHandlers;
|
||||
|
||||
@@ -74,21 +68,9 @@ const char R_updatedone[] PROGMEM = "/u";
|
||||
|
||||
class WiFiManagerServer {
|
||||
public:
|
||||
static WiFiManagerServer* instance();
|
||||
|
||||
WiFiManagerServer(WiFiManager* wm);
|
||||
~WiFiManagerServer();
|
||||
|
||||
void setupTemplateEngine();
|
||||
PlaceholderRegistry* getPlaceholderRegistry() { return _tplRegistry.get(); }
|
||||
|
||||
static const char* tplGetPageTitle();
|
||||
|
||||
void registerDefaultStyles(PlaceholderRegistry& reg);
|
||||
void registerDefaultPageTitle(PlaceholderRegistry& reg);
|
||||
/** Shell placeholders only: %STYLES%, %PAGE_TITLE%. */
|
||||
void registerDefaultPlaceholders(PlaceholderRegistry& reg);
|
||||
|
||||
void createServer(uint16_t port);
|
||||
void registerRoutes();
|
||||
void setupDNSD();
|
||||
@@ -103,14 +85,10 @@ class WiFiManagerServer {
|
||||
std::unique_ptr<AsyncWebServer> server;
|
||||
std::unique_ptr<DNSServer> dnsServer;
|
||||
|
||||
std::unique_ptr<PlaceholderRegistry> _tplRegistry;
|
||||
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
std::unique_ptr<WiFiManagerDfteLogger> _dfteLogger;
|
||||
bool _wmOwnsDfteLogSink = false;
|
||||
#endif
|
||||
|
||||
static WiFiManagerServer* s_instance;
|
||||
};
|
||||
|
||||
#endif // defined(ESP8266) || defined(ESP32)
|
||||
|
||||
@@ -18,9 +18,11 @@ const char PORTAL_APP_JS[] PROGMEM = R"rawliteral(
|
||||
var boot={};
|
||||
try{var el=document.getElementById('wm-bootstrap');if(el&&el.textContent)boot=JSON.parse(el.textContent);}catch(e){boot={};}
|
||||
|
||||
// --- Core / DOM ---
|
||||
function $(id){return document.getElementById(id);}
|
||||
function esc(t){if(!t)return'';return String(t).replace(/[&<>"']/g,function(c){return({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]);});}
|
||||
|
||||
// --- Toast / dialog ---
|
||||
var _wmToastTimer=null;
|
||||
function showToast(msg,isErr){
|
||||
var el=$('wm-toast');
|
||||
@@ -50,13 +52,37 @@ function showConfirm(msg,onOk){
|
||||
ok.onclick=function(){hide();if(onOk)onOk();};
|
||||
}
|
||||
|
||||
function bindPortalChromeEvents(){
|
||||
var back=$('wm-nav-back');
|
||||
if(back){
|
||||
back.addEventListener('click', function(ev){ ev.preventDefault(); history.back(); });
|
||||
}
|
||||
var r=$('wm-restart-btn'), e=$('wm-exit-btn'), c=$('wm-close-captive-btn'), er=$('wm-erase-btn');
|
||||
if(r && window.portalRestart) r.addEventListener('click', window.portalRestart);
|
||||
if(e && window.portalExit) e.addEventListener('click', window.portalExit);
|
||||
if(c && window.portalCloseCaptive) c.addEventListener('click', window.portalCloseCaptive);
|
||||
if(er && window.portalErase) er.addEventListener('click', window.portalErase);
|
||||
}
|
||||
|
||||
function bindFormSubmitHandlers(){
|
||||
var wf=$('wm-wifi-form'), pf=$('wm-param-form'), of=$('wm-ota-form');
|
||||
if(wf && window.portalWifiSave) wf.addEventListener('submit', window.portalWifiSave);
|
||||
if(pf && window.portalParamSave) pf.addEventListener('submit', window.portalParamSave);
|
||||
if(of && window.portalOtaSubmit) of.addEventListener('submit', window.portalOtaSubmit);
|
||||
}
|
||||
|
||||
// --- View shell ---
|
||||
function setView(html){
|
||||
var a=$('app');
|
||||
if(!a)return;
|
||||
stopWifiScanPolling();
|
||||
stopWifiConnectPolling();
|
||||
a.innerHTML="<div class='wrap'>"+html+"</div>";
|
||||
bindPortalChromeEvents();
|
||||
bindFormSubmitHandlers();
|
||||
}
|
||||
|
||||
// --- Field render (WiFi / params meta) ---
|
||||
function renderMetaField(f){
|
||||
if(!f)return'';
|
||||
if(f.html)return f.html;
|
||||
@@ -89,7 +115,7 @@ function navBar(){
|
||||
if(!f.paramsInWifi){h+=" · <a href='#/setup'>Setup</a>";}
|
||||
if(f.showInfo!==false){h+=" · <a href='#/info'>Info</a>";}
|
||||
if(f.showUpdate){h+=" · <a href='#/update'>Update</a>";}
|
||||
if(boot.showBack){h+=" · <a href='#' onclick='history.back();return false;'>Back</a>";}
|
||||
if(boot.showBack){h+=" · <a href='#' id='wm-nav-back'>Back</a>";}
|
||||
h+="</p><hr/>";
|
||||
return h;
|
||||
}
|
||||
@@ -97,9 +123,9 @@ function navBar(){
|
||||
function deviceActionsHtml(){
|
||||
var f=boot.features||{};
|
||||
var h='', ok=false;
|
||||
if(f.showRestart){h+="<p><button type='button' onclick='portalRestart()'>Restart</button></p>";ok=true;}
|
||||
if(f.showExitPortal){h+="<p><button type='button' onclick='portalExit()'>Exit portal</button></p>";ok=true;}
|
||||
if(f.showCloseCaptive){h+="<p><button type='button' onclick='portalCloseCaptive()'>Stop captive portal detection</button></p>";ok=true;}
|
||||
if(f.showRestart){h+="<p><button type='button' id='wm-restart-btn'>Restart</button></p>";ok=true;}
|
||||
if(f.showExitPortal){h+="<p><button type='button' id='wm-exit-btn'>Exit portal</button></p>";ok=true;}
|
||||
if(f.showCloseCaptive){h+="<p><button type='button' id='wm-close-captive-btn'>Stop captive portal detection</button></p>";ok=true;}
|
||||
if(!ok)return'';
|
||||
return "<div class='device-actions'><h3>Device actions</h3>"+h+"</div>";
|
||||
}
|
||||
@@ -115,12 +141,14 @@ function viewHome(){
|
||||
setView(html);
|
||||
}
|
||||
|
||||
// --- HTTP ---
|
||||
function api(path, opt){
|
||||
opt=opt||{};
|
||||
return fetch(path,{method:opt.method||'GET',headers:opt.headers,body:opt.body,credentials:'same-origin'})
|
||||
.then(function(r){return r.text().then(function(t){return {ok:r.ok,status:r.status,body:t};});});
|
||||
}
|
||||
|
||||
// --- WiFi scan view ---
|
||||
var _wmWifiScanPollTimer=null;
|
||||
|
||||
function stopWifiScanPolling(){
|
||||
@@ -206,7 +234,7 @@ function viewWifi(){
|
||||
try{var m=JSON.parse(res.body);}catch(e){m={};}
|
||||
var html=navBar()+"<h2>WiFi</h2>";
|
||||
html+="<div id='wm-scan-results'></div>";
|
||||
html+="<form id='wm-wifi-form' onsubmit='return portalWifiSave(event)'>";
|
||||
html+="<form id='wm-wifi-form'>";
|
||||
html+=renderFieldList(m.wifiFields||[]);
|
||||
var wf=m.wifiFields||[];
|
||||
var hasPass=false;
|
||||
@@ -231,26 +259,38 @@ function viewWifi(){
|
||||
});
|
||||
}
|
||||
|
||||
// --- WiFi connect polling ---
|
||||
var _wmWifiConnectPollTimer=null;
|
||||
|
||||
function stopWifiConnectPolling(){
|
||||
if(_wmWifiConnectPollTimer){
|
||||
clearInterval(_wmWifiConnectPollTimer);
|
||||
_wmWifiConnectPollTimer=null;
|
||||
}
|
||||
}
|
||||
|
||||
function pollWifiConnectStatus(){
|
||||
var msg=$('wm-wifi-msg');
|
||||
var iv=setInterval(function(){
|
||||
stopWifiConnectPolling();
|
||||
_wmWifiConnectPollTimer=setInterval(function(){
|
||||
api('/api/wifi/connect-status').then(function(res){
|
||||
var j={};
|
||||
var msg=$('wm-wifi-msg');
|
||||
try{j=JSON.parse(res.body);}catch(e){}
|
||||
if(msg&&j.message){
|
||||
msg.innerHTML=esc(j.message);
|
||||
}
|
||||
if(j.state==='success'){
|
||||
clearInterval(iv);
|
||||
stopWifiConnectPolling();
|
||||
showToast(j.message||'WiFi connected',false);
|
||||
location.hash='#/';
|
||||
}else if(j.state==='failed'){
|
||||
clearInterval(iv);
|
||||
stopWifiConnectPolling();
|
||||
showToast(j.message||'WiFi connect failed',true);
|
||||
}
|
||||
});
|
||||
},700);
|
||||
}
|
||||
|
||||
window.portalWifiSave=function(ev){
|
||||
ev.preventDefault();
|
||||
var fd=new FormData(document.getElementById('wm-wifi-form'));
|
||||
@@ -269,6 +309,7 @@ window.portalWifiSave=function(ev){
|
||||
return false;
|
||||
};
|
||||
|
||||
// --- Info view ---
|
||||
function viewInfo(){
|
||||
setView(navBar()+"<p>Loading info...</p>");
|
||||
api('/api/info').then(function(res){
|
||||
@@ -300,11 +341,11 @@ function viewInfo(){
|
||||
html+=section('About',d.about||[]);
|
||||
var act=d.actions||{};
|
||||
html+="<h3>Actions</h3><hr/>";
|
||||
if(act.showRestart)html+="<p><button type='button' onclick='portalRestart()'>Restart</button></p>";
|
||||
if(act.showExitPortal)html+="<p><button type='button' onclick='portalExit()'>Exit portal</button></p>";
|
||||
if(act.showCloseCaptive)html+="<p><button type='button' onclick='portalCloseCaptive()'>Stop captive portal detection</button></p>";
|
||||
if(act.showRestart)html+="<p><button type='button' id='wm-restart-btn'>Restart</button></p>";
|
||||
if(act.showExitPortal)html+="<p><button type='button' id='wm-exit-btn'>Exit portal</button></p>";
|
||||
if(act.showCloseCaptive)html+="<p><button type='button' id='wm-close-captive-btn'>Stop captive portal detection</button></p>";
|
||||
if(act.showUpdate)html+="<p><a href='#/update'>Firmware update</a></p>";
|
||||
if(act.showErase)html+="<p><button type='button' class='D' onclick='portalErase()'>Erase WiFi config</button></p>";
|
||||
if(act.showErase)html+="<p><button type='button' class='D' id='wm-erase-btn'>Erase WiFi config</button></p>";
|
||||
setView(html);
|
||||
});
|
||||
}
|
||||
@@ -332,12 +373,13 @@ window.portalErase=function(){
|
||||
});
|
||||
};
|
||||
|
||||
// --- Params / OTA ---
|
||||
function viewSetup(){
|
||||
setView(navBar()+"<p>Loading...</p>");
|
||||
api('/api/params').then(function(res){
|
||||
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
||||
var html=navBar()+"<h2>Parameters</h2>";
|
||||
html+="<form id='wm-param-form' onsubmit='return portalParamSave(event)'>";
|
||||
html+="<form id='wm-param-form'>";
|
||||
html+=renderFieldList(d.params||[]);
|
||||
html+="<button type='submit'>Save</button></form>";
|
||||
html+="<div id='wm-param-msg'></div>";
|
||||
@@ -361,7 +403,7 @@ function viewUpdate(){
|
||||
var f=boot.features||{};
|
||||
var html=navBar()+"<h2>Firmware update</h2>";
|
||||
if(!f.showUpdate){html+="<p>Firmware update is disabled.</p>";setView(html);return;}
|
||||
html+="<form id='wm-ota-form' onsubmit='return portalOtaSubmit(event)'>";
|
||||
html+="<form id='wm-ota-form'>";
|
||||
html+="<input type='file' id='wm-ota-file' name='update'/>";
|
||||
html+="<button type='submit'>Upload</button></form>";
|
||||
html+="<p><small>Upload a .bin firmware. The device restarts after a successful update.</small></p>";
|
||||
@@ -405,6 +447,7 @@ window.portalOtaSubmit=function(ev){
|
||||
return false;
|
||||
};
|
||||
|
||||
// --- Routing ---
|
||||
function route(){
|
||||
var h=location.hash||'#/';
|
||||
if(h.indexOf('#/')!==0)h='#/';
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
*
|
||||
* Single-page portal shell: only HTML document served for GET /.
|
||||
* Dynamic UI is driven by embedded JS + JSON APIs under /api/...
|
||||
*
|
||||
* Placeholder keys (%PAGE_TITLE%, %STYLES%, %BOOTSTRAP_JSON%, %PORTAL_APP_JS%) are filled per request in
|
||||
* WiFiManagerHandlers::handleRoot (shell contract comment in WiFiManagerHandlers.cpp); do not treat
|
||||
* placeholders as a customization API.
|
||||
*/
|
||||
|
||||
#ifndef _WM_ROOT_SHELL_TEMPLATE_H_
|
||||
|
||||
@@ -727,6 +727,9 @@ void WiFiManager::processPortalConnect() {
|
||||
return;
|
||||
case wm_cp_connect_state_t::starting: {
|
||||
setSTAConfig();
|
||||
if (_cleanConnect) {
|
||||
WiFi_Disconnect(); // match connectWifi(): force a clean STA begin for portal saves too
|
||||
}
|
||||
if (!_connectonsave) {
|
||||
if (!wifiConnectNew(_cpConnectSsid, _cpConnectPass, false)) {
|
||||
failPortalConnect(WL_CONNECT_FAILED, F("WiFi begin failed"));
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
#include "WiFiManagerHandlers.h"
|
||||
#include "WiFiManagerServer.h"
|
||||
#include "templates/CSS.h"
|
||||
#include "templates/RootShell.h"
|
||||
#include "templates/PortalAppJS.h"
|
||||
@@ -40,8 +39,6 @@ static void jsonAppendEscaped(String& out, const String& s) {
|
||||
|
||||
namespace {
|
||||
|
||||
const char kEmptyTemplateChunk[] PROGMEM = "";
|
||||
|
||||
inline void reservePage(String& page, size_t extraBytes = WM_PAGE_RESERVE_BYTES) {
|
||||
if (extraBytes == 0) return;
|
||||
const size_t targetLen = page.length() + extraBytes;
|
||||
@@ -76,16 +73,13 @@ AsyncWebServerResponse* beginTemplateResponse(AsyncWebServerRequest* request,
|
||||
);
|
||||
}
|
||||
|
||||
void registerSharedShellPlaceholders(WiFiManagerServer* server, PlaceholderRegistry& registry) {
|
||||
if (server) {
|
||||
server->registerDefaultStyles(registry);
|
||||
return;
|
||||
}
|
||||
// Shell contract (must match templates/RootShell.h — customize UI via WiFiManager APIs + bootstrap JSON, not registries):
|
||||
// %PAGE_TITLE% -> document title (from WiFiManager title state)
|
||||
// %STYLES% -> embedded portal CSS
|
||||
// %BOOTSTRAP_JSON% -> initial SPA runtime payload
|
||||
// %PORTAL_APP_JS% -> embedded SPA source
|
||||
|
||||
registry.registerProgmemData("%STYLES%", CSS_STYLE);
|
||||
}
|
||||
|
||||
struct ShellRenderBundle {
|
||||
struct PortalShellRenderBundle {
|
||||
String bootstrapJson;
|
||||
String pageTitleStatic;
|
||||
PlaceholderRegistry registry;
|
||||
@@ -93,10 +87,23 @@ struct ShellRenderBundle {
|
||||
DynamicTemplateDescriptor bootstrapDescriptor;
|
||||
DynamicTemplateDescriptor pageTitleDescriptor;
|
||||
|
||||
ShellRenderBundle()
|
||||
PortalShellRenderBundle()
|
||||
: registry(WM_TEMPLATE_REGISTRY_CAPACITY), bootstrapDescriptor{}, pageTitleDescriptor{} {}
|
||||
};
|
||||
|
||||
void populatePortalShellBundle(PortalShellRenderBundle& bundle,
|
||||
const String& pageTitle,
|
||||
const String& bootstrapJson) {
|
||||
bundle.pageTitleStatic = pageTitle;
|
||||
bundle.bootstrapJson = bootstrapJson;
|
||||
bundle.registry.registerProgmemData("%STYLES%", CSS_STYLE);
|
||||
bundle.registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
|
||||
configureDynamicStringDescriptor(bundle.pageTitleDescriptor, bundle.pageTitleStatic);
|
||||
bundle.registry.registerDynamicTemplate("%PAGE_TITLE%", &bundle.pageTitleDescriptor);
|
||||
configureDynamicStringDescriptor(bundle.bootstrapDescriptor, bundle.bootstrapJson);
|
||||
bundle.registry.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bundle.bootstrapDescriptor);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {}
|
||||
@@ -547,22 +554,15 @@ void WiFiManagerHandlers::handleRoot(AsyncWebServerRequest *request) {
|
||||
if (captivePortal(request)) return;
|
||||
handleRequest(request);
|
||||
|
||||
auto bundle = std::make_shared<ShellRenderBundle>();
|
||||
bundle->bootstrapJson = buildPortalBootstrapJson();
|
||||
|
||||
if (_wm->_serverManager) {
|
||||
_wm->_serverManager->registerDefaultStyles(bundle->registry);
|
||||
_wm->_serverManager->registerDefaultPageTitle(bundle->registry);
|
||||
} else {
|
||||
registerSharedShellPlaceholders(nullptr, bundle->registry);
|
||||
bundle->pageTitleStatic = _wm->_title;
|
||||
configureDynamicStringDescriptor(bundle->pageTitleDescriptor, bundle->pageTitleStatic);
|
||||
bundle->registry.registerDynamicTemplate("%PAGE_TITLE%", &bundle->pageTitleDescriptor);
|
||||
}
|
||||
|
||||
configureDynamicStringDescriptor(bundle->bootstrapDescriptor, bundle->bootstrapJson);
|
||||
bundle->registry.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bundle->bootstrapDescriptor);
|
||||
bundle->registry.registerProgmemData("%PORTAL_APP_JS%", PORTAL_APP_JS);
|
||||
// Root shell render model:
|
||||
// - WiFiManagerHandlers owns request-time shell assembly; WiFiManagerServer owns HTTP lifecycle only.
|
||||
// - Build one request-scoped PortalShellRenderBundle.
|
||||
// - Populate one request-scoped PlaceholderRegistry with shell defaults + request payloads.
|
||||
// - Render WM_ROOT_SHELL_TEMPLATE.
|
||||
auto bundle = std::make_shared<PortalShellRenderBundle>();
|
||||
populatePortalShellBundle(*bundle,
|
||||
_wm ? _wm->_title : String(),
|
||||
buildPortalBootstrapJson());
|
||||
|
||||
request->send(beginTemplateResponse(request, bundle, WM_ROOT_SHELL_TEMPLATE));
|
||||
}
|
||||
@@ -1011,7 +1011,7 @@ void WiFiManagerHandlers::handleApiWifiScan(AsyncWebServerRequest *request) {
|
||||
_wm->requestAsyncScan(true);
|
||||
|
||||
AsyncWebServerResponse *response =
|
||||
request->beginResponse(202, "application/json", "{\"accepted\":true,\"state\":\"queued\"}");
|
||||
request->beginResponse(202, "application/json", jsonApiWifiScanAccepted());
|
||||
response->addHeader(F("Cache-Control"), F("no-cache"));
|
||||
request->send(response);
|
||||
}
|
||||
@@ -1181,6 +1181,10 @@ void WiFiManagerHandlers::handleApiStatus(AsyncWebServerRequest *request) {
|
||||
sendApiJson(request, 200, buildApiStatusJson());
|
||||
}
|
||||
|
||||
String WiFiManagerHandlers::jsonApiWifiScanAccepted() {
|
||||
return F("{\"accepted\":true,\"state\":\"queued\"}");
|
||||
}
|
||||
|
||||
String WiFiManagerHandlers::jsonApiDeviceRestartScheduled() {
|
||||
return F("{\"ok\":true,\"message\":\"Restart scheduled\"}");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Server lifecycle: single shell + JSON APIs + OTA + captive portal.
|
||||
* HTTP server lifecycle and route registration only. HTML shell rendering is request-scoped in
|
||||
* WiFiManagerHandlers (WM_ROOT_SHELL_TEMPLATE + per-request placeholder registry).
|
||||
* Route inventory matches WiFiManagerServer.h (GET /, /api/..., POST /u, onNotFound).
|
||||
*/
|
||||
|
||||
@@ -15,61 +16,14 @@
|
||||
#include "WiFiManagerDfteLogger.h"
|
||||
#include <DeviceFrameworkTemplateEngineDebug.h>
|
||||
#endif
|
||||
#include "templates/CSS.h"
|
||||
|
||||
#if defined(ESP8266) || defined(ESP32)
|
||||
|
||||
WiFiManagerServer* WiFiManagerServer::s_instance = nullptr;
|
||||
|
||||
WiFiManagerServer* WiFiManagerServer::instance() {
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
WiFiManagerServer::WiFiManagerServer(WiFiManager* wm)
|
||||
: _wm(wm), _handlers(std::make_unique<WiFiManagerHandlers>(wm)) {
|
||||
s_instance = this;
|
||||
}
|
||||
: _wm(wm), _handlers(std::make_unique<WiFiManagerHandlers>(wm)) {}
|
||||
|
||||
WiFiManagerServer::~WiFiManagerServer() = default;
|
||||
|
||||
const char* WiFiManagerServer::tplGetPageTitle() {
|
||||
auto srv = WiFiManagerServer::instance();
|
||||
if (!srv || !srv->_wm) return "";
|
||||
return srv->_wm->_title.c_str();
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerDefaultStyles(PlaceholderRegistry& reg) {
|
||||
reg.registerProgmemData("%STYLES%", CSS_STYLE);
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerDefaultPageTitle(PlaceholderRegistry& reg) {
|
||||
reg.registerRamData("%PAGE_TITLE%", &WiFiManagerServer::tplGetPageTitle);
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerDefaultPlaceholders(PlaceholderRegistry& reg) {
|
||||
registerDefaultStyles(reg);
|
||||
registerDefaultPageTitle(reg);
|
||||
}
|
||||
|
||||
void WiFiManagerServer::setupTemplateEngine() {
|
||||
if (!_tplRegistry) {
|
||||
_tplRegistry = std::unique_ptr<PlaceholderRegistry>(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY));
|
||||
} else {
|
||||
_tplRegistry->clear();
|
||||
}
|
||||
registerDefaultPlaceholders(*_tplRegistry);
|
||||
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
if (!deviceFrameworkTemplateEngineIsLoggingEnabled()) {
|
||||
if (!_dfteLogger) {
|
||||
_dfteLogger = std::make_unique<WiFiManagerDfteLogger>(_wm);
|
||||
}
|
||||
deviceFrameworkTemplateEngineEnableLogging(_dfteLogger.get(), static_cast<const void*>(this));
|
||||
_wmOwnsDfteLogSink = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiManagerServer::createServer(uint16_t port) {
|
||||
if (server) {
|
||||
#ifndef WM_NO_LOG
|
||||
@@ -89,7 +43,18 @@ void WiFiManagerServer::createServer(uint16_t port) {
|
||||
}
|
||||
|
||||
server.reset(new AsyncWebServer(port));
|
||||
setupTemplateEngine();
|
||||
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
// DFTE logging is still owned by server lifecycle; it is intentionally independent of
|
||||
// request-scoped shell rendering / placeholder registration.
|
||||
if (!deviceFrameworkTemplateEngineIsLoggingEnabled()) {
|
||||
if (!_dfteLogger) {
|
||||
_dfteLogger = std::make_unique<WiFiManagerDfteLogger>(_wm);
|
||||
}
|
||||
deviceFrameworkTemplateEngineEnableLogging(_dfteLogger.get(), static_cast<const void*>(this));
|
||||
_wmOwnsDfteLogSink = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerRoutes() {
|
||||
@@ -100,10 +65,6 @@ void WiFiManagerServer::registerRoutes() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_tplRegistry) {
|
||||
setupTemplateEngine();
|
||||
}
|
||||
|
||||
if (_wm->_webservercallback != NULL) {
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("[CB] _webservercallback calling"));
|
||||
@@ -212,8 +173,6 @@ void WiFiManagerServer::shutdownServer() {
|
||||
dnsServer->stop();
|
||||
dnsServer.reset();
|
||||
}
|
||||
|
||||
if (s_instance == this) s_instance = nullptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -15,8 +15,6 @@ TestCase tests[] = {
|
||||
TEST_ENTRY(test_api_info_json_shape),
|
||||
TEST_ENTRY(test_api_params_json_shape),
|
||||
TEST_ENTRY(test_api_status_json_shape),
|
||||
TEST_ENTRY(test_api_action_response_json_literals),
|
||||
TEST_ENTRY(test_api_ota_done_success_json_literal),
|
||||
|
||||
// Configuration tests
|
||||
TEST_ENTRY(test_configuration_setters),
|
||||
|
||||
@@ -131,8 +131,6 @@ void test_api_wifi_meta_json_shape();
|
||||
void test_api_info_json_shape();
|
||||
void test_api_params_json_shape();
|
||||
void test_api_status_json_shape();
|
||||
void test_api_action_response_json_literals();
|
||||
void test_api_ota_done_success_json_literal();
|
||||
|
||||
// State transition tests
|
||||
void test_portal_to_connected_transition();
|
||||
|
||||
@@ -81,41 +81,3 @@ void test_api_status_json_shape() {
|
||||
|
||||
Serial.println("[TEST] API status JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_action_response_json_literals() {
|
||||
Serial.println("[TEST] Testing POST /api action JSON payloads...");
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Restart scheduled\"}",
|
||||
WiFiManagerHandlers::jsonApiDeviceRestartScheduled().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Setup saved\"}",
|
||||
WiFiManagerHandlers::jsonApiParamsSaveOk().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Captive portal detection disabled\"}",
|
||||
WiFiManagerHandlers::jsonApiPortalCloseOk().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Exiting portal\"}",
|
||||
WiFiManagerHandlers::jsonApiPortalExitOk().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":false,\"message\":\"Exit not allowed\"}",
|
||||
WiFiManagerHandlers::jsonApiPortalExitForbidden().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"WiFi configuration erased. Device will restart shortly.\"}",
|
||||
WiFiManagerHandlers::jsonApiEraseResponse(true).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":false,\"message\":\"Erase failed\"}",
|
||||
WiFiManagerHandlers::jsonApiEraseResponse(false).c_str());
|
||||
|
||||
Serial.println("[TEST] Action response JSON literals test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_ota_done_success_json_literal() {
|
||||
Serial.println("[TEST] Testing POST /u completion success JSON...");
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Firmware updated. Restarting...\"}",
|
||||
WiFiManagerHandlers::jsonApiOtaUpdateSuccess().c_str());
|
||||
|
||||
Serial.println("[TEST] OTA success JSON literal test completed successfully");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user