mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-16 07:51:36 +10:00
feat: refine portal flow and examples
This commit is contained in:
@@ -268,7 +268,7 @@ struct PortalHomeCard {
|
||||
struct PortalBrandState {
|
||||
WiFiManagerPortalText title;
|
||||
WiFiManagerPortalText identityTextOverride;
|
||||
WiFiManagerPortalText homeIntro;
|
||||
WiFiManagerPortalText tagline;
|
||||
WiFiManagerPortalAsset logo;
|
||||
WiFiManagerPortalText logoAltText;
|
||||
};
|
||||
@@ -915,7 +915,7 @@ class WiFiManager
|
||||
// preload scanning causes AP to delay showing for users, but also caches and lets the cp load faster once its open
|
||||
// my scan takes 7-10 seconds
|
||||
public:
|
||||
boolean _preloadwifiscan = false; // preload wifiscan if true
|
||||
boolean _preloadwifiscan = true; // begin one asynchronous scan as the portal starts
|
||||
unsigned int _scancachetime = 30000; // ms cache time for preload scans
|
||||
|
||||
protected:
|
||||
@@ -963,6 +963,7 @@ protected:
|
||||
void handleStationAttemptFailure(uint8_t status, const String& message);
|
||||
void handleStationConnectionSuccess();
|
||||
void completePortalStationAttempt(bool success, uint8_t status, const String& message);
|
||||
void acknowledgePortalConnectHandoff();
|
||||
void enterStationPortal();
|
||||
bool hasUsableStationConnection() const;
|
||||
bool isStationProfileEnabled(const WiFiManagerStationProfiles& profiles, uint8_t slot) const;
|
||||
@@ -1083,6 +1084,7 @@ protected:
|
||||
_cpConnectStationIp = stationIp;
|
||||
_cpConnectStatus = status;
|
||||
}
|
||||
void wmTestCompleteProfilePortalConnectionSuccess();
|
||||
void wmTestSetPortalConnectFailure(const String& message, uint8_t status = WL_CONNECT_FAILED) {
|
||||
_cpConnectState = wm_cp_connect_state_t::failed;
|
||||
_cpConnectMessage = message;
|
||||
|
||||
@@ -55,6 +55,8 @@ class WiFiManagerHandlers {
|
||||
void handleApiWifiMeta(AsyncWebServerRequest *request);
|
||||
void handleApiWifiSave(AsyncWebServerRequest *request);
|
||||
void handleApiWifiConnectStatus(AsyncWebServerRequest *request);
|
||||
void handleApiWifiConnectComplete(AsyncWebServerRequest *request);
|
||||
void handleApiPortalTimeoutReset(AsyncWebServerRequest *request);
|
||||
void handleApiParamsGet(AsyncWebServerRequest *request);
|
||||
void handleApiParamsSave(AsyncWebServerRequest *request);
|
||||
void handleApiInfo(AsyncWebServerRequest *request);
|
||||
|
||||
@@ -84,7 +84,7 @@ struct WiFiManagerPortalTheme {
|
||||
struct WiFiManagerPortalConfig {
|
||||
WiFiManagerPortalText title;
|
||||
WiFiManagerPortalText identityText;
|
||||
WiFiManagerPortalText homeIntro;
|
||||
WiFiManagerPortalText tagline;
|
||||
WiFiManagerPortalAsset logo;
|
||||
WiFiManagerPortalText logoAltText;
|
||||
WiFiManagerPortalTheme theme;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
* GET /api/wifi/meta
|
||||
* POST /api/wifi/save
|
||||
* GET /api/wifi/connect-status
|
||||
* POST /api/wifi/connect-complete
|
||||
* POST /api/portal/timeout-reset
|
||||
* GET /api/params
|
||||
* POST /api/params/save
|
||||
* GET /api/info
|
||||
@@ -55,6 +57,8 @@ const char R_api_wifi_scan[] PROGMEM = "/api/wifi/scan";
|
||||
const char R_api_wifi_meta[] PROGMEM = "/api/wifi/meta";
|
||||
const char R_api_wifi_save[] PROGMEM = "/api/wifi/save";
|
||||
const char R_api_wifi_connect_status[] PROGMEM = "/api/wifi/connect-status";
|
||||
const char R_api_wifi_connect_complete[] PROGMEM = "/api/wifi/connect-complete";
|
||||
const char R_api_portal_timeout_reset[] PROGMEM = "/api/portal/timeout-reset";
|
||||
const char R_api_params[] PROGMEM = "/api/params";
|
||||
const char R_api_params_save[] PROGMEM = "/api/params/save";
|
||||
const char R_api_info[] PROGMEM = "/api/info";
|
||||
|
||||
@@ -74,19 +74,21 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
".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}"
|
||||
".wm-hero-sub{font-size:1rem;color:var(--wm-muted);font-weight:500;margin:0}"
|
||||
".wm-brand-logo{margin-bottom:12px;line-height:0}"
|
||||
".wm-brand-logo svg{display:block;max-width:160px;height:auto}"
|
||||
".wm-hero-intro{font-size:.95rem;color:var(--wm-muted);margin:.5em 0 0;line-height:1.45;max-width:42ch}"
|
||||
/* Header / page chrome */
|
||||
".wm-site-header{display:flex;align-items:center;gap:10px;min-height:40px;margin:0 0 12px}"
|
||||
".wm-site-logo{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:0 0 40px;line-height:0}"
|
||||
".wm-site-logo svg{display:block;width:100%;height:100%}"
|
||||
".wm-site-brand-copy{min-width:0}"
|
||||
".wm-site-brand-name{margin:0;font-size:.78rem;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--wm-text)}"
|
||||
".wm-site-tagline{margin:2px 0 0;font-size:.82rem;line-height:1.25;color:var(--wm-muted)}"
|
||||
".wm-page-head{margin-bottom:16px}"
|
||||
".wm-page-head h1{font-size:1.45rem}"
|
||||
".wm-page-desc{font-size:.95rem;color:var(--wm-muted);margin:.35em 0 0}"
|
||||
/* Cards */
|
||||
".wm-card{background:var(--wm-surface);border:1px solid var(--wm-border);border-radius:var(--wm-radius);"
|
||||
"box-shadow:var(--wm-shadow);padding:18px 16px;margin-bottom:16px;text-align:left}"
|
||||
".wm-scan-card,.wm-ota-card{position:relative;overflow:hidden}"
|
||||
".wm-scan-results{min-height:126px}"
|
||||
".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)}"
|
||||
@@ -120,6 +122,31 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
".wm-checkbox-row{display:flex;align-items:center;gap:8px;margin:12px 0;font-size:.92rem}"
|
||||
".wm-checkbox-row label{display:inline;margin:0;font-weight:500;color:var(--wm-text)}"
|
||||
".wm-form-actions{margin-top:16px}"
|
||||
".wm-wifi-details-card{position:relative}"
|
||||
".wm-wifi-save-overlay{position:fixed;inset:0;z-index:10002;display:none;align-items:center;justify-content:center;padding:18px;"
|
||||
"box-sizing:border-box;background:rgba(15,23,42,.58)}"
|
||||
".wm-wifi-save-panel{display:flex;align-items:center;gap:14px;width:100%;max-width:340px;padding:18px;border:1px solid var(--wm-border);"
|
||||
"border-radius:var(--wm-radius-sm);background:var(--wm-surface);box-shadow:var(--wm-shadow);text-align:left}"
|
||||
".wm-wifi-save-panel strong{display:block;font-size:1rem;color:var(--wm-text)}"
|
||||
".wm-wifi-save-panel p{margin:4px 0 0;font-size:.9rem;line-height:1.45;color:var(--wm-muted)}"
|
||||
".wm-operation-overlay{position:absolute;inset:0;z-index:5;display:none;align-items:center;justify-content:center;padding:16px;background:rgba(15,23,42,.56)}"
|
||||
".wm-operation-panel{display:flex;align-items:center;gap:12px;width:100%;max-width:310px;padding:16px;border:1px solid var(--wm-border);border-radius:var(--wm-radius-sm);background:var(--wm-surface);box-shadow:var(--wm-shadow);text-align:left}"
|
||||
".wm-operation-panel strong{display:block;font-size:.98rem;color:var(--wm-text)}"
|
||||
".wm-operation-panel p{margin:3px 0 0;font-size:.88rem;line-height:1.4;color:var(--wm-muted)}"
|
||||
".wm-operation-progress{margin-top:10px}"
|
||||
".wm-operation-progress-track{height:7px;overflow:hidden;border-radius:999px;background:var(--wm-border)}"
|
||||
".wm-operation-progress-track span{display:block;width:0;height:100%;border-radius:inherit;background:var(--wm-brand);transition:width .15s ease}"
|
||||
".wm-operation-progress-label{display:block;margin-top:4px;font-size:.8rem;font-weight:700;color:var(--wm-muted);text-align:right}"
|
||||
".wm-portal-timeout-row{display:flex;align-items:stretch;gap:8px;margin:0 0 12px}"
|
||||
".wm-portal-timeout-row .wm-status{flex:1;margin:0}"
|
||||
".wm-icon-button{display:inline-flex;align-items:center;justify-content:center;width:42px;min-height:42px;padding:0;border:1px solid var(--wm-border);border-radius:var(--wm-radius-sm);background:var(--wm-bg);color:var(--wm-text);cursor:pointer}"
|
||||
".wm-icon-button:hover{border-color:var(--wm-brand);color:var(--wm-brand)}"
|
||||
".wm-icon-button:disabled{opacity:.5;cursor:not-allowed}"
|
||||
".wm-icon-button .wm-icon{width:1.05rem;height:1.05rem}"
|
||||
".wm-inline-link{display:inline-block;margin-top:12px;font-size:.92rem}"
|
||||
".wm-spinner{width:24px;height:24px;flex:0 0 24px;border:3px solid var(--wm-border);border-top-color:var(--wm-brand);border-radius:50%;animation:wm-spin .8s linear infinite}"
|
||||
"@keyframes wm-spin{to{transform:rotate(360deg)}}"
|
||||
"@media (prefers-reduced-motion:reduce){.wm-spinner{animation:none;border-right-color:var(--wm-brand)}}"
|
||||
/* Buttons */
|
||||
".wm-btn{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 18px;"
|
||||
"font-size:1rem;font-weight:600;font-family:inherit;border-radius:var(--wm-radius-sm);border:0;cursor:pointer;"
|
||||
@@ -180,8 +207,6 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
".wm-kv dd:last-of-type{border-bottom:0}"
|
||||
/* Feedback areas */
|
||||
"#wm-wifi-msg,#wm-param-msg{font-size:.9rem;margin-top:12px;min-height:1.2em;color:var(--wm-muted)}"
|
||||
"#wm-ota-log{font-size:.8rem;background:var(--wm-bg);padding:12px;border-radius:var(--wm-radius-sm);"
|
||||
"border:1px solid var(--wm-border);overflow:auto;max-height:160px}"
|
||||
// invert legacy body
|
||||
"body.invert{background-color:var(--wm-bg)}"
|
||||
"body.invert .msg{border-color:var(--wm-border)}"
|
||||
|
||||
@@ -120,7 +120,7 @@ function startPortalTimeoutCountdown(initialSeconds){
|
||||
function render(){
|
||||
if(!el)return;
|
||||
el.className='wm-status'+(levelClass(remaining)?' '+levelClass(remaining):'');
|
||||
el.textContent='Captive portal timeout: '+formatDurationSeconds(remaining);
|
||||
el.textContent='Portal closes in '+formatDurationSeconds(remaining);
|
||||
}
|
||||
render();
|
||||
_wmPortalTimeoutTimer=setInterval(function(){
|
||||
@@ -260,6 +260,28 @@ function rowsWithoutKeys(rows,blocked){
|
||||
return out;
|
||||
}
|
||||
|
||||
function portalHeader(){
|
||||
var brand=boot.brand||{};
|
||||
var ctx=boot.context||{};
|
||||
var identity=esc(ctx.identityText||brand.title||'WiFiManager');
|
||||
var logoAlt=esc(brand.logoAltText||'');
|
||||
var h="<header class='wm-site-header'>";
|
||||
if(brand.logoSvg){h+="<div class='wm-site-logo'"+(logoAlt?" role='img' aria-label='"+logoAlt+"'":"")+">"+brand.logoSvg+"</div>";}
|
||||
h+="<div class='wm-site-brand-copy'><p class='wm-site-brand-name'>"+identity+"</p>";
|
||||
if(brand.tagline){h+="<p class='wm-site-tagline'>"+esc(brand.tagline)+"</p>";}
|
||||
h+="</div></header>";
|
||||
return h;
|
||||
}
|
||||
|
||||
function applyPortalFavicon(){
|
||||
var brand=boot.brand||{};
|
||||
if(!brand.logoSvg)return;
|
||||
var icon=document.querySelector("link[rel~='icon']");
|
||||
if(!icon){icon=document.createElement('link');icon.rel='icon';document.head.appendChild(icon);}
|
||||
icon.type='image/svg+xml';
|
||||
icon.href='data:image/svg+xml,'+encodeURIComponent(brand.logoSvg);
|
||||
}
|
||||
|
||||
function navBar(active){
|
||||
active=active||'home';
|
||||
var pages=boot.pages||{};
|
||||
@@ -269,11 +291,11 @@ function navBar(active){
|
||||
var ps=pages.setup||{};
|
||||
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='#/'>"+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>";}
|
||||
var h=portalHeader()+"<nav class='wm-nav' aria-label='Configuration'>";
|
||||
h+="<a class='wm-nav-link"+(active==='home'?' wm-nav-link--active':'')+"' href='#/'>"+labelWithIcon('home','Overview')+"</a>";
|
||||
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>"+labelWithIcon('wifi','Wi-Fi')+"</a>";
|
||||
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>"+labelWithIcon('setup','Settings')+"</a>";}
|
||||
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>"+labelWithIcon('info','Device')+"</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>";
|
||||
@@ -311,30 +333,91 @@ function renderExtraHomeCards(cards){
|
||||
return out;
|
||||
}
|
||||
|
||||
function renderHomeStatusDetails(info){
|
||||
var st=info.status||{};
|
||||
var rows=[];
|
||||
if(st.connected!==undefined)rows.push({label:'Wi-Fi',value:st.connected?'Connected':'Not connected'});
|
||||
if(st.ssid)rows.push({label:'Network',value:st.ssid});
|
||||
if(st.connected&&st.stationIp&&st.stationIp!=='0.0.0.0'&&st.stationIp!=='(IP unset)')rows.push({label:'Address',value:st.stationIp});
|
||||
else if(st.apIp)rows.push({label:'Portal',value:st.apIp});
|
||||
var device=info.device||[];
|
||||
var wanted={uptime:true,freeheap:true};
|
||||
for(var i=0;i<device.length;i++){
|
||||
if(device[i]&&wanted[device[i].key]){
|
||||
rows.push({label:device[i].label||device[i].key,value:device[i].value||''});
|
||||
}
|
||||
}
|
||||
if(!rows.length)return"<p class='wm-lead'>Device details are unavailable.</p>";
|
||||
var html="<dl class='wm-kv wm-home-status-details'>";
|
||||
for(var j=0;j<rows.length;j++){
|
||||
html+="<dt>"+esc(rows[j].label)+"</dt>";
|
||||
html+="<dd>"+esc(rows[j].value)+"</dd>";
|
||||
}
|
||||
return html+="</dl>";
|
||||
}
|
||||
|
||||
function loadHomeStatus(){
|
||||
var box=$('wm-home-status-details');
|
||||
if(!box)return;
|
||||
api('/api/info').then(function(res){
|
||||
var info={};
|
||||
try{info=JSON.parse(res.body);}catch(e){}
|
||||
box.innerHTML=renderHomeStatusDetails(info);
|
||||
}).catch(function(){
|
||||
box.innerHTML="<p class='wm-lead'>Unable to load device details.</p>";
|
||||
});
|
||||
}
|
||||
|
||||
function resetPortalTimeout(){
|
||||
var button=$('wm-reset-portal-timeout');
|
||||
if(button)button.disabled=true;
|
||||
api('/api/portal/timeout-reset',{method:'POST'}).then(function(res){
|
||||
var data={};
|
||||
try{data=JSON.parse(res.body);}catch(e){}
|
||||
if(!res.ok){
|
||||
showToast(data.message||'Unable to reset portal timeout.',true);
|
||||
return;
|
||||
}
|
||||
startPortalTimeoutCountdown(data.timeoutSecondsRemaining||0);
|
||||
showToast('Portal timeout reset.',false);
|
||||
}).catch(function(){
|
||||
showToast('Unable to reset portal timeout.',true);
|
||||
}).then(function(){
|
||||
if(button)button.disabled=false;
|
||||
});
|
||||
}
|
||||
|
||||
function viewHome(){
|
||||
var brand=boot.brand||{};
|
||||
var ctx=boot.context||{};
|
||||
var t=esc(brand.title||'WiFiManager');
|
||||
var ident=ctx.identityText?esc(ctx.identityText):'';
|
||||
var title=esc(brand.title||'WiFiManager');
|
||||
var summary=ctx.statusSummary||'';
|
||||
var timeoutSeconds=ctx.portalTimeoutSecondsRemaining||0;
|
||||
var html=navBar('home');
|
||||
html+="<header class='wm-hero'>";
|
||||
var logoAlt=esc(brand.logoAltText||'');
|
||||
if(brand.logoSvg){html+="<div class='wm-brand-logo'"+(logoAlt?" role='img' aria-label='"+logoAlt+"'":"")+">"+brand.logoSvg+"</div>";}
|
||||
html+="<h1 class='wm-hero-title'>"+t+"</h1>";
|
||||
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'>"+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>";
|
||||
html+="</div>";
|
||||
html+="<div class='wm-page-head'><h1>"+title+"</h1></div>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('status','Device status');
|
||||
if(summary){
|
||||
html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";
|
||||
}
|
||||
if(timeoutSeconds>0){
|
||||
html+="<div class='wm-portal-timeout-row'>";
|
||||
html+="<p class='wm-status' id='wm-portal-timeout'></p>";
|
||||
html+="<button type='button' class='wm-icon-button'";
|
||||
html+=" id='wm-reset-portal-timeout' aria-label='Reset portal timeout'";
|
||||
html+=" title='Reset portal timeout'>"+iconSvg('restart')+"</button></div>";
|
||||
}
|
||||
html+="<div id='wm-home-status-details'>";
|
||||
html+="<p class='wm-lead'>Loading device details…</p></div>";
|
||||
html+="<a class='wm-inline-link' href='#/info'>View full device details</a></div>";
|
||||
html+=renderExtraHomeCards(boot.extraHomeCards||[]);
|
||||
html+=deviceActionsHtml();
|
||||
setView(html);
|
||||
if(timeoutSeconds>0)startPortalTimeoutCountdown(timeoutSeconds);
|
||||
if(timeoutSeconds>0){
|
||||
startPortalTimeoutCountdown(timeoutSeconds);
|
||||
var reset=$('wm-reset-portal-timeout');
|
||||
if(reset)reset.addEventListener('click',resetPortalTimeout);
|
||||
}
|
||||
loadHomeStatus();
|
||||
}
|
||||
|
||||
// --- HTTP ---
|
||||
@@ -366,27 +449,55 @@ function stopWifiScanPolling(){
|
||||
setScanRefreshEnabled(true);
|
||||
}
|
||||
|
||||
function setWifiScanProgress(visible,title,detail){
|
||||
var overlay=$('wm-wifi-scan-overlay');
|
||||
var titleEl=$('wm-wifi-scan-title');
|
||||
var detailEl=$('wm-wifi-scan-detail');
|
||||
if(!overlay)return;
|
||||
if(titleEl)titleEl.textContent=title||'Scanning Wi-Fi';
|
||||
if(detailEl)detailEl.textContent=detail||'';
|
||||
overlay.style.display=visible?'flex':'none';
|
||||
overlay.setAttribute('aria-hidden',visible?'false':'true');
|
||||
}
|
||||
|
||||
function startWifiScanPolling(box){
|
||||
stopWifiScanPolling();
|
||||
setScanRefreshEnabled(false);
|
||||
setWifiScanProgress(true,'Scanning Wi-Fi','Looking for nearby networks…');
|
||||
var token=++_wmWifiScanPollToken;
|
||||
var unavailableDeadline=Date.now()+20000;
|
||||
function retryAfterTemporaryRadioLoss(){
|
||||
if(Date.now()<unavailableDeadline){
|
||||
setWifiScanProgress(true,'Scanning Wi-Fi','Waiting for the device radio to return…');
|
||||
_wmWifiScanPollTimer=setTimeout(poll,1000);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function poll(){
|
||||
api('/api/wifi/scan-status').then(function(res){
|
||||
if(token!==_wmWifiScanPollToken)return;
|
||||
var d={};
|
||||
try{d=JSON.parse(res.body);}catch(e){return;}
|
||||
if(!res.ok)throw new Error('scan status unavailable');
|
||||
var data={};
|
||||
try{data=JSON.parse(res.body);}catch(e){
|
||||
if(retryAfterTemporaryRadioLoss())return;
|
||||
throw e;
|
||||
}
|
||||
box=getScanResultsBox()||box;
|
||||
if(box)box.innerHTML=renderScanList(d);
|
||||
if(!d.scanning){
|
||||
if(!data.scanning){
|
||||
if(box)box.innerHTML=renderScanList(data);
|
||||
setWifiScanProgress(false);
|
||||
stopWifiScanPolling();
|
||||
return;
|
||||
}
|
||||
_wmWifiScanPollTimer=setTimeout(poll,800);
|
||||
}).catch(function(){
|
||||
if(token!==_wmWifiScanPollToken)return;
|
||||
if(retryAfterTemporaryRadioLoss())return;
|
||||
stopWifiScanPolling();
|
||||
setWifiScanProgress(false);
|
||||
box=getScanResultsBox()||box;
|
||||
if(box)box.innerHTML=renderScanError('Portal became unreachable while scanning.');
|
||||
if(box)box.innerHTML=renderScanError('The portal did not return after scanning.');
|
||||
});
|
||||
}
|
||||
poll();
|
||||
@@ -440,21 +551,23 @@ function wifiRefresh(){
|
||||
var box=getScanResultsBox();
|
||||
if(!box)return;
|
||||
setScanRefreshEnabled(false);
|
||||
box.innerHTML="<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>Starting scan…</p></div>";
|
||||
setWifiScanProgress(true,'Preparing Wi-Fi scan','Starting a scan…');
|
||||
api('/api/wifi/scan',{method:'POST'}).then(function(res){
|
||||
box=getScanResultsBox()||box;
|
||||
if(!res.ok){
|
||||
setScanRefreshEnabled(true);
|
||||
if(box)box.innerHTML=renderScanError('Failed to start WiFi scan.');
|
||||
showToast('Failed to start WiFi scan.',true);
|
||||
setWifiScanProgress(false);
|
||||
if(box)box.innerHTML=renderScanError('Failed to start Wi-Fi scan.');
|
||||
showToast('Failed to start Wi-Fi scan.',true);
|
||||
return;
|
||||
}
|
||||
startWifiScanPolling(box);
|
||||
}).catch(function(){
|
||||
stopWifiScanPolling();
|
||||
setWifiScanProgress(false);
|
||||
box=getScanResultsBox()||box;
|
||||
if(box)box.innerHTML=renderScanError('Failed to reach portal while starting scan.');
|
||||
showToast('Failed to reach portal while starting scan.',true);
|
||||
if(box)box.innerHTML=renderScanError('Failed to reach the portal while starting a scan.');
|
||||
showToast('Failed to reach the portal while starting a scan.',true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -530,57 +643,76 @@ function renderStationProfileFields(profiles){
|
||||
}
|
||||
|
||||
function viewWifi(){
|
||||
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading WiFi options…</p></div>");
|
||||
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading Wi-Fi options…</p></div>");
|
||||
api('/api/wifi/meta').then(function(res){
|
||||
try{var m=JSON.parse(res.body);}catch(e){m={};}
|
||||
var meta={};
|
||||
try{meta=JSON.parse(res.body);}catch(e){}
|
||||
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'>"+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'>"+labelWithIcon('scan','Scan again')+"</button></div>";
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
|
||||
if(Array.isArray(m.profiles) && m.profiles.length){
|
||||
html+=renderStationProfileFields(m.profiles);
|
||||
html+="<div class='wm-page-head'><h1>Connect to Wi-Fi</h1>";
|
||||
html+="<p class='wm-page-desc'>Choose a network, then save and connect.</p></div>";
|
||||
html+="<div class='wm-card wm-scan-card'>"+cardTitleHtml('scan','Networks nearby');
|
||||
html+="<div id='wm-scan-results' class='wm-scan-results'>";
|
||||
html+="<p class='wm-lead'>Preparing nearby networks…</p></div>";
|
||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block'";
|
||||
html+=" id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button>";
|
||||
html+="<div id='wm-wifi-scan-overlay' class='wm-operation-overlay'";
|
||||
html+=" role='status' aria-live='polite' aria-atomic='true' aria-hidden='true'>";
|
||||
html+="<div class='wm-operation-panel'><span class='wm-spinner' aria-hidden='true'></span>";
|
||||
html+="<div><strong id='wm-wifi-scan-title'>Scanning Wi-Fi</strong>";
|
||||
html+="<p id='wm-wifi-scan-detail'>Looking for nearby networks…</p></div></div></div></div>";
|
||||
html+="<div class='wm-card wm-wifi-details-card'>"+cardTitleHtml('wifi','Network details');
|
||||
if(Array.isArray(meta.profiles)&&meta.profiles.length){
|
||||
html+=renderStationProfileFields(meta.profiles);
|
||||
html+="<form class='wm-wifi-data-form' id='wm-wifi-form'>";
|
||||
}else{
|
||||
html+="<form class='wm-wifi-data-form' id='wm-wifi-form'>";
|
||||
html+=renderFieldList(m.wifiFields||[]);
|
||||
var wf=m.wifiFields||[];
|
||||
var hasPass=false;
|
||||
var i;
|
||||
for(i=0;i<wf.length;i++){if(wf[i]&&wf[i].id==='p'){hasPass=true;break;}}
|
||||
if(hasPass){
|
||||
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/> <label for='wm-showpass'>Show password</label></div>";
|
||||
html+=renderFieldList(meta.wifiFields||[]);
|
||||
var wifiFields=meta.wifiFields||[];
|
||||
var hasPassword=false;
|
||||
for(var i=0;i<wifiFields.length;i++){
|
||||
if(wifiFields[i]&&wifiFields[i].id==='p'){hasPassword=true;break;}
|
||||
}
|
||||
if(hasPassword){
|
||||
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/>";
|
||||
html+=" <label for='wm-showpass'>Show password</label></div>";
|
||||
}
|
||||
}
|
||||
html+=renderFieldList(m.staticFields||[]);
|
||||
html+=renderFieldList(m.params||[]);
|
||||
html+=renderFieldList(meta.staticFields||[]);
|
||||
html+=renderFieldList(meta.params||[]);
|
||||
html+="</form>";
|
||||
html+="<div class='wm-form-actions'><button type='button' class='wm-btn wm-btn--primary wm-btn--block' data-wm-wifi-action='connect'>"+labelWithIcon('connect','Save and connect')+"</button>";
|
||||
if(Array.isArray(m.profiles) && m.profiles.length){
|
||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' data-wm-wifi-action='save'>Save for later</button>";
|
||||
html+="<div class='wm-form-actions'><button type='button'";
|
||||
html+=" class='wm-btn wm-btn--primary wm-btn--block' data-wm-wifi-action='connect'>";
|
||||
html+=labelWithIcon('connect','Save and connect')+"</button>";
|
||||
if(Array.isArray(meta.profiles)&&meta.profiles.length){
|
||||
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block'";
|
||||
html+=" data-wm-wifi-action='save'>Save for later</button>";
|
||||
}
|
||||
html+="</div>";
|
||||
html+="<div id='wm-wifi-msg'></div></div>";
|
||||
html+="</div><div id='wm-wifi-msg'></div></div>";
|
||||
html+="<div id='wm-wifi-save-overlay' class='wm-wifi-save-overlay'";
|
||||
html+=" role='status' aria-live='polite' aria-atomic='true' aria-hidden='true'>";
|
||||
html+="<div class='wm-wifi-save-panel'><span class='wm-spinner' aria-hidden='true'></span>";
|
||||
html+="<div><strong id='wm-wifi-save-title'>Saving settings</strong>";
|
||||
html+="<p id='wm-wifi-save-detail'>Please wait…</p></div></div></div>";
|
||||
setView(html);
|
||||
bindWifiViewEvents();
|
||||
setScanRefreshEnabled(true);
|
||||
api('/api/wifi/scan-status').then(function(r2){
|
||||
try{var d=JSON.parse(r2.body);}catch(e){d={};}
|
||||
api('/api/wifi/scan-status').then(function(scanRes){
|
||||
var scan={};
|
||||
try{scan=JSON.parse(scanRes.body);}catch(e){}
|
||||
var box=getScanResultsBox();
|
||||
if(shouldAutoStartWifiScan(d)){
|
||||
if(shouldAutoStartWifiScan(scan)){
|
||||
wifiRefresh();
|
||||
return;
|
||||
}
|
||||
if(box)box.innerHTML=renderScanList(d);
|
||||
if(d&&d.scanning){startWifiScanPolling(box);}
|
||||
if(box)box.innerHTML=renderScanList(scan);
|
||||
if(scan&&scan.scanning)startWifiScanPolling(box);
|
||||
}).catch(function(){
|
||||
var box=getScanResultsBox();
|
||||
if(box)box.innerHTML=renderScanError('Failed to reach portal while loading scan status.');
|
||||
if(box)box.innerHTML=renderScanError('Failed to load nearby networks.');
|
||||
setScanRefreshEnabled(true);
|
||||
});
|
||||
}).catch(function(){
|
||||
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Failed to load WiFi options. Reconnect to the portal and try again.</p></div>");
|
||||
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Failed to load Wi-Fi options. Reconnect to the portal and try again.</p></div>");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -588,6 +720,33 @@ function viewWifi(){
|
||||
var _wmWifiConnectPollTimer=null;
|
||||
var _wmWifiConnectPollToken=0;
|
||||
var _wmWifiConnectPending=false;
|
||||
var _wmWifiSaveInFlight=false;
|
||||
|
||||
function setWifiSaveControlsDisabled(disabled){
|
||||
var controls=document.querySelectorAll('.wm-wifi-data-form input,.wm-wifi-data-form select,.wm-wifi-data-form textarea,[data-wm-wifi-action],#wm-refresh-scan');
|
||||
for(var i=0;i<controls.length;i++)controls[i].disabled=disabled;
|
||||
}
|
||||
|
||||
function setWifiSaveProgress(visible,title,detail){
|
||||
var overlay=$('wm-wifi-save-overlay'),titleEl=$('wm-wifi-save-title'),detailEl=$('wm-wifi-save-detail');
|
||||
if(!overlay)return;
|
||||
if(titleEl)titleEl.textContent=title||'Saving settings';
|
||||
if(detailEl)detailEl.textContent=detail||'';
|
||||
overlay.style.display=visible?'flex':'none';
|
||||
overlay.setAttribute('aria-hidden',visible?'false':'true');
|
||||
}
|
||||
|
||||
function beginWifiSave(title,detail){
|
||||
_wmWifiSaveInFlight=true;
|
||||
setWifiSaveControlsDisabled(true);
|
||||
setWifiSaveProgress(true,title,detail);
|
||||
}
|
||||
|
||||
function finishWifiSave(){
|
||||
_wmWifiSaveInFlight=false;
|
||||
setWifiSaveControlsDisabled(false);
|
||||
setWifiSaveProgress(false);
|
||||
}
|
||||
|
||||
function stopWifiConnectPolling(){
|
||||
_wmWifiConnectPollToken++;
|
||||
@@ -597,47 +756,72 @@ function stopWifiConnectPolling(){
|
||||
}
|
||||
}
|
||||
|
||||
function completeWifiHandoff(data,attempt){
|
||||
attempt=attempt||0;
|
||||
api('/api/wifi/connect-complete',{method:'POST'}).then(function(res){
|
||||
if(!res.ok){
|
||||
if(attempt<2){setTimeout(function(){completeWifiHandoff(data,attempt+1);},500);return;}
|
||||
throw new Error('handoff acknowledgement rejected');
|
||||
}
|
||||
if(data.redirectUrl){
|
||||
setTimeout(function(){window.location.href=data.redirectUrl;},1200);
|
||||
}else{
|
||||
location.hash='#/';
|
||||
}
|
||||
}).catch(function(){
|
||||
if(attempt<2){
|
||||
setTimeout(function(){completeWifiHandoff(data,attempt+1);},500);
|
||||
return;
|
||||
}
|
||||
setWifiSaveProgress(true,'Wi-Fi connected','Open the device address shown below.');
|
||||
showToast('Wi-Fi connected, but automatic redirect was unavailable.',true);
|
||||
});
|
||||
}
|
||||
|
||||
function pollWifiConnectStatus(){
|
||||
stopWifiConnectPolling();
|
||||
var token=++_wmWifiConnectPollToken;
|
||||
function poll(){
|
||||
api('/api/wifi/connect-status').then(function(res){
|
||||
if(token!==_wmWifiConnectPollToken)return;
|
||||
var j={};
|
||||
var data={};
|
||||
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'){
|
||||
try{data=JSON.parse(res.body);}catch(e){}
|
||||
if(msg&&data.message)msg.innerHTML=esc(data.message);
|
||||
if(data.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>…";
|
||||
if(!data.stationIp){
|
||||
finishWifiSave();
|
||||
if(msg)msg.innerHTML=esc(data.message||'Settings saved.');
|
||||
showToast(data.message||'Settings saved.',false);
|
||||
return;
|
||||
}
|
||||
showToast(j.message||'WiFi connected',false);
|
||||
if(j.redirectUrl){
|
||||
setTimeout(function(){ window.location.href=j.redirectUrl; }, 1400);
|
||||
}else{
|
||||
location.hash='#/';
|
||||
var destination=data.redirectUrl||'';
|
||||
setWifiSaveProgress(true,'Connected to Wi-Fi','Opening '+data.stationIp+'…');
|
||||
if(msg){
|
||||
msg.innerHTML="Connected. Opening <a href='"+esc(destination||('http://'+data.stationIp+'/'))+"'>";
|
||||
msg+=esc(data.stationIp)+"</a>…";
|
||||
}
|
||||
}else if(j.state==='failed'){
|
||||
showToast(data.message||'Wi-Fi connected',false);
|
||||
completeWifiHandoff(data);
|
||||
}else if(data.state==='failed'){
|
||||
stopWifiConnectPolling();
|
||||
_wmWifiConnectPending=false;
|
||||
showToast(j.message||'WiFi connect failed',true);
|
||||
finishWifiSave();
|
||||
showToast(data.message||'Wi-Fi connection failed',true);
|
||||
}else{
|
||||
setWifiSaveProgress(true,'Connecting to Wi-Fi',data.message||'Waiting for the device to join the selected network…');
|
||||
_wmWifiConnectPollTimer=setTimeout(poll,700);
|
||||
}
|
||||
}).catch(function(){
|
||||
if(token!==_wmWifiConnectPollToken)return;
|
||||
stopWifiConnectPolling();
|
||||
finishWifiSave();
|
||||
var msg=$('wm-wifi-msg');
|
||||
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.';
|
||||
}
|
||||
if(msg)msg.innerHTML='Unable to check Wi-Fi connection status. Try again.';
|
||||
_wmWifiConnectPending=false;
|
||||
showToast('Unable to check Wi-Fi connection status.',true);
|
||||
});
|
||||
}
|
||||
poll();
|
||||
@@ -652,9 +836,11 @@ function collectWifiFormData(){
|
||||
}
|
||||
|
||||
function submitWifi(action){
|
||||
if(_wmWifiSaveInFlight)return false;
|
||||
var fd=collectWifiFormData();
|
||||
if(action)fd.append('stationAction',action);
|
||||
var msg=$('wm-wifi-msg');
|
||||
beginWifiSave(action==='save'?'Saving settings':'Saving and connecting',action==='save'?'Saving these settings for a later connection...':'Saving settings and preparing the WiFi connection...');
|
||||
if(msg)msg.innerHTML='Submitting...';
|
||||
api('/api/wifi/save',{method:'POST',body:fd}).then(function(res){
|
||||
var j={};
|
||||
@@ -664,8 +850,19 @@ function submitWifi(action){
|
||||
}
|
||||
if(res.status===202){
|
||||
_wmWifiConnectPending=true;
|
||||
setWifiSaveProgress(true,'Connecting to WiFi',(j&&j.message)?j.message:'Waiting for the device to join the selected network...');
|
||||
pollWifiConnectStatus();
|
||||
}else if(res.ok){
|
||||
finishWifiSave();
|
||||
showToast((j&&j.message)?j.message:'Settings saved.',false);
|
||||
}else{
|
||||
finishWifiSave();
|
||||
showToast((j&&j.message)?j.message:'Unable to save settings.',true);
|
||||
}
|
||||
}).catch(function(){
|
||||
finishWifiSave();
|
||||
if(msg)msg.innerHTML='Unable to reach the portal while saving. Please try again.';
|
||||
showToast('Unable to reach the portal while saving.',true);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -686,7 +883,7 @@ function viewInfo(){
|
||||
api('/api/info').then(function(res){
|
||||
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>";
|
||||
html+="<div class='wm-page-head'><h1>Device</h1><p class='wm-page-desc'>Connection status, hardware, and actions.</p></div>";
|
||||
var st=d.status;
|
||||
if(st && typeof st==='object' && !Array.isArray(st)){
|
||||
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
|
||||
@@ -755,7 +952,7 @@ function viewSetup(){
|
||||
api('/api/params').then(function(res){
|
||||
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
||||
var html=navBar('setup');
|
||||
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-page-head'><h1>Settings</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'>"+labelWithIcon('save','Save parameters')+"</button></div>";
|
||||
@@ -776,59 +973,100 @@ window.portalParamSave=function(ev){
|
||||
return false;
|
||||
};
|
||||
|
||||
function setOtaControlsDisabled(disabled){
|
||||
var controls=document.querySelectorAll('#wm-ota-form input,#wm-ota-form button');
|
||||
for(var i=0;i<controls.length;i++)controls[i].disabled=disabled;
|
||||
}
|
||||
|
||||
function setOtaProgress(visible,title,detail,percent){
|
||||
var overlay=$('wm-ota-overlay');
|
||||
var titleEl=$('wm-ota-title');
|
||||
var detailEl=$('wm-ota-detail');
|
||||
var progress=$('wm-ota-progress');
|
||||
var bar=$('wm-ota-progress-bar');
|
||||
var label=$('wm-ota-progress-label');
|
||||
if(!overlay)return;
|
||||
if(titleEl)titleEl.textContent=title||'Uploading firmware';
|
||||
if(detailEl)detailEl.textContent=detail||'';
|
||||
var hasPercent=typeof percent==='number';
|
||||
if(progress)progress.style.display=hasPercent?'block':'none';
|
||||
if(hasPercent){
|
||||
var value=Math.max(0,Math.min(100,Math.round(percent)));
|
||||
if(bar)bar.style.width=value+'%';
|
||||
if(label)label.textContent=value+'%';
|
||||
}
|
||||
overlay.style.display=visible?'flex':'none';
|
||||
overlay.setAttribute('aria-hidden',visible?'false':'true');
|
||||
}
|
||||
|
||||
function viewUpdate(){
|
||||
var pages=boot.pages||{};
|
||||
var pupd=pages.update||{};
|
||||
var update=pages.update||{};
|
||||
var html=navBar('update');
|
||||
html+="<div class='wm-page-head'><h1>Firmware update</h1><p class='wm-page-desc'>Flash a new firmware build (.bin). The device reboots when done.</p></div>";
|
||||
if(!pupd.visible){
|
||||
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this configuration.</p></div>";
|
||||
html+="<div class='wm-page-head'><h1>Update firmware</h1>";
|
||||
html+="<p class='wm-page-desc'>Upload a firmware build for this device.</p></div>";
|
||||
if(!update.visible){
|
||||
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this device.</p></div>";
|
||||
setView(html);
|
||||
return;
|
||||
}
|
||||
html+="<div class='wm-card'><form id='wm-ota-form'>";
|
||||
html+="<div class='wm-card wm-ota-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'>"+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>";
|
||||
html+="<div class='wm-form-actions'><button type='submit'";
|
||||
html+=" class='wm-btn wm-btn--primary wm-btn--block'>";
|
||||
html+=labelWithIcon('upload','Upload firmware')+"</button></div></form>";
|
||||
html+="<p class='wm-lead'>Use the binary built for this device. It will restart after a successful update.</p>";
|
||||
html+="<div id='wm-ota-overlay' class='wm-operation-overlay'";
|
||||
html+=" role='status' aria-live='polite' aria-atomic='true' aria-hidden='true'>";
|
||||
html+="<div class='wm-operation-panel'><span class='wm-spinner' aria-hidden='true'></span><div>";
|
||||
html+="<strong id='wm-ota-title'>Uploading firmware</strong>";
|
||||
html+="<p id='wm-ota-detail'>Preparing upload…</p>";
|
||||
html+="<div id='wm-ota-progress' class='wm-operation-progress' style='display:none'>";
|
||||
html+="<div class='wm-operation-progress-track'><span id='wm-ota-progress-bar'></span></div>";
|
||||
html+="<span id='wm-ota-progress-label'>0%</span></div></div></div></div></div>";
|
||||
setView(html);
|
||||
}
|
||||
|
||||
window.portalOtaSubmit=function(ev){
|
||||
ev.preventDefault();
|
||||
var fi=$('wm-ota-file');
|
||||
var logEl=$('wm-ota-log');
|
||||
if(!fi||!fi.files||!fi.files[0]){
|
||||
if(logEl)logEl.textContent='Choose a firmware file first.';
|
||||
var fileInput=$('wm-ota-file');
|
||||
if(!fileInput||!fileInput.files||!fileInput.files[0]){
|
||||
showToast('Choose a firmware file first.',true);
|
||||
return false;
|
||||
}
|
||||
if(logEl)logEl.textContent='Uploading…';
|
||||
setOtaControlsDisabled(true);
|
||||
setOtaProgress(true,'Uploading firmware','Preparing upload…');
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.upload.onprogress=function(e){
|
||||
if(!logEl||!e.lengthComputable)return;
|
||||
logEl.textContent='Uploading… '+Math.round(100*e.loaded/e.total)+'%';
|
||||
xhr.upload.onprogress=function(event){
|
||||
if(!event.lengthComputable)return;
|
||||
var percent=100*event.loaded/event.total;
|
||||
setOtaProgress(true,'Uploading firmware','Transferring firmware…',percent);
|
||||
};
|
||||
xhr.onreadystatechange=function(){
|
||||
if(xhr.readyState!==4)return;
|
||||
var t=xhr.responseText||'';
|
||||
if(logEl)logEl.textContent=t;
|
||||
var httpOk=xhr.status>=200&&xhr.status<300;
|
||||
try{
|
||||
var j=JSON.parse(t);
|
||||
if(j&&j.message){
|
||||
if(logEl)logEl.textContent=j.message;
|
||||
showToast(j.message,j.ok===false);
|
||||
return;
|
||||
}
|
||||
}catch(e){}
|
||||
showToast(httpOk?'Upload finished.':('HTTP '+xhr.status),!httpOk);
|
||||
var text=xhr.responseText||'';
|
||||
var data={};
|
||||
try{data=JSON.parse(text);}catch(e){}
|
||||
var ok=xhr.status>=200&&xhr.status<300&&data.ok!==false;
|
||||
if(ok){
|
||||
setOtaProgress(true,'Firmware updated','Restarting device…',100);
|
||||
showToast(data.message||'Firmware updated. Restarting device…',false);
|
||||
return;
|
||||
}
|
||||
setOtaProgress(false);
|
||||
setOtaControlsDisabled(false);
|
||||
showToast(data.message||('Upload failed (HTTP '+xhr.status+').'),true);
|
||||
};
|
||||
xhr.onerror=function(){
|
||||
setOtaProgress(false);
|
||||
setOtaControlsDisabled(false);
|
||||
showToast('Upload failed before the device responded.',true);
|
||||
};
|
||||
xhr.open('POST','/u');
|
||||
var fd=new FormData();
|
||||
fd.append('update',fi.files[0]);
|
||||
xhr.send(fd);
|
||||
var formData=new FormData();
|
||||
formData.append('update',fileInput.files[0]);
|
||||
xhr.send(formData);
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -853,6 +1091,7 @@ function route(){
|
||||
}
|
||||
|
||||
function bootPortal(){
|
||||
applyPortalFavicon();
|
||||
route();
|
||||
try{document.dispatchEvent(new CustomEvent('wm:ready',{detail:{boot:boot}}));}catch(e){}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ namespace {
|
||||
constexpr size_t kMaxHostnameLength = 32;
|
||||
constexpr uint8_t kSoftApStartMaxAttempts = 3;
|
||||
constexpr unsigned long kStationProfileSwitchDelayMs = 2000UL; // ESP WiFi needs time to leave a failed association.
|
||||
// A portal client acknowledges the successful station hand-off before the AP
|
||||
// is closed. The fallback protects headless/captive clients that disappear.
|
||||
constexpr unsigned long kPortalSuccessHandoffFallbackMs = 12000UL;
|
||||
constexpr unsigned long kPortalSuccessHandoffAcknowledgedDelayMs = 700UL;
|
||||
|
||||
bool isValidHostnameChar(char c) {
|
||||
return isAlphaNumeric(c) || c == '-';
|
||||
@@ -505,6 +509,14 @@ void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, con
|
||||
_cpConnectState = success ? wm_cp_connect_state_t::success : wm_cp_connect_state_t::failed;
|
||||
|
||||
if (success) {
|
||||
// Profile-backed provisioning reaches this path rather than
|
||||
// processPortalConnect(). Keep the AP available until the SPA has read
|
||||
// the station address and acknowledged its redirect hand-off.
|
||||
if (_disableConfigPortal && (configPortalActive || webPortalActive)) {
|
||||
_cpConnectCloseAt = millis() + kPortalSuccessHandoffFallbackMs;
|
||||
_cpConnectState = wm_cp_connect_state_t::success_waiting_close;
|
||||
return;
|
||||
}
|
||||
if (_savewificallback != NULL) {
|
||||
_savewificallback();
|
||||
}
|
||||
@@ -518,6 +530,22 @@ void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, con
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiManager::acknowledgePortalConnectHandoff() {
|
||||
if (_cpConnectState != wm_cp_connect_state_t::success_waiting_close) {
|
||||
return;
|
||||
}
|
||||
_cpConnectCloseAt = millis() + kPortalSuccessHandoffAcknowledgedDelayMs;
|
||||
}
|
||||
|
||||
#ifdef UNIT_TEST
|
||||
void WiFiManager::wmTestCompleteProfilePortalConnectionSuccess() {
|
||||
_stationCandidateFromPortal = true;
|
||||
configPortalActive = true;
|
||||
_disableConfigPortal = true;
|
||||
completePortalStationAttempt(true, WL_CONNECTED, F("WiFi connected"));
|
||||
}
|
||||
#endif
|
||||
|
||||
void WiFiManager::handleStationConnectionSuccess() {
|
||||
const uint8_t slot = _stationStatus.attemptedSlot;
|
||||
const bool wasCandidate = _stationCandidateActive;
|
||||
@@ -1177,7 +1205,6 @@ 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) {
|
||||
@@ -1279,7 +1306,7 @@ void WiFiManager::processPortalConnect() {
|
||||
? String(F("WiFi connected. Redirecting to ")) + _cpConnectStationIp
|
||||
: String(F("WiFi connected"));
|
||||
if (_disableConfigPortal && (configPortalActive || webPortalActive)) {
|
||||
_cpConnectCloseAt = now + successHandoffDelayMs;
|
||||
_cpConnectCloseAt = now + kPortalSuccessHandoffFallbackMs;
|
||||
_cpConnectState = wm_cp_connect_state_t::success_waiting_close;
|
||||
} else {
|
||||
finalizePortalConnectSuccess();
|
||||
@@ -2635,7 +2662,7 @@ bool WiFiManager::setPortalConfig(const WiFiManagerPortalConfig& config) {
|
||||
? WiFiManagerPortalText::progmem(kDefaultPortalTitle)
|
||||
: config.title;
|
||||
_portalBrand.identityTextOverride = config.identityText;
|
||||
_portalBrand.homeIntro = config.homeIntro;
|
||||
_portalBrand.tagline = config.tagline;
|
||||
_portalBrand.logo = config.logo;
|
||||
_portalBrand.logoAltText = config.logoAltText;
|
||||
_portalTheme = config.theme;
|
||||
|
||||
@@ -1064,12 +1064,12 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() {
|
||||
const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
|
||||
String json;
|
||||
reservePage(json, 1200);
|
||||
json += F("{\"contractVersion\":2");
|
||||
json += F("{\"contractVersion\":3");
|
||||
json += F(",\"brand\":{");
|
||||
json += F("\"title\":\"");
|
||||
jsonAppendEscaped(json, _wm->_portalBrand.title);
|
||||
json += F("\",\"homeIntro\":\"");
|
||||
jsonAppendEscaped(json, _wm->_portalBrand.homeIntro);
|
||||
json += F("\",\"tagline\":\"");
|
||||
jsonAppendEscaped(json, _wm->_portalBrand.tagline);
|
||||
json += F("\",\"logoSvg\":\"");
|
||||
jsonAppendEscaped(json, _wm->_portalBrand.logo.svg);
|
||||
json += F("\",\"logoAltText\":\"");
|
||||
@@ -1404,6 +1404,33 @@ void WiFiManagerHandlers::handleApiWifiConnectStatus(AsyncWebServerRequest *requ
|
||||
sendApiJson(request, 200, buildApiWifiConnectStatusJson());
|
||||
}
|
||||
|
||||
void WiFiManagerHandlers::handleApiWifiConnectComplete(AsyncWebServerRequest *request) {
|
||||
handleRequest(request);
|
||||
if (!_wm->didConfigPortalConnectSucceed() || _wm->_cpConnectStationIp.length() == 0) {
|
||||
sendApiJson(request, 409, F("{\"ok\":false,\"message\":\"WiFi hand-off is not ready\"}"));
|
||||
return;
|
||||
}
|
||||
|
||||
// The SPA has received the station address and can navigate to it. Keep the
|
||||
// portal alive briefly so the normal device web server can start cleanly.
|
||||
sendApiJson(request, 200, F("{\"ok\":true}"));
|
||||
_wm->acknowledgePortalConnectHandoff();
|
||||
}
|
||||
|
||||
void WiFiManagerHandlers::handleApiPortalTimeoutReset(AsyncWebServerRequest *request) {
|
||||
handleRequest(request);
|
||||
if (!_wm->configPortalActive || _wm->_configPortalTimeout == 0) {
|
||||
sendApiJson(request, 409, F("{\"ok\":false,\"message\":\"Portal timeout is not active\"}"));
|
||||
return;
|
||||
}
|
||||
|
||||
_wm->_configPortalStart = millis();
|
||||
String json = F("{\"ok\":true,\"timeoutSecondsRemaining\":");
|
||||
json += String((_wm->_configPortalTimeout + 999UL) / 1000UL);
|
||||
json += F("}");
|
||||
sendApiJson(request, 200, json);
|
||||
}
|
||||
|
||||
String WiFiManagerHandlers::buildApiParamsGetJson() {
|
||||
String json = F("{\"params\":[");
|
||||
bool first = true;
|
||||
|
||||
@@ -96,6 +96,12 @@ void WiFiManagerServer::registerRoutes() {
|
||||
server->on(WM_G(R_api_wifi_connect_status), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiWifiConnectStatus(request);
|
||||
});
|
||||
server->on(WM_G(R_api_wifi_connect_complete), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiWifiConnectComplete(request);
|
||||
});
|
||||
server->on(WM_G(R_api_portal_timeout_reset), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiPortalTimeoutReset(request);
|
||||
});
|
||||
server->on(WM_G(R_api_params), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleApiParamsGet(request);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user