diff --git a/wled00/data/common.js b/wled00/data/common.js index 5a98b4fe1f..08ee0d7f86 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -51,6 +51,35 @@ function tooltip(cont=null) { }); }); }; +// sequential loading of external resources (JS or CSS) with retry, calls init() when done +function loadResources(files, init) { + let i = 0; + const loadNext = () => { + if (i >= files.length) { + if (init) { + d.documentElement.style.visibility = 'visible'; // make page visible after all files are laoded if it was hidden (prevent ugly display) + d.readyState === 'complete' ? init() : window.addEventListener('load', init); + } + return; + } + const file = files[i++]; + const isCSS = file.endsWith('.css'); + const el = d.createElement(isCSS ? 'link' : 'script'); + if (isCSS) { + el.rel = 'stylesheet'; + el.href = file; + } else { + el.src = file; + } + el.onload = () => { setTimeout(loadNext, 0);}; + el.onerror = () => { + i--; // load this file again + setTimeout(loadNext, 100); + }; + d.head.appendChild(el); + }; + loadNext(); +} // https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) { let scE = d.createElement("script"); diff --git a/wled00/data/index.js b/wled00/data/index.js index 2514f03fb1..af06d7e6ae 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -16,7 +16,7 @@ var simplifiedUI = false; var tr = 7; var d = document; const ranges = RangeTouch.setup('input[type="range"]', {}); -var retry = false; +var retry = 0; var palettesData; var fxdata = []; var pJson = {}, eJson = {}, lJson = {}; @@ -200,19 +200,17 @@ function loadBg() { }); } -function loadSkinCSS(cId) -{ - if (!gId(cId)) // check if element exists - { - var h = d.getElementsByTagName('head')[0]; - var l = d.createElement('link'); - l.id = cId; - l.rel = 'stylesheet'; - l.type = 'text/css'; +function loadSkinCSS(cId) { + return new Promise((resolve, reject) => { + if (gId(cId)) return resolve(); + const l = d.createElement('link'); + l.id = cId; + l.rel = 'stylesheet'; l.href = getURL('/skin.css'); - l.media = 'all'; - h.appendChild(l); - } + l.onload = resolve; + l.onerror = reject; + d.head.appendChild(l); + }); } function getURL(path) { @@ -278,19 +276,22 @@ function onLoad() cpick.on("color:change", () => {updatePSliders()}); pmtLS = localStorage.getItem('wledPmt'); - // Load initial data - loadPalettes(()=>{ - // fill effect extra data array - loadFXData(()=>{ - // load and populate effects - setTimeout(()=>{loadFX(()=>{ - loadPalettesData(()=>{ - requestJson();// will load presets and create WS - if (cfg.comp.css) setTimeout(()=>{loadSkinCSS('skinCss')},50); - }); - })},50); - }); - }); + // Load initial data sequentially, no parallel requests to avoid "503" errors when heap is low (slower but much more reliable) + (async ()=>{ + try { + await loadPalettes(); // loads base palettes and builds #pallist (safe first) + await loadFXData(); // loads fx data + await loadFX(); // populates effect list + await loadPalettesData(); // fills palettesData[] for previews + await requestJson(); // fills lastinfo.cpalcount (safe now, pallist exists) + populatePalettes(); // repopulate with custom palettes now that cpalcount is known + await loadPresets(); // load presets last + if (cfg.comp.css) await loadSkinCSS('skinCss'); + if (!ws) makeWS(); + } catch(e) { + showToast("Init failed: " + e, true); + } + })(); resetUtil(); d.addEventListener("visibilitychange", handleVisibilityChange, false); @@ -481,8 +482,7 @@ function restore(txt) { return false; } -function loadPresets(callback = null) -{ +async function loadPresets(callback = null) { // 1st boot (because there is a callback) if (callback && pmt == pmtLS && pmt > 0) { // we have a copy of the presets in local storage and don't need to fetch another one @@ -491,113 +491,94 @@ function loadPresets(callback = null) callback(); return; } - // afterwards if (!callback && pmt == pmtLast) return; - fetch(getURL('/presets.json'), { - method: 'get' - }) - .then(res => { - if (res.status=="404") return {"0":{}}; - //if (!res.ok) showErrorToast(); - return res.json(); - }) - .then(json => { - pJson = json; - pmtLast = pmt; - populatePresets(); - }) - .catch((e)=>{ - //showToast(e, true); - presetError(false); - }) - .finally(()=>{ - if (callback) setTimeout(callback,99); + return new Promise((resolve) => { + fetch(getURL('/presets.json'), {method: 'get'}) + .then(res => res.status=="404" ? {"0":{}} : res.json()) + .then(json => { + pJson = json; + pmtLast = pmt; + populatePresets(); + resolve(); + }) + .catch(() => { + presetError(false); + resolve(); + }) + .finally(() => { + if (callback) setTimeout(callback, 99); + }); }); } -function loadPalettes(callback = null) -{ - fetch(getURL('/json/palettes'), { - method: 'get' - }) - .then((res)=>{ - if (!res.ok) showErrorToast(); - return res.json(); - }) - .then((json)=>{ - lJson = Object.entries(json); - populatePalettes(); - retry = false; - }) - .catch((e)=>{ - if (!retry) { - retry = true; - setTimeout(loadPalettes, 500); // retry - } - showToast(e, true); - }) - .finally(()=>{ - if (callback) callback(); - updateUI(); +async function loadPalettes() { + // Just wrap existing code in Promise + return new Promise((resolve) => { + fetch(getURL('/json/palettes'), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject()) + .then(json => { + lJson = Object.entries(json); + populatePalettes(); + retry = 0; + resolve(); + }) + .catch((e) => { + if (retry<5) { + retry++; + setTimeout(() => loadPalettes().then(resolve), 100); + } else { + showToast(e, true); + resolve(); + } + }); }); } -function loadFX(callback = null) -{ - fetch(getURL('/json/effects'), { - method: 'get' - }) - .then((res)=>{ - if (!res.ok) showErrorToast(); - return res.json(); - }) - .then((json)=>{ - eJson = Object.entries(json); - populateEffects(); - retry = false; - }) - .catch((e)=>{ - if (!retry) { - retry = true; - setTimeout(loadFX, 500); // retry - } - showToast(e, true); - }) - .finally(()=>{ - if (callback) callback(); - updateUI(); +async function loadFX() { + return new Promise((resolve) => { + fetch(getURL('/json/effects'), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject()) + .then(json => { + eJson = Object.entries(json); + populateEffects(); + retry = 0; + resolve(); + }) + .catch((e) => { + if (retry<5) { + retry++; + setTimeout(() => loadFX().then(resolve), 100); + } else { + showToast(e, true); + resolve(); + } + }); }); } -function loadFXData(callback = null) -{ - fetch(getURL('/json/fxdata'), { - method: 'get' - }) - .then((res)=>{ - if (!res.ok) showErrorToast(); - return res.json(); - }) - .then((json)=>{ - fxdata = json||[]; - // add default value for Solid - fxdata.shift() - fxdata.unshift(";!;"); - retry = false; - }) - .catch((e)=>{ - fxdata = []; - if (!retry) { - retry = true; - setTimeout(()=>{loadFXData(loadFX);}, 500); // retry - } - showToast(e, true); - }) - .finally(()=>{ - if (callback) callback(); - updateUI(); +async function loadFXData() { + return new Promise((resolve) => { + fetch(getURL('/json/fxdata'), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject()) + .then(json => { + fxdata = json||[]; + fxdata.shift(); + fxdata.unshift(";!;"); + retry = 0; + resolve(); + }) + .catch((e) => { + fxdata = []; + if (retry<5) { + retry++; + setTimeout(() => loadFXData().then(resolve), 100); + } else { + showToast(e, true); + resolve(); + } + }); }); } @@ -700,10 +681,10 @@ function parseInfo(i) { //var setInnerHTML = function(elm, html) { // elm.innerHTML = html; // Array.from(elm.querySelectorAll("script")).forEach( oldScript => { -// const newScript = document.createElement("script"); +// const newScript = d.createElement("script"); // Array.from(oldScript.attributes) // .forEach( attr => newScript.setAttribute(attr.name, attr.value) ); -// newScript.appendChild(document.createTextNode(oldScript.innerHTML)); +// newScript.appendChild(d.createTextNode(oldScript.innerHTML)); // oldScript.parentNode.replaceChild(newScript, oldScript); // }); //} @@ -1709,77 +1690,80 @@ function setEffectParameters(idx) var jsonTimeout; var reqsLegal = false; - -function requestJson(command=null) -{ - gId('connind').style.backgroundColor = "var(--c-y)"; - if (command && !reqsLegal) return; // stop post requests from chrome onchange event on page restore - if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000); - var req = null; - var useWs = (ws && ws.readyState === WebSocket.OPEN); - var type = command ? 'post':'get'; - if (command) { - command.v = true; // force complete /json/si API response - command.time = Math.floor(Date.now() / 1000); - var t = gId('tt'); - if (t.validity.valid && command.transition==null) { - var tn = parseInt(t.value*10); - if (tn != tr) command.transition = tn; +async function requestJson(command=null) { + return new Promise((resolve, reject) => { + gId('connind').style.backgroundColor = "var(--c-y)"; + if (command && !reqsLegal) {resolve(); return;} + if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000); + + var useWs = (ws && ws.readyState === WebSocket.OPEN); + var req = null; + if (command) { + command.v = true; + command.time = Math.floor(Date.now() / 1000); + var t = gId('tt'); + if (t && t.validity.valid && command.transition==null) { + var tn = parseInt(t.value*10); + if (tn != tr) command.transition = tn; + } + req = JSON.stringify(command); + if (req.length > 1340) useWs = false; + if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; } - //command.bs = parseInt(gId('bs').value); - req = JSON.stringify(command); - if (req.length > 1340) useWs = false; // do not send very long requests over websocket - if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; // esp8266 can only handle 500 bytes - }; - - if (useWs) { - ws.send(req?req:'{"v":true}'); - return; - } - fetch(getURL('/json/si'), { - method: type, - headers: {"Content-Type": "application/json; charset=UTF-8"}, - body: req - }) - .then(res => { - clearTimeout(jsonTimeout); - jsonTimeout = null; - if (!res.ok) showErrorToast(); - return res.json(); - }) - .then(json => { - lastUpdate = new Date(); - clearErrorToast(3000); - gId('connind').style.backgroundColor = "var(--c-g)"; - if (!json) { showToast('Empty response', true); return; } - if (json.success) return; - if (json.info) { - let i = json.info; - parseInfo(i); - populatePalettes(i); - if (isInfo) populateInfo(i); - if (simplifiedUI) simplifyUI(); + if (useWs) { + ws.send(req?req:'{"v":true}'); + resolve(); + return; } - var s = json.state ? json.state : json; - readState(s); - //load presets and open websocket sequentially - if (!pJson || isEmpty(pJson)) setTimeout(()=>{ - loadPresets(()=>{ - wsRpt = 0; - if (!(ws && ws.readyState === WebSocket.OPEN)) makeWS(); - }); - },25); - reqsLegal = true; - retry = false; - }) - .catch((e)=>{ - if (!retry) { - retry = true; - setTimeout(requestJson,500); - } - showToast(e, true); + fetch(getURL('/json/si'), { + method: command ? 'post' : 'get', + headers: {"Content-Type": "application/json; charset=UTF-8"}, + body: req + }) + .then(res => { + clearTimeout(jsonTimeout); + jsonTimeout = null; + return res.ok ? res.json() : Promise.reject(); + }) + .then(json => { + lastUpdate = new Date(); + clearErrorToast(3000); + gId('connind').style.backgroundColor = "var(--c-g)"; + if (!json) { showToast('Empty response', true); resolve(); return; } + if (json.success) {resolve(); return;} + if (json.info) { + parseInfo(json.info); + if (isInfo) populateInfo(json.info); + if (simplifiedUI) simplifyUI(); + } + var s = json.state ? json.state : json; + readState(s); + + // Load presets and open websocket sequentially + if (!pJson || isEmpty(pJson)) { + setTimeout(()=>{ + loadPresets(()=>{ + wsRpt = 0; + if (!(ws && ws.readyState === WebSocket.OPEN)) makeWS(); + }); + }, 25); + } + + reqsLegal = true; + retry = 0; + resolve(); + }) + .catch((e)=>{ + if (retry<10) { + retry++; + setTimeout(() => requestJson(command).then(resolve).catch(reject), 100); + } else { + showToast(e, true); + resolve(); + } + }); }); } @@ -2820,56 +2804,53 @@ function rSegs() requestJson(obj); } -function loadPalettesData(callback = null) -{ - if (palettesData) return; - const lsKey = "wledPalx"; - var lsPalData = localStorage.getItem(lsKey); - if (lsPalData) { - try { - var d = JSON.parse(lsPalData); - if (d && d.vid == d.vid) { - palettesData = d.p; - if (callback) callback(); - return; - } - } catch (e) {} - } +function loadPalettesData() { + return new Promise((resolve) => { + if (palettesData) return resolve(); // already loaded + const lsKey = "wledPalx"; + var lsPalData = localStorage.getItem(lsKey); + if (lsPalData) { + try { + var d = JSON.parse(lsPalData); + if (d && d.vid == d.vid) { + palettesData = d.p; + return resolve(); + } + } catch (e) {} + } - palettesData = {}; - getPalettesData(0, ()=>{ - localStorage.setItem(lsKey, JSON.stringify({ - p: palettesData, - vid: lastinfo.vid - })); - redrawPalPrev(); - if (callback) setTimeout(callback, 99); + palettesData = {}; + getPalettesData(0, () => { + localStorage.setItem(lsKey, JSON.stringify({ + p: palettesData, + vid: lastinfo.vid + })); + redrawPalPrev(); + setTimeout(resolve, 99); // delay optional + }); }); -} + } -function getPalettesData(page, callback) -{ - fetch(getURL(`/json/palx?page=${page}`), { - method: 'get' - }) - .then(res => { - if (!res.ok) showErrorToast(); - return res.json(); - }) +function getPalettesData(page, callback) { + fetch(getURL(`/json/palx?page=${page}`), {method: 'get'}) + .then(res => res.ok ? res.json() : Promise.reject()) .then(json => { - retry = false; + retry = 0; palettesData = Object.assign({}, palettesData, json.p); if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 75); else callback(); }) .catch((error)=>{ - if (!retry) { - retry = true; - setTimeout(()=>{getPalettesData(page,callback);}, 500); // retry + if (retry<5) { + retry++; + setTimeout(()=>{getPalettesData(page,callback);}, 100); + } else { + showToast(error, true); + callback(); } - showToast(error, true); }); } + /* function hideModes(txt) { @@ -2971,7 +2952,7 @@ function filterFocus(e) { } if (e.type === "blur") { setTimeout(() => { - if (e.target === document.activeElement && document.hasFocus()) return; + if (e.target === d.activeElement && d.hasFocus()) return; // do not hide if filter is active if (!c) { // compute sticky top @@ -3219,7 +3200,7 @@ function simplifyUI() { // Create dropdown dialog function createDropdown(id, buttonText, dialogElements = null) { // Create dropdown dialog - const dialog = document.createElement("dialog"); + const dialog = d.createElement("dialog"); // Move every dialogElement to the dropdown dialog or if none are given, move all children of the element with the given id if (dialogElements) { dialogElements.forEach((e) => { @@ -3232,7 +3213,7 @@ function simplifyUI() { } // Create button for the dropdown - const btn = document.createElement("button"); + const btn = d.createElement("button"); btn.id = id + "btn"; btn.classList.add("btn"); btn.innerText = buttonText; @@ -3280,7 +3261,7 @@ function simplifyUI() { // Hide palette label gId("pall").style.display = "none"; - gId("Colors").insertBefore(document.createElement("br"), gId("pall")); + gId("Colors").insertBefore(d.createElement("br"), gId("pall")); // Hide effect label gId("modeLabel").style.display = "none"; @@ -3292,7 +3273,7 @@ function simplifyUI() { // Hide bottom bar gId("bot").style.display = "none"; - document.documentElement.style.setProperty('--bh', '0px'); + d.documentElement.style.setProperty('--bh', '0px'); // Hide other tabs gId("Effects").style.display = "none"; diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm index 52d5bcc5d5..fd113452f2 100644 --- a/wled00/data/settings.htm +++ b/wled00/data/settings.htm @@ -4,13 +4,33 @@ WLED Settings - + + + + + + + + + + + + - - - - - - - - - - - - \ No newline at end of file diff --git a/wled00/data/settings_2D.htm b/wled00/data/settings_2D.htm index 5c7e66e41d..63ea4a60bf 100644 --- a/wled00/data/settings_2D.htm +++ b/wled00/data/settings_2D.htm @@ -4,11 +4,20 @@ 2D Set-up - + - - +
diff --git a/wled00/data/settings_dmx.htm b/wled00/data/settings_dmx.htm index 7b7fa2bb88..391c2bdc97 100644 --- a/wled00/data/settings_dmx.htm +++ b/wled00/data/settings_dmx.htm @@ -4,8 +4,16 @@ DMX Settings - + - - +
diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 8a3330e473..141f00d79e 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -4,7 +4,7 @@ LED Settings - + - - +
diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index d0ca118fd9..182e729648 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -4,8 +4,16 @@ Misc Settings - + - - +
diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index 97c8d1d160..73e4d9a268 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -4,8 +4,16 @@ Sync Settings - + - - +
diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index 698e25b2af..bee982def1 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -4,10 +4,19 @@ Time Settings - + - - +
diff --git a/wled00/data/settings_ui.htm b/wled00/data/settings_ui.htm index a608d57764..7955e8e699 100644 --- a/wled00/data/settings_ui.htm +++ b/wled00/data/settings_ui.htm @@ -4,10 +4,19 @@ UI Settings - + - - +
diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index 3879541724..dbef550115 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -4,12 +4,22 @@ Usermod Settings - + - - +
diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm index 7f402b5bbd..5167eec450 100644 --- a/wled00/data/settings_wifi.htm +++ b/wled00/data/settings_wifi.htm @@ -4,8 +4,17 @@ WiFi Settings - + - - +
diff --git a/wled00/ws.cpp b/wled00/ws.cpp index 3a97459fee..cf81d3bba6 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -130,12 +130,6 @@ void sendDataWs(AsyncWebSocketClient * client) // the following may no longer be necessary as heap management has been fixed by @willmmiles in AWS size_t heap1 = getFreeHeapSize(); DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); - #ifdef ESP8266 - if (len>heap1) { - DEBUG_PRINTLN(F("Out of memory (WS)!")); - return; - } - #endif AsyncWebSocketBuffer buffer(len); #ifdef ESP8266 size_t heap2 = getFreeHeapSize();