mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
Improve portal WiFi handoff and bump WiFiManager to v2.0.19.
Expose redirect details after connect-on-save so clients can land on the station IP before the AP closes, hide the mobile nav scrollbar, and document the updated API contract.
This commit is contained in:
@@ -32,6 +32,7 @@ This fork currently includes the following architectural improvements:
|
||||
- **Capability-driven UI flags** in bootstrap/API payloads so features like info, update, erase, and action visibility can be controlled by backend state.
|
||||
- **Portal bootstrap contract v2**: nested `brand`, `context`, `pages`, `actions`, `layout`, `extraHomeCards`; Wi-Fi meta params include `kind` (`field` | `html`) for first-class custom HTML parameters.
|
||||
- **SPA-native feedback UX** using in-DOM dialog/toast behavior rather than page-based action flows.
|
||||
- **Connect-on-save handoff**: when portal save triggers a station connect, `/api/wifi/connect-status` now reports a brief success state with `stationIp` and `redirectUrl` so the SPA can show the new address and redirect before the AP shuts down.
|
||||
- **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 `%PAGE_TITLE%`, `%STYLES%`, `%BOOTSTRAP_JSON%`, `%PORTAL_APP_JS%`, and `%PORTAL_APPEND_JS%` — filled in `WiFiManagerHandlers` from WiFiManager state and embedded assets (not from a server-wide template registry).
|
||||
- **Customization via WiFiManager `portal*` APIs** (`portalSetBrandTitle`, `portalSetPageInfoVisible`, `portalSetLayoutParamsLocation`, `portalAddParameter`, asset hooks, etc.) and JSON under `/api/...`, not by exposing placeholder-registry mutation to consumers.
|
||||
- A clearer separation between:
|
||||
@@ -140,6 +141,29 @@ wm.portalAppendJs(
|
||||
);
|
||||
```
|
||||
|
||||
## WiFi Connect Status API
|
||||
|
||||
When `portalSetBehaviorConnectOnSave(true)` is enabled, saving WiFi credentials queues a station join and the SPA polls `GET /api/wifi/connect-status`.
|
||||
|
||||
Response shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"state": "waiting | success | failed",
|
||||
"message": "human readable status",
|
||||
"wifiStatus": "WL_CONNECTED",
|
||||
"stationIp": "192.168.1.42",
|
||||
"redirectUrl": "http://192.168.1.42/"
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `stationIp` and `redirectUrl` are present only after a successful station join.
|
||||
- If the portal HTTP server is not on port `80`, `redirectUrl` includes the active port.
|
||||
- On success, WiFiManager keeps the portal alive briefly so the client can read the success payload and navigate to the new device address before the captive AP is shut down.
|
||||
- This improves the handoff on typical home networks, but it is not a universal guarantee: client captive-portal helpers, browser behavior, DHCP timing, and network isolation can still affect whether the redirect completes automatically.
|
||||
|
||||
## Dependencies
|
||||
|
||||
This fork depends on **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)) and **ESP32Async/ESPAsyncWebServer**.
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
#define WM_STRING(x) WM_STRING2(x)
|
||||
|
||||
// WiFiManager version
|
||||
const char WM_VERSION_STR[] PROGMEM = "v2.0.18";
|
||||
const char WM_VERSION_STR[] PROGMEM = "v2.0.19";
|
||||
|
||||
// #include <esp_idf_version.h>
|
||||
#ifdef ESP_IDF_VERSION
|
||||
@@ -965,7 +965,26 @@ protected:
|
||||
_scan.completionPending = true;
|
||||
_scan.completionResult = completionResult;
|
||||
}
|
||||
void wmTestSetPortalActive(bool active) { configPortalActive = active; }
|
||||
void wmTestSetPortalActive(bool active) {
|
||||
configPortalActive = active;
|
||||
// Keep the test helper aligned with real portal startup so timeout-based
|
||||
// assertions are anchored to "now" instead of device boot.
|
||||
if (active) {
|
||||
_configPortalStart = millis();
|
||||
}
|
||||
}
|
||||
void wmTestSetPortalConnectSuccess(const String& message, const String& stationIp, uint8_t status = WL_CONNECTED) {
|
||||
_cpConnectState = wm_cp_connect_state_t::success;
|
||||
_cpConnectMessage = message;
|
||||
_cpConnectStationIp = stationIp;
|
||||
_cpConnectStatus = status;
|
||||
}
|
||||
void wmTestSetPortalConnectFailure(const String& message, uint8_t status = WL_CONNECT_FAILED) {
|
||||
_cpConnectState = wm_cp_connect_state_t::failed;
|
||||
_cpConnectMessage = message;
|
||||
_cpConnectStationIp = "";
|
||||
_cpConnectStatus = status;
|
||||
}
|
||||
void wmTestSetConnectPending(bool active);
|
||||
void wmTestSetScanLifecycleBlocked(bool blocked) { _scanLifecycleBlocked = blocked; }
|
||||
void wmTestSetScanGenerations(uint32_t generation, uint32_t runningGeneration, uint32_t completionGeneration) {
|
||||
@@ -987,6 +1006,7 @@ protected:
|
||||
delaying,
|
||||
starting,
|
||||
waiting,
|
||||
success_waiting_close,
|
||||
success,
|
||||
failed
|
||||
};
|
||||
@@ -1001,6 +1021,8 @@ protected:
|
||||
unsigned long _cpConnectStartedAt = 0;
|
||||
unsigned long _cpConnectDelayUntil = 0;
|
||||
unsigned long _cpConnectTimeoutMs = 0;
|
||||
unsigned long _cpConnectCloseAt = 0;
|
||||
String _cpConnectStationIp;
|
||||
WiFiManagerEventCallback _eventCallback = nullptr;
|
||||
|
||||
//helpers (rendering methods moved to WiFiManagerHandlers)
|
||||
|
||||
@@ -57,15 +57,22 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
"h2{font-size:1.15rem}"
|
||||
"h3{font-size:1rem}"
|
||||
"p{margin:.5em 0}"
|
||||
/* Top navigation */
|
||||
".wm-nav{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 20px;padding:6px;"
|
||||
/* Top navigation — single row; overflow scrollable but scrollbar hidden (touch still pans) */
|
||||
".wm-nav{display:flex;flex-wrap:nowrap;align-items:stretch;gap:4px;margin:0 0 20px;padding:4px 6px;"
|
||||
"overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;-ms-overflow-style:none;"
|
||||
"background:var(--wm-surface);border-radius:var(--wm-radius);box-shadow:var(--wm-shadow);border:1px solid var(--wm-border)}"
|
||||
".wm-nav-link{display:inline-flex;align-items:center;justify-content:center;padding:10px 14px;border-radius:var(--wm-radius-sm);"
|
||||
"font-size:.9rem;font-weight:600;color:var(--wm-muted);text-decoration:none;border:1px solid transparent}"
|
||||
".wm-nav::-webkit-scrollbar{display:none;width:0;height:0}"
|
||||
".wm-nav-link{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;padding:6px 8px;border-radius:var(--wm-radius-sm);"
|
||||
"font-size:.8rem;font-weight:600;color:var(--wm-muted);text-decoration:none;border:1px solid transparent;white-space:nowrap}"
|
||||
".wm-nav-link:hover{color:var(--wm-brand);background:rgba(14,165,233,.08);text-decoration:none}"
|
||||
"body.invert .wm-nav-link:hover{background:rgba(56,189,248,.12)}"
|
||||
".wm-nav-link--active{color:var(--wm-text);background:rgba(14,165,233,.15);border-color:rgba(14,165,233,.35)}"
|
||||
"body.invert .wm-nav-link--active{background:rgba(56,189,248,.2);border-color:rgba(56,189,248,.4)}"
|
||||
".wm-nav .wm-icon-label{gap:4px}"
|
||||
".wm-nav .wm-icon{width:.95rem;height:.95rem;flex:0 0 .95rem}"
|
||||
".wm-icon-label{display:inline-flex;align-items:center;gap:8px;min-width:0}"
|
||||
".wm-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1rem;height:1.1rem;flex:0 0 1.1rem;color:currentColor}"
|
||||
".wm-icon svg{display:block;width:100%;height:100%}"
|
||||
/* Hero / page chrome */
|
||||
".wm-hero{margin-bottom:20px}"
|
||||
".wm-hero-title{font-size:1.65rem;line-height:1.2;margin-bottom:.25em}"
|
||||
@@ -81,6 +88,7 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
"box-shadow:var(--wm-shadow);padding:18px 16px;margin-bottom:16px;text-align:left}"
|
||||
".wm-card-title{font-size:1rem;margin:0 0 12px;padding-bottom:10px;border-bottom:1px solid var(--wm-border);"
|
||||
"color:var(--wm-text)}"
|
||||
".wm-card-subtitle{margin:-4px 0 12px;font-size:.9rem;line-height:1.45;color:var(--wm-muted)}"
|
||||
".wm-lead{margin:0 0 8px;font-size:.95rem;color:var(--wm-muted);line-height:1.5}"
|
||||
".wm-home-summary{margin:0 0 14px;font-size:1rem;font-weight:600;line-height:1.45;color:var(--wm-text)}"
|
||||
/* Callout / status */
|
||||
@@ -88,7 +96,16 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
"background:rgba(14,165,233,.06);margin-bottom:16px}"
|
||||
"body.invert .wm-callout{background:rgba(56,189,248,.08)}"
|
||||
".wm-callout--info{border-left:4px solid var(--wm-brand)}"
|
||||
".wm-status{margin:0 0 10px;font-size:.9rem;font-weight:600;color:var(--wm-brand)}"
|
||||
".wm-info-summary{font-weight:600;color:var(--wm-text)}"
|
||||
".wm-status{margin:0 0 12px;padding:10px 12px;font-size:.92rem;font-weight:700;color:var(--wm-brand);"
|
||||
"text-align:center;border-radius:var(--wm-radius-sm);background:rgba(14,165,233,.08);border:1px solid rgba(14,165,233,.18)}"
|
||||
".wm-status--warning{color:#c2410c;background:rgba(249,115,22,.12);border-color:rgba(249,115,22,.28)}"
|
||||
".wm-status--caution{color:#a16207;background:rgba(234,179,8,.14);border-color:rgba(234,179,8,.3)}"
|
||||
".wm-status--danger{color:var(--wm-danger);background:rgba(220,38,38,.12);border-color:rgba(220,38,38,.28)}"
|
||||
"body.invert .wm-status{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.22)}"
|
||||
"body.invert .wm-status--warning{background:rgba(249,115,22,.18);border-color:rgba(249,115,22,.34)}"
|
||||
"body.invert .wm-status--caution{background:rgba(234,179,8,.18);border-color:rgba(234,179,8,.34)}"
|
||||
"body.invert .wm-status--danger{background:rgba(220,38,38,.18);border-color:rgba(220,38,38,.34)}"
|
||||
/* Form fields */
|
||||
".wm-field{margin-bottom:14px;text-align:left}"
|
||||
".wm-field:last-child{margin-bottom:0}"
|
||||
@@ -109,6 +126,7 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
".wm-btn:disabled{opacity:.5;cursor:not-allowed}"
|
||||
".wm-btn:active{transform:scale(.98)}"
|
||||
"a.wm-btn{box-sizing:border-box;text-decoration:none}"
|
||||
".wm-btn .wm-icon-label{justify-content:center}"
|
||||
".wm-btn--block{width:100%}"
|
||||
".wm-btn--primary{background:var(--wm-brand);color:#fff}"
|
||||
".wm-btn--primary:hover{background:var(--wm-brand-hover)}"
|
||||
|
||||
@@ -106,8 +106,15 @@ function startPortalTimeoutCountdown(initialSeconds){
|
||||
var el=$('wm-portal-timeout');
|
||||
var remaining=Math.max(0,Math.floor(initialSeconds||0));
|
||||
if(!el||remaining<=0)return;
|
||||
function levelClass(seconds){
|
||||
if(seconds<10)return'wm-status--danger';
|
||||
if(seconds<30)return'wm-status--caution';
|
||||
if(seconds<60)return'wm-status--warning';
|
||||
return'';
|
||||
}
|
||||
function render(){
|
||||
if(!el)return;
|
||||
el.className='wm-status'+(levelClass(remaining)?' '+levelClass(remaining):'');
|
||||
el.textContent='Captive portal timeout: '+formatDurationSeconds(remaining);
|
||||
}
|
||||
render();
|
||||
@@ -150,6 +157,104 @@ function renderFieldList(arr){
|
||||
return h;
|
||||
}
|
||||
|
||||
function iconSvg(name){
|
||||
var body='';
|
||||
switch(name){
|
||||
case 'home':
|
||||
body="<path d='M3 10.5 12 3l9 7.5'/><path d='M5 9.5V20h14V9.5'/>";
|
||||
break;
|
||||
case 'wifi':
|
||||
body="<path d='M4.5 9.5a12 12 0 0 1 15 0'/><path d='M7.5 12.5a7.5 7.5 0 0 1 9 0'/><path d='M10.5 15.5a3.5 3.5 0 0 1 3 0'/><circle cx='12' cy='19' r='1.2' fill='currentColor' stroke='none'/>";
|
||||
break;
|
||||
case 'setup':
|
||||
body="<line x1='5' y1='6' x2='19' y2='6'/><circle cx='9' cy='6' r='2'/><line x1='5' y1='12' x2='19' y2='12'/><circle cx='15' cy='12' r='2'/><line x1='5' y1='18' x2='19' y2='18'/><circle cx='11' cy='18' r='2'/>";
|
||||
break;
|
||||
case 'info':
|
||||
body="<circle cx='12' cy='12' r='9'/><line x1='12' y1='10.5' x2='12' y2='16.5'/><circle cx='12' cy='7.2' r='1' fill='currentColor' stroke='none'/>";
|
||||
break;
|
||||
case 'update':
|
||||
body="<path d='M12 4v10'/><path d='m8.5 10.5 3.5 3.5 3.5-3.5'/><path d='M5 19h14'/>";
|
||||
break;
|
||||
case 'back':
|
||||
body="<path d='M10 6 4 12l6 6'/><path d='M4 12h16'/>";
|
||||
break;
|
||||
case 'restart':
|
||||
body="<path d='M20 12a8 8 0 1 1-2.3-5.7'/><path d='M20 4v5h-5'/>";
|
||||
break;
|
||||
case 'exit':
|
||||
body="<path d='M10 6H6v12h4'/><path d='M14 8l4 4-4 4'/><path d='M9 12h9'/>";
|
||||
break;
|
||||
case 'close':
|
||||
body="<path d='M4 12a8 8 0 1 0 16 0A8 8 0 1 0 4 12'/><path d='m8.5 8.5 7 7'/>";
|
||||
break;
|
||||
case 'erase':
|
||||
body="<path d='M5 7h14'/><path d='M9 7V5h6v2'/><path d='M8 7l1 12h6l1-12'/><path d='M10 10v6'/><path d='M14 10v6'/>";
|
||||
break;
|
||||
case 'status':
|
||||
body="<circle cx='12' cy='12' r='9'/><path d='M12 7v5l3 2'/>";
|
||||
break;
|
||||
case 'device':
|
||||
body="<rect x='7' y='7' width='10' height='10' rx='1.5'/><path d='M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4'/>";
|
||||
break;
|
||||
case 'diagnostics':
|
||||
body="<path d='M4 17h3l2-5 3 3 2-7 2 9h4'/>";
|
||||
break;
|
||||
case 'about':
|
||||
body="<circle cx='12' cy='12' r='9'/><path d='M12 10.5v5.5'/><circle cx='12' cy='7.2' r='1' fill='currentColor' stroke='none'/>";
|
||||
break;
|
||||
case 'actions':
|
||||
body="<path d='M12 3v18'/><path d='M3 12h18'/><path d='m6.5 6.5 11 11'/><path d='m17.5 6.5-11 11'/>";
|
||||
break;
|
||||
case 'scan':
|
||||
body="<path d='M4 7h16'/><path d='M4 12h10'/><path d='M4 17h7'/><path d='m16 15 4 4'/><circle cx='14' cy='13' r='4'/>";
|
||||
break;
|
||||
case 'save':
|
||||
body="<path d='M5 4h11l3 3v13H5z'/><path d='M8 4v5h8'/><path d='M9 19v-5h6v5'/>";
|
||||
break;
|
||||
case 'connect':
|
||||
body="<path d='M4.5 12a7.5 7.5 0 0 1 7.5-7.5'/><path d='m17 6 2 2-2 2'/><path d='M12 4.5H19'/>";
|
||||
break;
|
||||
case 'upload':
|
||||
body="<path d='M12 20V8'/><path d='m8.5 11.5 3.5-3.5 3.5 3.5'/><path d='M5 20h14'/>";
|
||||
break;
|
||||
}
|
||||
if(!body)return'';
|
||||
return "<span class='wm-icon wm-icon--"+name+"' aria-hidden='true'><svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'>"+body+"</svg></span>";
|
||||
}
|
||||
|
||||
function labelWithIcon(name,label){
|
||||
return "<span class='wm-icon-label'>"+iconSvg(name)+"<span>"+esc(label||'')+"</span></span>";
|
||||
}
|
||||
|
||||
function cardTitleHtml(name,title){
|
||||
return "<h2 class='wm-card-title'>"+labelWithIcon(name,title)+"</h2>";
|
||||
}
|
||||
|
||||
function sectionCardHtml(name,title,subtitle,rows){
|
||||
if(!rows||!rows.length)return'';
|
||||
var h="<div class='wm-card'>"+cardTitleHtml(name,title);
|
||||
if(subtitle)h+="<p class='wm-card-subtitle'>"+esc(subtitle)+"</p>";
|
||||
h+="<dl class='wm-kv'>";
|
||||
for(var i=0;i<rows.length;i++){
|
||||
h+="<dt>"+esc(rows[i].label)+"</dt><dd>"+rows[i].value+"</dd>";
|
||||
}
|
||||
h+="</dl></div>";
|
||||
return h;
|
||||
}
|
||||
|
||||
function rowsWithoutKeys(rows,blocked){
|
||||
if(!rows||!rows.length)return[];
|
||||
var out=[],i,row,key;
|
||||
for(i=0;i<rows.length;i++){
|
||||
row=rows[i];
|
||||
if(!row)continue;
|
||||
key=row.key||'';
|
||||
if(blocked.indexOf(key)!==-1)continue;
|
||||
out.push(row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function navBar(active){
|
||||
active=active||'home';
|
||||
var pages=boot.pages||{};
|
||||
@@ -160,12 +265,12 @@ function navBar(active){
|
||||
var pi=pages.info||{};
|
||||
var pu=pages.update||{};
|
||||
var h="<nav class='wm-nav' aria-label='Configuration'>";
|
||||
h+="<a class='wm-nav-link"+(active==='home'?' wm-nav-link--active':'')+"' href='#/'>Home</a>";
|
||||
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>WiFi</a>";
|
||||
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>Setup</a>";}
|
||||
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>Info</a>";}
|
||||
if(pu.visible){h+="<a class='wm-nav-link"+(active==='update'?' wm-nav-link--active':'')+"' href='#/update'>Update</a>";}
|
||||
if(actions.back&&actions.back.visible){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>Back</a>";}
|
||||
h+="<a class='wm-nav-link"+(active==='home'?' wm-nav-link--active':'')+"' href='#/'>"+labelWithIcon('home','Home')+"</a>";
|
||||
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>"+labelWithIcon('wifi','WiFi')+"</a>";
|
||||
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>"+labelWithIcon('setup','Setup')+"</a>";}
|
||||
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>"+labelWithIcon('info','Info')+"</a>";}
|
||||
if(pu.visible){h+="<a class='wm-nav-link"+(active==='update'?' wm-nav-link--active':'')+"' href='#/update'>"+labelWithIcon('update','Update')+"</a>";}
|
||||
if(actions.back&&actions.back.visible){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>"+labelWithIcon('back','Back')+"</a>";}
|
||||
h+="</nav>";
|
||||
return h;
|
||||
}
|
||||
@@ -173,11 +278,11 @@ function navBar(active){
|
||||
function deviceActionsHtml(){
|
||||
var actions=boot.actions||{};
|
||||
var h='', ok=false;
|
||||
if(actions.restart&&actions.restart.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>Restart device</button>";ok=true;}
|
||||
if(actions.exitPortal&&actions.exitPortal.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>Exit portal</button>";ok=true;}
|
||||
if(actions.closeCaptive&&actions.closeCaptive.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>Stop captive portal</button>";ok=true;}
|
||||
if(actions.restart&&actions.restart.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>"+labelWithIcon('restart','Restart device')+"</button>";ok=true;}
|
||||
if(actions.exitPortal&&actions.exitPortal.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>"+labelWithIcon('exit','Exit portal')+"</button>";ok=true;}
|
||||
if(actions.closeCaptive&&actions.closeCaptive.visible){h+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>"+labelWithIcon('close','Stop captive portal')+"</button>";ok=true;}
|
||||
if(!ok)return'';
|
||||
return "<div class='wm-card'><h2 class='wm-card-title'>Device</h2><div class='wm-btn-group'>"+h+"</div></div>";
|
||||
return "<div class='wm-card'>"+cardTitleHtml('device','Device')+"<p class='wm-card-subtitle'>Quick device-level actions while the portal is open.</p><div class='wm-btn-group'>"+h+"</div></div>";
|
||||
}
|
||||
|
||||
function renderExtraHomeCards(cards){
|
||||
@@ -215,7 +320,7 @@ function viewHome(){
|
||||
if(ident){html+="<p class='wm-hero-sub'>"+ident+"</p>";}
|
||||
if(brand.homeIntro){html+="<p class='wm-hero-intro'>"+esc(brand.homeIntro)+"</p>";}
|
||||
html+="</header>";
|
||||
html+="<div class='wm-card'><h2 class='wm-card-title'>Status</h2>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
|
||||
if(summary){html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";}
|
||||
if(timeoutSeconds>0){html+="<p class='wm-status' id='wm-portal-timeout'></p>";}
|
||||
html+="<p class='wm-lead'>Use <strong>WiFi</strong> to scan networks and connect. Open <strong>Info</strong> for addresses and firmware actions.</p>";
|
||||
@@ -316,6 +421,15 @@ function renderScanError(msg){
|
||||
return "<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>"+esc(msg||'Failed to reach portal while scanning.')+"</p></div>";
|
||||
}
|
||||
|
||||
function shouldAutoStartWifiScan(data){
|
||||
if(!data)return true;
|
||||
if(data.scanning)return false;
|
||||
if((data.lastscan||0)>0)return false;
|
||||
if(data.results_valid)return false;
|
||||
if(data.error==='timeout' || data.error==='failed')return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function wifiRefresh(){
|
||||
var box=getScanResultsBox();
|
||||
if(!box)return;
|
||||
@@ -360,7 +474,15 @@ function bindWifiViewEvents(){
|
||||
if(el.tagName==='A' && el.getAttribute('data-ssid')){
|
||||
ev.preventDefault();
|
||||
var inp=$('wm-s');
|
||||
var pass=$('wm-p');
|
||||
if(inp)inp.value=el.getAttribute('data-ssid')||'';
|
||||
if(pass){
|
||||
if(pass.scrollIntoView)pass.scrollIntoView({behavior:'smooth',block:'center'});
|
||||
if(pass.focus)pass.focus();
|
||||
}else if(inp){
|
||||
if(inp.scrollIntoView)inp.scrollIntoView({behavior:'smooth',block:'center'});
|
||||
if(inp.focus)inp.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
el=el.parentElement;
|
||||
@@ -375,10 +497,10 @@ function viewWifi(){
|
||||
try{var m=JSON.parse(res.body);}catch(e){m={};}
|
||||
var html=navBar('wifi');
|
||||
html+="<div class='wm-page-head'><h1>WiFi</h1><p class='wm-page-desc'>Pick a network below, confirm SSID and password, then connect.</p></div>";
|
||||
html+="<div class='wm-card'><h2 class='wm-card-title'>Networks nearby</h2>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('scan','Networks nearby');
|
||||
html+="<div id='wm-scan-results'></div>";
|
||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>Scan again</button></div>";
|
||||
html+="<div class='wm-card'><h2 class='wm-card-title'>Network details</h2>";
|
||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button></div>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
|
||||
html+="<form id='wm-wifi-form'>";
|
||||
html+=renderFieldList(m.wifiFields||[]);
|
||||
var wf=m.wifiFields||[];
|
||||
@@ -390,7 +512,7 @@ function viewWifi(){
|
||||
}
|
||||
html+=renderFieldList(m.staticFields||[]);
|
||||
html+=renderFieldList(m.params||[]);
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>Save and connect</button></div></form>";
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('connect','Save and connect')+"</button></div></form>";
|
||||
html+="<div id='wm-wifi-msg'></div></div>";
|
||||
setView(html);
|
||||
bindWifiViewEvents();
|
||||
@@ -398,6 +520,10 @@ function viewWifi(){
|
||||
api('/api/wifi/scan-status').then(function(r2){
|
||||
try{var d=JSON.parse(r2.body);}catch(e){d={};}
|
||||
var box=getScanResultsBox();
|
||||
if(shouldAutoStartWifiScan(d)){
|
||||
wifiRefresh();
|
||||
return;
|
||||
}
|
||||
if(box)box.innerHTML=renderScanList(d);
|
||||
if(d&&d.scanning){startWifiScanPolling(box);}
|
||||
}).catch(function(){
|
||||
@@ -412,18 +538,23 @@ function viewWifi(){
|
||||
|
||||
// --- WiFi connect polling ---
|
||||
var _wmWifiConnectPollTimer=null;
|
||||
var _wmWifiConnectPollToken=0;
|
||||
var _wmWifiConnectPending=false;
|
||||
|
||||
function stopWifiConnectPolling(){
|
||||
_wmWifiConnectPollToken++;
|
||||
if(_wmWifiConnectPollTimer){
|
||||
clearInterval(_wmWifiConnectPollTimer);
|
||||
clearTimeout(_wmWifiConnectPollTimer);
|
||||
_wmWifiConnectPollTimer=null;
|
||||
}
|
||||
}
|
||||
|
||||
function pollWifiConnectStatus(){
|
||||
stopWifiConnectPolling();
|
||||
_wmWifiConnectPollTimer=setInterval(function(){
|
||||
var token=++_wmWifiConnectPollToken;
|
||||
function poll(){
|
||||
api('/api/wifi/connect-status').then(function(res){
|
||||
if(token!==_wmWifiConnectPollToken)return;
|
||||
var j={};
|
||||
var msg=$('wm-wifi-msg');
|
||||
try{j=JSON.parse(res.body);}catch(e){}
|
||||
@@ -432,18 +563,36 @@ function pollWifiConnectStatus(){
|
||||
}
|
||||
if(j.state==='success'){
|
||||
stopWifiConnectPolling();
|
||||
_wmWifiConnectPending=false;
|
||||
if(msg && j.stationIp){
|
||||
msg.innerHTML="Connected to WiFi. Redirecting to <a href='"+esc(j.redirectUrl||("http://"+j.stationIp+"/"))+"'>"+esc(j.stationIp)+"</a>…";
|
||||
}
|
||||
showToast(j.message||'WiFi connected',false);
|
||||
location.hash='#/';
|
||||
if(j.redirectUrl){
|
||||
setTimeout(function(){ window.location.href=j.redirectUrl; }, 1400);
|
||||
}else{
|
||||
location.hash='#/';
|
||||
}
|
||||
}else if(j.state==='failed'){
|
||||
stopWifiConnectPolling();
|
||||
_wmWifiConnectPending=false;
|
||||
showToast(j.message||'WiFi connect failed',true);
|
||||
}else{
|
||||
_wmWifiConnectPollTimer=setTimeout(poll,700);
|
||||
}
|
||||
}).catch(function(){
|
||||
if(token!==_wmWifiConnectPollToken)return;
|
||||
stopWifiConnectPolling();
|
||||
var msg=$('wm-wifi-msg');
|
||||
if(msg)msg.innerHTML='Portal became unreachable while checking connection status.';
|
||||
if(msg){
|
||||
msg.innerHTML=_wmWifiConnectPending
|
||||
? 'Portal became unreachable after saving. If the device joined your WiFi network, reconnect to it there.'
|
||||
: 'Portal became unreachable while checking connection status.';
|
||||
}
|
||||
_wmWifiConnectPending=false;
|
||||
});
|
||||
},700);
|
||||
}
|
||||
poll();
|
||||
}
|
||||
|
||||
window.portalWifiSave=function(ev){
|
||||
@@ -458,6 +607,7 @@ window.portalWifiSave=function(ev){
|
||||
msg.innerHTML=(j&&j.message)?esc(j.message):(res.ok?'Queued.':'Error');
|
||||
}
|
||||
if(res.status===202){
|
||||
_wmWifiConnectPending=true;
|
||||
pollWifiConnectStatus();
|
||||
}
|
||||
});
|
||||
@@ -471,47 +621,41 @@ function viewInfo(){
|
||||
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
||||
var html=navBar('info');
|
||||
html+="<div class='wm-page-head'><h1>Device info</h1><p class='wm-page-desc'>Connection status, hardware, and actions.</p></div>";
|
||||
function section(title,rows){
|
||||
if(!rows||!rows.length)return'';
|
||||
var h="<div class='wm-card'><h2 class='wm-card-title'>"+esc(title)+"</h2><dl class='wm-kv'>";
|
||||
for(var i=0;i<rows.length;i++){
|
||||
h+="<dt>"+esc(rows[i].label)+"</dt><dd>"+rows[i].value+"</dd>";
|
||||
}
|
||||
h+="</dl></div>";
|
||||
return h;
|
||||
}
|
||||
var st=d.status;
|
||||
if(st && typeof st==='object' && !Array.isArray(st)){
|
||||
html+="<div class='wm-card'><h2 class='wm-card-title'>Status</h2><dl class='wm-kv'>";
|
||||
html+="<dt>Connected</dt><dd>"+esc(String(st.connected))+"</dd>";
|
||||
if(st.ssid!==undefined)html+="<dt>SSID</dt><dd>"+esc(st.ssid)+"</dd>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
|
||||
if(st.summary){html+="<p class='wm-callout wm-callout--info wm-info-summary'>"+esc(st.summary)+"</p>";}
|
||||
html+="<dl class='wm-kv'>";
|
||||
html+="<dt>Connected</dt><dd>"+esc(st.connected?'Yes':'No')+"</dd>";
|
||||
if(st.ssid!==undefined && st.ssid!=='')html+="<dt>SSID</dt><dd>"+esc(st.ssid)+"</dd>";
|
||||
if(st.stationIp)html+="<dt>Station IP</dt><dd>"+esc(st.stationIp)+"</dd>";
|
||||
if(st.apIp)html+="<dt>AP IP</dt><dd>"+esc(st.apIp)+"</dd>";
|
||||
if(st.summary)html+="<dt>Summary</dt><dd>"+esc(st.summary)+"</dd>";
|
||||
if(st.apIp)html+="<dt>Portal IP</dt><dd>"+esc(st.apIp)+"</dd>";
|
||||
html+="</dl></div>";
|
||||
} else {
|
||||
html+=section('Status',d.status||[]);
|
||||
html+=sectionCardHtml('status','Status','Connection summary and main addresses.',d.status||[]);
|
||||
}
|
||||
html+=section('Device',d.device||[]);
|
||||
html+=section('WiFi',d.wifi||[]);
|
||||
html+=section('About',d.about||[]);
|
||||
html+=sectionCardHtml('device','Device','Runtime, memory, and hardware details.',d.device||[]);
|
||||
html+=sectionCardHtml('diagnostics','WiFi details','Network diagnostics and interface metadata.',rowsWithoutKeys(d.wifi||[],['conx','stassid','staip','apip']));
|
||||
html+=sectionCardHtml('about','About','Library, SDK, and build information.',d.about||[]);
|
||||
if(d.extraSections){
|
||||
var xi,zs,zrows,zi,it;
|
||||
for(xi=0;xi<d.extraSections.length;xi++){
|
||||
zs=d.extraSections[xi];
|
||||
zrows=[];
|
||||
if(zs.items){for(zi=0;zi<zs.items.length;zi++){it=zs.items[zi];zrows.push({label:it.label,value:esc(it.value)});}}
|
||||
html+=section(zs.title,zrows);
|
||||
html+=sectionCardHtml('info',zs.title||'Details','',zrows);
|
||||
}
|
||||
}
|
||||
var act=d.actions||{};
|
||||
html+="<div class='wm-card'><h2 class='wm-card-title'>Actions</h2><div class='wm-btn-group'>";
|
||||
if(act.showRestart)html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>Restart device</button>";
|
||||
if(act.showExitPortal)html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>Exit portal</button>";
|
||||
if(act.showCloseCaptive)html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>Stop captive portal</button>";
|
||||
if(act.showUpdate)html+="<a class='wm-btn wm-btn--secondary wm-btn--block' href='#/update'>Firmware update</a>";
|
||||
if(act.showErase)html+="<button type='button' class='wm-btn wm-btn--danger wm-btn--block' id='wm-erase-btn'>Erase WiFi config</button>";
|
||||
html+="</div></div>";
|
||||
var actionsHtml='', hasActions=false;
|
||||
if(act.showRestart){actionsHtml+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-restart-btn'>"+labelWithIcon('restart','Restart device')+"</button>";hasActions=true;}
|
||||
if(act.showExitPortal){actionsHtml+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-exit-btn'>"+labelWithIcon('exit','Exit portal')+"</button>";hasActions=true;}
|
||||
if(act.showCloseCaptive){actionsHtml+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-close-captive-btn'>"+labelWithIcon('close','Stop captive portal')+"</button>";hasActions=true;}
|
||||
if(act.showUpdate){actionsHtml+="<a class='wm-btn wm-btn--secondary wm-btn--block' href='#/update'>"+labelWithIcon('update','Firmware update')+"</a>";hasActions=true;}
|
||||
if(act.showErase){actionsHtml+="<button type='button' class='wm-btn wm-btn--danger wm-btn--block' id='wm-erase-btn'>"+labelWithIcon('erase','Erase WiFi config')+"</button>";hasActions=true;}
|
||||
if(hasActions){
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('actions','Actions')+"<p class='wm-card-subtitle'>Maintenance and recovery actions for this device.</p><div class='wm-btn-group'>"+actionsHtml+"</div></div>";
|
||||
}
|
||||
setView(html);
|
||||
});
|
||||
}
|
||||
@@ -548,7 +692,7 @@ function viewSetup(){
|
||||
html+="<div class='wm-page-head'><h1>Setup</h1><p class='wm-page-desc'>Custom parameters stored on this device.</p></div>";
|
||||
html+="<div class='wm-card'><form id='wm-param-form'>";
|
||||
html+=renderFieldList(d.params||[]);
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>Save parameters</button></div>";
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('save','Save parameters')+"</button></div>";
|
||||
html+="</form><div id='wm-param-msg'></div></div>";
|
||||
setView(html);
|
||||
});
|
||||
@@ -579,7 +723,7 @@ function viewUpdate(){
|
||||
html+="<div class='wm-card'><form id='wm-ota-form'>";
|
||||
html+="<div class='wm-field'><label for='wm-ota-file'>Firmware file</label>";
|
||||
html+="<input type='file' id='wm-ota-file' name='update' accept='.bin,.bin.gz'/></div>";
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>Upload firmware</button></div>";
|
||||
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('upload','Upload firmware')+"</button></div>";
|
||||
html+="</form>";
|
||||
html+="<p class='wm-lead' style='margin-top:14px;margin-bottom:8px'>Choose the correct binary for this chip. Upload progress appears below.</p>";
|
||||
html+="<pre id='wm-ota-log'></pre></div>";
|
||||
|
||||
@@ -684,7 +684,9 @@ void WiFiManager::queuePortalConnect(const String& ssid, const String& pass) {
|
||||
_cpConnectQueuedAt = millis();
|
||||
_cpConnectStartedAt = 0;
|
||||
_cpConnectDelayUntil = 0;
|
||||
_cpConnectCloseAt = 0;
|
||||
_cpConnectTimeoutMs = (_saveTimeout > 0) ? (unsigned long)_saveTimeout : _connectTimeout;
|
||||
_cpConnectStationIp = "";
|
||||
_cpConnectState = wm_cp_connect_state_t::queued;
|
||||
_cpConnectMessage = ssid.length() ? F("WiFi connect queued") : F("Settings saved");
|
||||
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_QUEUED);
|
||||
@@ -692,9 +694,24 @@ void WiFiManager::queuePortalConnect(const String& ssid, const String& pass) {
|
||||
|
||||
void WiFiManager::processPortalConnect() {
|
||||
const unsigned long now = millis();
|
||||
const unsigned long successHandoffDelayMs = 2500UL;
|
||||
auto finalizePortalConnectSuccess = [&]() {
|
||||
_cpConnectState = wm_cp_connect_state_t::success;
|
||||
if (_savewificallback != NULL) {
|
||||
#ifndef WM_NO_LOG
|
||||
log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("[CB] _savewificallback calling"));
|
||||
#endif
|
||||
_savewificallback();
|
||||
}
|
||||
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_SUCCESS);
|
||||
if (_disableConfigPortal) {
|
||||
shutdownConfigPortal();
|
||||
}
|
||||
};
|
||||
auto failPortalConnect = [&](uint8_t status, const String& message) {
|
||||
_cpConnectStatus = status;
|
||||
_cpConnectMessage = message;
|
||||
_cpConnectStationIp = "";
|
||||
_cpConnectState = wm_cp_connect_state_t::failed;
|
||||
updateConxResult(status);
|
||||
if (_shouldBreakAfterConfig) {
|
||||
@@ -711,14 +728,16 @@ void WiFiManager::processPortalConnect() {
|
||||
|
||||
switch (_cpConnectState) {
|
||||
case wm_cp_connect_state_t::idle:
|
||||
case wm_cp_connect_state_t::success:
|
||||
case wm_cp_connect_state_t::failed:
|
||||
return;
|
||||
case wm_cp_connect_state_t::success:
|
||||
return;
|
||||
case wm_cp_connect_state_t::queued:
|
||||
resetAsyncScan(false);
|
||||
if (_cpConnectSsid.length() == 0) {
|
||||
_cpConnectStatus = WL_IDLE_STATUS;
|
||||
_cpConnectMessage = F("Settings saved");
|
||||
_cpConnectStationIp = "";
|
||||
_cpConnectState = wm_cp_connect_state_t::success;
|
||||
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_SUCCESS);
|
||||
return;
|
||||
@@ -746,6 +765,7 @@ void WiFiManager::processPortalConnect() {
|
||||
}
|
||||
_cpConnectStatus = WL_IDLE_STATUS;
|
||||
_cpConnectMessage = F("Settings saved");
|
||||
_cpConnectStationIp = "";
|
||||
_cpConnectState = wm_cp_connect_state_t::success;
|
||||
if (_savewificallback != NULL) {
|
||||
#ifndef WM_NO_LOG
|
||||
@@ -771,17 +791,15 @@ void WiFiManager::processPortalConnect() {
|
||||
_cpConnectStatus = status;
|
||||
if (status == WL_CONNECTED) {
|
||||
updateConxResult(status);
|
||||
_cpConnectMessage = F("WiFi connected");
|
||||
_cpConnectState = wm_cp_connect_state_t::success;
|
||||
if (_savewificallback != NULL) {
|
||||
#ifndef WM_NO_LOG
|
||||
log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("[CB] _savewificallback calling"));
|
||||
#endif
|
||||
_savewificallback();
|
||||
}
|
||||
emitPortalEvent(WM_EVENT_PORTAL_CONNECT_SUCCESS);
|
||||
if (_disableConfigPortal) {
|
||||
shutdownConfigPortal();
|
||||
_cpConnectStationIp = WiFi.localIP().toString();
|
||||
_cpConnectMessage = _cpConnectStationIp.length() > 0
|
||||
? String(F("WiFi connected. Redirecting to ")) + _cpConnectStationIp
|
||||
: String(F("WiFi connected"));
|
||||
if (_disableConfigPortal && (configPortalActive || webPortalActive)) {
|
||||
_cpConnectCloseAt = now + successHandoffDelayMs;
|
||||
_cpConnectState = wm_cp_connect_state_t::success_waiting_close;
|
||||
} else {
|
||||
finalizePortalConnectSuccess();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -794,6 +812,11 @@ void WiFiManager::processPortalConnect() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
case wm_cp_connect_state_t::success_waiting_close:
|
||||
if (now >= _cpConnectCloseAt) {
|
||||
finalizePortalConnectSuccess();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2233,6 +2256,7 @@ WiFiManager::wm_configportal_connect_state_t WiFiManager::getConfigPortalConnect
|
||||
case wm_cp_connect_state_t::starting:
|
||||
case wm_cp_connect_state_t::waiting:
|
||||
return WM_CP_CONNECT_WAITING;
|
||||
case wm_cp_connect_state_t::success_waiting_close:
|
||||
case wm_cp_connect_state_t::success:
|
||||
return WM_CP_CONNECT_SUCCESS;
|
||||
case wm_cp_connect_state_t::failed:
|
||||
@@ -2245,11 +2269,13 @@ WiFiManager::wm_configportal_connect_state_t WiFiManager::getConfigPortalConnect
|
||||
|
||||
bool WiFiManager::isConfigPortalConnectPending() const {
|
||||
return _cpConnectState == wm_cp_connect_state_t::queued || _cpConnectState == wm_cp_connect_state_t::delaying
|
||||
|| _cpConnectState == wm_cp_connect_state_t::starting || _cpConnectState == wm_cp_connect_state_t::waiting;
|
||||
|| _cpConnectState == wm_cp_connect_state_t::starting || _cpConnectState == wm_cp_connect_state_t::waiting
|
||||
|| _cpConnectState == wm_cp_connect_state_t::success_waiting_close;
|
||||
}
|
||||
|
||||
bool WiFiManager::didConfigPortalConnectSucceed() const {
|
||||
return _cpConnectState == wm_cp_connect_state_t::success;
|
||||
return _cpConnectState == wm_cp_connect_state_t::success
|
||||
|| _cpConnectState == wm_cp_connect_state_t::success_waiting_close;
|
||||
}
|
||||
|
||||
bool WiFiManager::didConfigPortalConnectFail() const {
|
||||
|
||||
@@ -129,6 +129,15 @@ struct PortalShellRenderBundle {
|
||||
appendJsDescriptor{} {}
|
||||
};
|
||||
|
||||
// Custom HTML attribute strings (e.g. from DeviceFramework generateCustomHTML) may include
|
||||
// type='password' — the JSON field descriptor must echo that or the SPA renders type=text.
|
||||
bool portalCustomAttrsIndicatePassword(const String& customAttrs) {
|
||||
if (customAttrs.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
return customAttrs.indexOf("type='password'") >= 0 || customAttrs.indexOf("type=\"password\"") >= 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WiFiManagerHandlers::WiFiManagerHandlers(WiFiManager* wm) : _wm(wm) {}
|
||||
@@ -308,31 +317,31 @@ void WiFiManagerHandlers::appendPortalJsonCustomParams(String& json, bool& first
|
||||
first = false;
|
||||
if (p->getID() != nullptr) {
|
||||
String pid = String(p->getID());
|
||||
const String customAttrs = String(p->getCustomHTML());
|
||||
json += F("{\"kind\":\"field\",\"name\":\"");
|
||||
jsonAppendEscaped(json, pid);
|
||||
json += F("\",\"id\":\"");
|
||||
jsonAppendEscaped(json, pid);
|
||||
json += F("\",\"label\":\"");
|
||||
jsonAppendEscaped(json, String(p->getLabel()));
|
||||
json += F("\",\"type\":\"text\",\"maxlength\":");
|
||||
json += F("\",\"type\":\"");
|
||||
json += portalCustomAttrsIndicatePassword(customAttrs) ? F("password") : F("text");
|
||||
json += F("\",\"maxlength\":");
|
||||
json += String(p->getValueLength());
|
||||
json += F(",\"value\":\"");
|
||||
jsonAppendEscaped(json, String(p->getValue()));
|
||||
json += F("\",\"labelPlacement\":");
|
||||
json += String(p->getLabelPlacement());
|
||||
{
|
||||
String c = p->getCustomHTML();
|
||||
if (c.length() > 0) {
|
||||
json += F(",\"customAttrs\":\"");
|
||||
jsonAppendEscaped(json, c);
|
||||
json += F("\"");
|
||||
}
|
||||
if (customAttrs.length() > 0) {
|
||||
json += F(",\"customAttrs\":\"");
|
||||
jsonAppendEscaped(json, customAttrs);
|
||||
json += F("\"");
|
||||
}
|
||||
json += F("}");
|
||||
} else {
|
||||
json += F("{\"kind\":\"html\",\"html\":\"");
|
||||
jsonAppendEscaped(json, String(p->getCustomHTML()));
|
||||
json += F("}");
|
||||
json += F("\"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,7 +358,7 @@ void WiFiManagerHandlers::appendJsonKvItem(String& json, bool& first, const char
|
||||
jsonAppendEscaped(json, label);
|
||||
json += F("\",\"value\":\"");
|
||||
jsonAppendEscaped(json, value);
|
||||
json += F("}");
|
||||
json += F("\"}");
|
||||
}
|
||||
|
||||
void WiFiManagerHandlers::appendInfoSectionFromIds(String& json, const char* const* ids, size_t count, bool& first) {
|
||||
@@ -1249,7 +1258,19 @@ String WiFiManagerHandlers::buildApiWifiConnectStatusJson() {
|
||||
jsonAppendEscaped(json, _wm->getConfigPortalConnectMessage());
|
||||
json += F("\",\"wifiStatus\":\"");
|
||||
jsonAppendEscaped(json, _wm->getWLStatusString(_wm->getConfigPortalConnectStatus()));
|
||||
json += F("\"}");
|
||||
json += F("\"");
|
||||
if (_wm->_cpConnectStationIp.length() > 0) {
|
||||
json += F(",\"stationIp\":\"");
|
||||
jsonAppendEscaped(json, _wm->_cpConnectStationIp);
|
||||
json += F("\",\"redirectUrl\":\"http://");
|
||||
jsonAppendEscaped(json, _wm->_cpConnectStationIp);
|
||||
if (_wm->_httpPort != 80) {
|
||||
json += F(":");
|
||||
json += String(_wm->_httpPort);
|
||||
}
|
||||
json += F("/\"");
|
||||
}
|
||||
json += F("}");
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "WiFiManager",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.19",
|
||||
"keywords": [
|
||||
"wifi",
|
||||
"wi-fi",
|
||||
|
||||
@@ -11,6 +11,8 @@ TestCase tests[] = {
|
||||
// JSON API shape (buildApiInfoJson is large — run with fresh heap, before stress tests)
|
||||
TEST_ENTRY(test_captive_redirect_host_rule),
|
||||
TEST_ENTRY(test_api_wifi_meta_json_shape),
|
||||
TEST_ENTRY(test_api_wifi_meta_password_field_type),
|
||||
TEST_ENTRY(test_api_wifi_connect_status_success_redirect),
|
||||
TEST_ENTRY(test_api_info_json_shape),
|
||||
TEST_ENTRY(test_api_params_json_shape),
|
||||
TEST_ENTRY(test_api_status_json_shape),
|
||||
|
||||
@@ -113,6 +113,8 @@ void test_shell_template_renders_dynamic_styles_with_percent_values();
|
||||
// API JSON + captive helper tests
|
||||
void test_captive_redirect_host_rule();
|
||||
void test_api_wifi_meta_json_shape();
|
||||
void test_api_wifi_meta_password_field_type();
|
||||
void test_api_wifi_connect_status_success_redirect();
|
||||
void test_api_info_json_shape();
|
||||
void test_api_params_json_shape();
|
||||
void test_api_status_json_shape();
|
||||
|
||||
@@ -36,10 +36,48 @@ void test_api_wifi_meta_json_shape() {
|
||||
j = handlers.buildApiWifiMetaJson();
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"kind\":\"field\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"kind\":\"html\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"html\":\"<div class='wm-callout wm-callout--info'><p>GPS settings...</p></div>\"}")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"wifiFields\":[{\"id\":\"s\"")));
|
||||
|
||||
Serial.println("[TEST] WiFi meta JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_wifi_meta_password_field_type() {
|
||||
Serial.println("[TEST] Testing /api/wifi/meta password field type from custom attrs...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
WiFiManagerParameter pwd("mqttpass", "MQTT password", "secret", 24, " type='password' autocorrect='off'");
|
||||
wm.portalAddParameter(&pwd);
|
||||
|
||||
String j = handlers.buildApiWifiMetaJson();
|
||||
const char* p = j.c_str();
|
||||
const char* chunk = strstr(p, "\"id\":\"mqttpass\"");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(chunk, "mqttpass param missing from meta JSON");
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(strstr(chunk, "\"type\":\"password\""),
|
||||
"customAttrs type=password must map to JSON type password");
|
||||
|
||||
Serial.println("[TEST] WiFi meta password field type test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_wifi_connect_status_success_redirect() {
|
||||
Serial.println("[TEST] Testing /api/wifi/connect-status success redirect payload...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
#ifdef UNIT_TEST
|
||||
wm.wmTestSetPortalConnectSuccess("WiFi connected. Redirecting to 192.168.1.42", "192.168.1.42");
|
||||
#endif
|
||||
String j = handlers.buildApiWifiConnectStatusJson();
|
||||
const char* p = j.c_str();
|
||||
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"state\":\"success\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"stationIp\":\"192.168.1.42\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"redirectUrl\":\"http://192.168.1.42/\""));
|
||||
|
||||
Serial.println("[TEST] WiFi connect-status success redirect payload test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_info_json_shape() {
|
||||
Serial.println("[TEST] Testing /api/info JSON shape...");
|
||||
|
||||
@@ -67,6 +105,8 @@ void test_api_info_json_shape() {
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"title\":\"Battery\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"label\":\"State of charge\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"showUpdate\":"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"key\":\"aboutver\",\"label\":\"WiFiManager\",\"value\":\""));
|
||||
TEST_ASSERT_NOT_NULL(strstr(p, "\"},{\"key\":\"aboutarduinover\""));
|
||||
|
||||
Serial.println("[TEST] API info JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user