Use sequential loading and requests for all UI resources (#5013)

* use sequential loading for all UI resources

- load common.js and style.css sequentially for all config pages
- restrict all requrests in index.js to single connection
- retry more than once if requests fail
- incremental timeouts to make them faster and still more robust
- bugfix in connectWs()
- on page load, presets are loaded from localStorage if controller was not rebooted
- remove hiding of segment freeze button when not collapsed
This commit is contained in:
Damian Schneider
2025-12-14 10:13:00 +01:00
committed by GitHub
parent d1260ccf8b
commit 32b104e1a9
16 changed files with 401 additions and 342 deletions

View File

@@ -51,6 +51,38 @@ 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 loaded 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;
const st = d.head.querySelector('style');
if (st) d.head.insertBefore(el, st); // insert before any <style> to allow overrides
else d.head.appendChild(el);
} else {
el.src = file;
d.head.appendChild(el);
}
el.onload = () => { loadNext(); };
el.onerror = () => {
i--; // load this file again
setTimeout(loadNext, 100);
};
};
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");
@@ -116,21 +148,22 @@ function uploadFile(fileObj, name) {
fileObj.value = '';
return false;
}
// connect to WebSocket, use parent WS or open new
// connect to WebSocket, use parent WS or open new, callback function gets passed the new WS object
function connectWs(onOpen) {
try {
if (top.window.ws && top.window.ws.readyState === WebSocket.OPEN) {
if (onOpen) onOpen();
return top.window.ws;
}
} catch (e) {}
getLoc(); // ensure globals (loc, locip, locproto) are up to date
let url = loc ? getURL('/ws').replace("http","ws") : "ws://"+window.location.hostname+"/ws";
let ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
if (onOpen) { ws.onopen = onOpen; }
try { top.window.ws = ws; } catch (e) {} // store in parent for reuse
let ws;
try { ws = top.window.ws;} catch (e) {}
// reuse if open
if (ws && ws.readyState === WebSocket.OPEN) {
if (onOpen) onOpen(ws);
} else {
// create new ws connection
getLoc(); // ensure globals are up to date
let url = loc ? getURL('/ws').replace("http", "ws")
: "ws://" + window.location.hostname + "/ws";
ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
if (onOpen) ws.onopen = () => onOpen(ws);
}
return ws;
}

View File

@@ -1476,7 +1476,7 @@ dialog {
.expanded {
display: inline-block !important;
}
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .frz, .expanded .g-icon {
.hide, .expanded .segin.hide, .expanded .presin.hide, .expanded .sbs.hide, .expanded .g-icon {
display: none !important;
}

View File

@@ -16,13 +16,12 @@ var simplifiedUI = false;
var tr = 7;
var d = document;
const ranges = RangeTouch.setup('input[type="range"]', {});
var retry = false;
var palettesData;
var fxdata = [];
var pJson = {}, eJson = {}, lJson = {};
var plJson = {}; // array of playlists
var pN = "", pI = 0, pNum = 0;
var pmt = 1, pmtLS = 0, pmtLast = 0;
var pmt = 1, pmtLS = 0;
var lastinfo = {};
var isM = false, mw = 0, mh=0;
var ws, wsRpt=0;
@@ -200,19 +199,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 +275,23 @@ 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 requestJson(); // updates info variables
await loadPalettesData(); // fills palettesData[] for previews
populatePalettes(); // repopulate with custom palettes now that cpalcount is known
if(pmt == pmtLS) populatePresets(true); // load presets from localStorage if signature matches (i.e. no device reboot)
else await loadPresets(); // load and populate presets
if (cfg.comp.css) await loadSkinCSS('skinCss');
if (!ws) makeWS();
} catch(e) {
showToast("Init failed: " + e, true);
}
})();
resetUtil();
d.addEventListener("visibilitychange", handleVisibilityChange, false);
@@ -448,7 +449,7 @@ function presetError(empty)
if (bckstr.length > 10) hasBackup = true;
} catch (e) {}
var cn = `<div class="pres c" style="padding:8px;margin-bottom:8px;${empty?'':'cursor:pointer;'}" ${empty?'':'onclick="pmtLast=0;loadPresets();"'}>`;
var cn = `<div class="pres c" style="padding:8px;margin-bottom:8px;${empty?'':'cursor:pointer;'}" ${empty?'':'onclick="loadPresets();"'}>`;
if (empty)
cn += `You have no presets yet!`;
else
@@ -481,123 +482,81 @@ function restore(txt) {
return false;
}
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
populatePresets(true);
pmtLast = pmt;
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);
async function loadPresets() {
return new Promise((resolve) => {
fetch(getURL('/presets.json'), {method: 'get'})
.then(res => res.status=="404" ? {"0":{}} : res.json())
.then(json => {
pJson = json;
populatePresets();
resolve();
})
.catch(() => {
presetError(false);
resolve();
})
});
}
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(retry=0) {
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();
resolve();
})
.catch((e) => {
if (retry<5) {
setTimeout(() => loadPalettes(retry+1).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(retry=0) {
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();
resolve();
})
.catch((e) => {
if (retry<5) {
setTimeout(() => loadFX(retry+1).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(retry=0) {
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(";!;");
resolve();
})
.catch((e) => {
fxdata = [];
if (retry<5) {
setTimeout(() => loadFXData(retry+1).then(resolve), 100);
} else {
showToast(e, true);
resolve();
}
});
});
}
@@ -619,7 +578,7 @@ function populateQL()
function populatePresets(fromls)
{
if (fromls) pJson = JSON.parse(localStorage.getItem("wledP"));
if (!pJson) {setTimeout(loadPresets,250); return;}
if (!pJson) {loadPresets(); return;} // note: no await as this is a fallback that should not be needed as init function fetches pJson
delete pJson["0"];
var cn = "";
var arr = Object.entries(pJson).sort(cmpP);
@@ -701,10 +660,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);
// });
//}
@@ -906,6 +865,7 @@ function populateSegments(s)
gId("segcont").classList.remove("hide");
let noNewSegs = (lowestUnused >= maxSeg);
resetUtil(noNewSegs);
if (segCount === 0) return; // no segments to populate
for (var i = 0; i <= lSeg; i++) {
if (!gId(`seg${i}`)) continue;
updateLen(i);
@@ -1437,7 +1397,7 @@ function makeWS() {
};
ws.onclose = (e)=>{
gId('connind').style.backgroundColor = "var(--c-r)";
if (wsRpt++ < 5) setTimeout(makeWS,1500); // retry WS connection
if (wsRpt++ < 10) setTimeout(makeWS,wsRpt * 200); // retry WS connection
ws = null;
}
ws.onopen = (e)=>{
@@ -1470,6 +1430,7 @@ function readState(s,command=false)
populateSegments(s);
hasRGB = hasWhite = hasCCT = has2D = false;
segLmax = 0; // reset max selected segment length
let i = {};
// determine light capabilities from selected segments
for (let seg of (s.seg||[])) {
@@ -1709,77 +1670,68 @@ function setEffectParameters(idx)
var jsonTimeout;
var reqsLegal = false;
async function requestJson(command=null, retry=0) {
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);
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;
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);
reqsLegal = true;
resolve();
})
.catch((e)=>{
if (retry<10) {
setTimeout(() => requestJson(command,retry+1).then(resolve).catch(reject), retry*50);
} else {
showToast(e, true);
resolve();
}
});
});
}
@@ -2554,7 +2506,7 @@ function saveP(i,pl)
}
populatePresets();
resetPUtil();
setTimeout(()=>{pmtLast=0; loadPresets();}, 750); // force reloading of presets
setTimeout(()=>{loadPresets();}, 750); // force reloading of presets
}
function testPl(i,bt) {
@@ -2820,56 +2772,51 @@ 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
var lsPalData = localStorage.getItem("wledPalx");
if (lsPalData) {
try {
var d = JSON.parse(lsPalData);
if (d && d.vid == lastinfo.vid) {
palettesData = d.p;
redrawPalPrev();
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("wledPalx", 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, retry=0) {
fetch(getURL(`/json/palx?page=${page}`), {method: 'get'})
.then(res => res.ok ? res.json() : Promise.reject())
.then(json => {
retry = false;
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) {
setTimeout(()=>{getPalettesData(page,callback,retry+1);}, 100);
} else {
showToast(error, true);
callback();
}
showToast(error, true);
});
}
/*
function hideModes(txt)
{
@@ -2971,7 +2918,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
@@ -3218,7 +3165,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) => {
@@ -3231,7 +3178,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;
@@ -3279,7 +3226,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";
@@ -3291,7 +3238,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";

View File

@@ -17,8 +17,15 @@
position: absolute;
}
</style>
<script src="common.js"></script>
<script>
// load common.js with retry on error
(function common() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => S();
l.onerror = () => setTimeout(common, 100);
document.head.appendChild(l);
})();
var ws;
var tmout = null;
var c;
@@ -31,7 +38,7 @@
ctx.fillRect(Math.round((i - start) * w / skip), 0, Math.ceil(w), c.height);
}
}
function update() { // via HTTP (/json/live)
function update(retry=0) { // via HTTP (/json/live)
if (d.hidden) {
clearTimeout(tmout);
tmout = setTimeout(update, 250);
@@ -53,7 +60,7 @@
.catch((error)=>{
//console.error("Peek HTTP error:",error);
clearTimeout(tmout);
tmout = setTimeout(update, 2500);
if (retry<5) tmout = setTimeout(() => update(retry+1), 2500); // stop endlessly bugging the ESP if resource is not available
})
}
function S() { // Startup function (onload)
@@ -62,10 +69,7 @@
if (window.location.href.indexOf("?ws") == -1) {update(); return;}
// Initialize WebSocket connection
ws = connectWs(function () {
//console.info("Peek WS open");
ws.send('{"lv":true}');
});
ws = connectWs(ws => ws.send('{"lv":true}'));
ws.addEventListener('message', (e) => {
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
@@ -81,7 +85,7 @@
}
</script>
</head>
<body onload="S()">
<body>
<canvas id="canv"></canvas>
</body>
</html>

View File

@@ -10,11 +10,18 @@
margin: 0;
}
</style>
<script src="common.js"></script>
</head>
<body>
<canvas id="canv"></canvas>
<script>
// load common.js with retry on error
(function common() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => S();
l.onerror = () => setTimeout(common, 100);
document.head.appendChild(l);
})();
var c = document.getElementById('canv');
var leds = "";
var throttled = false;
@@ -22,36 +29,35 @@
c.width = window.innerWidth * 0.98; //remove scroll bars
c.height = window.innerHeight * 0.98; //remove scroll bars
}
setCanvas();
// Check for canvas support
var ctx = c.getContext('2d');
if (ctx) { // Access the rendering context
// use parent WS or open new
var ws = connectWs(()=>{
ws.send('{"lv":true}');
});
ws.addEventListener('message',(e)=>{
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
let leds = new Uint8Array(e.data);
if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp
let mW = leds[2]; // matrix width
let mH = leds[3]; // matrix height
let pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)
let lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)
var i = 4;
for (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {
ctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;
ctx.beginPath();
ctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);
ctx.fill();
i+=3;
function S() { // Startup function (onload)
setCanvas();
// Check for canvas support
var ctx = c.getContext('2d');
if (ctx) { // Access the rendering context
ws = connectWs(ws => ws.send('{"lv":true}')); // use parent WS or open new
ws.addEventListener('message',(e)=>{
try {
if (toString.call(e.data) === '[object ArrayBuffer]') {
let leds = new Uint8Array(e.data);
if (leds[0] != 76 || leds[1] != 2 || !ctx) return; //'L', set in ws.cpp
let mW = leds[2]; // matrix width
let mH = leds[3]; // matrix height
let pPL = Math.min(c.width / mW, c.height / mH); // pixels per LED (width of circle)
let lOf = Math.floor((c.width - pPL*mW)/2); //left offset (to center matrix)
var i = 4;
for (y=0.5;y<mH;y++) for (x=0.5; x<mW; x++) {
ctx.fillStyle = `rgb(${leds[i]},${leds[i+1]},${leds[i+2]})`;
ctx.beginPath();
ctx.arc(x*pPL+lOf, y*pPL, pPL*0.4, 0, 2 * Math.PI);
ctx.fill();
i+=3;
}
}
} catch (err) {
console.error("Peek WS error:",err);
}
} catch (err) {
console.error("Peek WS error:",err);
}
});
});
}
}
// window.resize event listener
window.addEventListener('resize', (e)=>{

View File

@@ -4,12 +4,19 @@
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>WLED Settings</title>
<script src="common.js" type="text/javascript"></script>
<script>
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=0'), false); // If we set async false, file is loaded and executed, then next statement is processed
}
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
// load style.css then initialize
l.onload = () => loadResources(['style.css'], () => {
getLoc();
loadJS(getURL('/settings/s.js?p=0'), false);
});
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
</script>
<style>
@import url("style.css");
@@ -31,7 +38,7 @@
}
</style>
</head>
<body onload="S()">
<body>
<button type=submit id="b" onclick="window.location=getURL('/')">Back</button>
<button type="submit" onclick="window.location=getURL('/settings/wifi')">WiFi Setup</button>
<button type="submit" onclick="window.location=getURL('/settings/leds')">LED Preferences</button>

View File

@@ -4,11 +4,20 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>2D Set-up</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var maxPanels=64;
var ctx = null;
function fS(){d.Sf.submit();} // <button type=submit> sometimes didn't work
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=10'), false, undefined, ()=>{
@@ -238,9 +247,8 @@ Y: <input name="P${i}Y" type="number" min="0" max="255" value="0" oninput="UI()"
gId("MD").innerHTML = "Matrix Dimensions (W*H=LC): " + maxWidth + " x " + maxHeight + " = " + maxWidth * maxHeight;
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/2D')">?</button></div>

View File

@@ -4,8 +4,16 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>DMX Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function HW(){window.open("https://kno.wled.ge/interfaces/dmx-output/");}
function GCH(num) {
gId('dmxchannels').innerHTML += "";
@@ -38,9 +46,8 @@
if (loc) d.Sf.action = getURL('/settings/dmx');
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="HW()">?</button></div>

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>LED Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var maxB=1,maxD=1,maxA=1,maxV=0,maxM=4000,maxPB=2048,maxL=1664,maxCO=5,maxBT=4; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
var customStarts=false,startsDirty=[];
@@ -26,6 +26,15 @@
function numPins(t){ return Math.max(gT(t).t.length, 1); } // type length determines number of GPIO pins
function chrID(x) { return String.fromCharCode((x<10?48:55)+x); }
function toNum(c) { let n=c.charCodeAt(0); return (n>=48 && n<=57)?n-48:(n>=65 && n<=90)?n-55:0; } // convert char (0-9A-Z) to number (0-35)
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=2'), false, ()=>{
@@ -846,9 +855,8 @@ Swap: <select id="xw${s}" name="XW${s}">
});
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#led-settings')">?</button></div>

View File

@@ -4,8 +4,16 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>Misc Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function U() { window.open(getURL("/update"),"_self"); }
function checkNum(o) {
const specialkeys = ["Backspace", "Tab", "Enter", "Shift", "Control", "Alt", "Pause", "CapsLock", "Escape", "Space", "PageUp", "PageDown", "End", "Home", "ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown", "Insert", "Delete"];
@@ -30,11 +38,8 @@
if (loc) d.Sf.action = getURL('/settings/sec');
}
</script>
<style>
@import url("style.css");
</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#security-settings')">?</button></div>

View File

@@ -4,8 +4,16 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>Sync Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function adj(){if (d.Sf.DI.value == 6454) {if (d.Sf.EU.value == 1) d.Sf.EU.value = 0;}
else if (d.Sf.DI.value == 5568) {if (d.Sf.DA.value == 0) d.Sf.DA.value = 1; if (d.Sf.EU.value == 0) d.Sf.EU.value = 1;} }
function FC()
@@ -43,9 +51,8 @@
function hideDMXInput(){gId("dmxInput").style.display="none";}
function hideNoDMXInput(){gId("dmxInputOff").style.display="none";}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post" onsubmit="GC()">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('interfaces/udp-notifier/')">?</button></div>

View File

@@ -4,10 +4,19 @@
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>Time Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var el=false;
var ms=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=5'), false, ()=>{BTa();}, ()=>{
@@ -119,9 +128,8 @@
if (parseFloat(d.Sf.LN.value)<0) { d.Sf.LNR.value = "W"; d.Sf.LN.value = -1*parseFloat(d.Sf.LN.value); } else d.Sf.LNR.value = "E";
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post" onsubmit="Wd()">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#time-settings')">?</button></div>

View File

@@ -4,10 +4,19 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>UI Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var initial_ds, initial_st, initial_su, oldUrl;
var sett = null;
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
var l = {
"comp":{
"labels":"Show button labels",
@@ -208,9 +217,8 @@
gId("theme_bg_rnd").checked = false;
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#user-interface-settings')">?</button></div>

View File

@@ -4,12 +4,22 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>Usermod Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
var umCfg = {};
var pins = [], pinO = [], owner;
var urows;
var numM = 0;
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
function S() {
getLoc();
// load settings and insert values into DOM
@@ -269,10 +279,9 @@
if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post" onsubmit="svS(event)">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H()">?</button></div>

View File

@@ -4,8 +4,17 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
<title>WiFi Settings</title>
<script src="common.js" type="text/javascript"></script>
<style> html { visibility: hidden; } </style> <!-- prevent white & ugly display while loading, unhidden in loadResources() -->
<script>
// load common.js with retry on error
(function loadFiles() {
const l = document.createElement('script');
l.src = 'common.js';
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
l.onerror = () => setTimeout(loadFiles, 100);
document.head.appendChild(l);
})();
var scanLoops = 0, preScanSSID = "";
var maxNetworks = 3;
function N() {
@@ -178,9 +187,8 @@ Static subnet mask:<br>
}
</script>
<style>@import url("style.css");</style>
</head>
<body onload="S()">
<body>
<form id="form_s" name="Sf" method="post">
<div class="toprow">
<div class="helpB"><button type="button" onclick="H('features/settings/#wifi-settings')">?</button></div>

View File

@@ -157,12 +157,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();