2021-05-07 12:41:39 +02:00
|
|
|
<!DOCTYPE html>
|
2023-10-04 21:42:15 +02:00
|
|
|
<html lang="en">
|
|
|
|
<head>
|
2022-09-10 19:27:00 +02:00
|
|
|
<meta charset="utf-8">
|
2023-10-04 21:37:10 +02:00
|
|
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
2022-09-10 19:27:00 +02:00
|
|
|
<title>Usermod Settings</title>
|
|
|
|
<script>
|
|
|
|
var d = document;
|
2023-06-10 20:43:27 +02:00
|
|
|
d.max_gpio = 50;
|
2023-01-15 15:19:48 +01:00
|
|
|
d.um_p = [];
|
|
|
|
d.rsvd = [];
|
2023-01-18 22:23:34 +01:00
|
|
|
d.ro_gpio = [];
|
2023-06-16 22:06:26 +02:00
|
|
|
d.extra = [];
|
2022-09-10 19:27:00 +02:00
|
|
|
var umCfg = {};
|
|
|
|
var pins = [], pinO = [], owner;
|
2023-06-04 18:40:29 +02:00
|
|
|
var loc = false, locip, locproto = "http:";
|
2022-09-10 19:27:00 +02:00
|
|
|
var urows;
|
|
|
|
var numM = 0;
|
|
|
|
function gId(s) { return d.getElementById(s); }
|
|
|
|
function isO(i) { return (i && typeof i === 'object' && !Array.isArray(i)); }
|
|
|
|
function H() { window.open("https://github.com/Aircoookie/WLED/wiki/Settings#usermod-settings"); }
|
2023-06-04 18:40:29 +02:00
|
|
|
function B() { window.open(getURL("/settings"),"_self"); }
|
2022-06-23 17:42:02 +02:00
|
|
|
// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript
|
|
|
|
function loadJS(FILE_URL, async = true) {
|
|
|
|
let scE = d.createElement("script");
|
2023-11-09 05:45:48 +01:00
|
|
|
scE.setAttribute("src", FILE_URL + "&c=" + Date.now());
|
2022-06-23 17:42:02 +02:00
|
|
|
scE.setAttribute("type", "text/javascript");
|
|
|
|
scE.setAttribute("async", async);
|
|
|
|
d.body.appendChild(scE);
|
|
|
|
// success event
|
|
|
|
scE.addEventListener("load", () => {
|
|
|
|
GetV();
|
2023-06-16 22:06:26 +02:00
|
|
|
for (let r of d.rsvd) { pins.push(r); pinO.push("rsvd"); } // reserved pins
|
|
|
|
if (d.um_p[0]==-1) d.um_p.shift(); // remove filler
|
|
|
|
d.Sf.SDA.max = d.Sf.SCL.max = d.Sf.MOSI.max = d.Sf.SCLK.max = d.Sf.MISO.max = d.max_gpio;
|
|
|
|
//for (let i of d.getElementsByTagName("input")) if (i.type === "number" && i.name.replace("[]","").substr(-3) === "pin") i.max = d.max_gpio;
|
|
|
|
pinDropdowns(); // convert INPUT to SELECT for pins
|
2022-06-23 17:42:02 +02:00
|
|
|
});
|
|
|
|
// error event
|
|
|
|
scE.addEventListener("error", (ev) => {
|
|
|
|
console.log("Error on loading file", ev);
|
|
|
|
alert("Loading of configuration script failed.\nIncomplete page data!");
|
|
|
|
});
|
|
|
|
}
|
2022-09-10 19:27:00 +02:00
|
|
|
function S() {
|
2023-06-07 21:37:54 +02:00
|
|
|
let l = window.location;
|
|
|
|
if (l.protocol == "file:") {
|
2022-09-10 19:27:00 +02:00
|
|
|
loc = true;
|
|
|
|
locip = localStorage.getItem('locIp');
|
|
|
|
if (!locip) {
|
|
|
|
locip = prompt("File Mode. Please enter WLED IP!");
|
|
|
|
localStorage.setItem('locIp', locip);
|
|
|
|
}
|
2023-06-04 18:40:29 +02:00
|
|
|
} else {
|
|
|
|
// detect reverse proxy
|
2023-06-07 21:37:54 +02:00
|
|
|
let path = l.pathname;
|
2023-06-04 18:40:29 +02:00
|
|
|
let paths = path.slice(1,path.endsWith('/')?-1:undefined).split("/");
|
|
|
|
if (paths.length > 2) {
|
2023-06-07 21:37:54 +02:00
|
|
|
locproto = l.protocol;
|
2023-06-04 18:40:29 +02:00
|
|
|
loc = true;
|
2023-06-08 07:14:03 +02:00
|
|
|
locip = l.hostname + (l.port ? ":" + l.port : "") + "/" + paths[0];
|
2023-06-04 18:40:29 +02:00
|
|
|
}
|
2022-09-10 19:27:00 +02:00
|
|
|
}
|
|
|
|
ldS();
|
|
|
|
if (!numM) gId("um").innerHTML = "No Usermods installed.";
|
2023-06-04 18:40:29 +02:00
|
|
|
if (loc) d.Sf.action = getURL('/settings/um');
|
|
|
|
}
|
|
|
|
function getURL(path) {
|
|
|
|
return (loc ? locproto + "//" + locip : "") + path;
|
2022-09-10 19:27:00 +02:00
|
|
|
}
|
|
|
|
// https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer
|
|
|
|
function isF(n) { return n === +n && n !== (n|0); }
|
|
|
|
function isI(n) { return n === +n && n === (n|0); }
|
|
|
|
function check(o,k) { // input object, pin owner key
|
2023-06-16 22:06:26 +02:00
|
|
|
/* no longer necessary with pin dropdown fields
|
2022-09-10 19:27:00 +02:00
|
|
|
var n = o.name.replace("[]","").substr(-3);
|
|
|
|
if (o.type=="number" && n.substr(0,3)=="pin") {
|
|
|
|
for (var i=0; i<pins.length; i++) {
|
|
|
|
if (k==pinO[i]) continue;
|
|
|
|
if (o.value==pins[i] && pinO[i]==="if") { o.style.color="lime"; break; }
|
|
|
|
if (o.value==pins[i] || o.value<-1 || o.value>d.max_gpio) { o.style.color="red"; break; } else o.style.color=d.ro_gpio.some((e)=>e==parseInt(o.value,10))?"orange":"#fff";
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
switch (o.name) {
|
|
|
|
case "SDA": break;
|
|
|
|
case "SCL": break;
|
|
|
|
case "MOSI": break;
|
|
|
|
case "SCLK": break;
|
|
|
|
case "MISO": break;
|
|
|
|
default: return;
|
|
|
|
}
|
|
|
|
for (var i=0; i<pins.length; i++) {
|
|
|
|
//if (k==pinO[i]) continue; // same owner
|
|
|
|
if (o.value==pins[i] && pinO[i]==="if") { o.style.color="tomato"; break; }
|
|
|
|
if (o.value==pins[i] || o.value<-1 || o.value>d.max_gpio) { o.style.color="red"; break; } else o.style.color=d.ro_gpio.some((e)=>e==parseInt(o.value,10))?"orange":"#fff";
|
|
|
|
}
|
|
|
|
}
|
2023-06-16 22:06:26 +02:00
|
|
|
*/
|
2022-09-10 19:27:00 +02:00
|
|
|
}
|
|
|
|
function getPins(o) {
|
|
|
|
if (isO(o)) {
|
|
|
|
for (const [k,v] of Object.entries(o)) {
|
|
|
|
if (isO(v)) {
|
2023-06-10 20:43:27 +02:00
|
|
|
let oldO = owner; // keep parent name
|
2022-09-10 19:27:00 +02:00
|
|
|
owner = k;
|
|
|
|
getPins(v);
|
2023-06-10 20:43:27 +02:00
|
|
|
owner = oldO;
|
2022-09-10 19:27:00 +02:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (k.replace("[]","").substr(-3)=="pin") {
|
|
|
|
if (Array.isArray(v)) {
|
|
|
|
for (var i=0; i<v.length; i++) if (v[i]>=0) { pins.push(v[i]); pinO.push(owner); }
|
|
|
|
} else {
|
|
|
|
if (v>=0) { pins.push(v); pinO.push(owner); }
|
|
|
|
}
|
|
|
|
} else if (Array.isArray(v)) {
|
|
|
|
for (var i=0; i<v.length; i++) getPins(v[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-11-26 03:44:04 +01:00
|
|
|
function initCap(s) {
|
2022-12-11 10:43:16 +01:00
|
|
|
if (typeof s !== 'string') return '';
|
|
|
|
// https://www.freecodecamp.org/news/how-to-capitalize-words-in-javascript/
|
|
|
|
return s.replace(/[\W_]/g,' ').replace(/(^\w{1})|(\s+\w{1})/g, l=>l.toUpperCase()); // replace - and _ with space, capitalize every 1st letter
|
2022-11-26 03:44:04 +01:00
|
|
|
}
|
2022-09-10 19:27:00 +02:00
|
|
|
function addField(k,f,o,a=false) { //key, field, (sub)object, isArray
|
|
|
|
if (isO(o)) {
|
2022-11-11 20:20:11 +01:00
|
|
|
urows += '<hr class="sml">';
|
2022-11-26 03:44:04 +01:00
|
|
|
if (f!=='unknown' && !k.includes(":")) urows += `<p><u>${initCap(f)}</u></p>`; //show group title
|
2022-09-10 19:27:00 +02:00
|
|
|
for (const [s,v] of Object.entries(o)) {
|
|
|
|
// possibility to nest objects (only 1 level)
|
|
|
|
if (f!=='unknown' && !k.includes(":")) addField(k+":"+f,s,v);
|
|
|
|
else addField(k,s,v);
|
|
|
|
}
|
|
|
|
} else if (Array.isArray(o)) {
|
|
|
|
for (var j=0; j<o.length; j++) {
|
|
|
|
addField(k,f,o[j],true);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
var c, t = typeof o;
|
|
|
|
switch (t) {
|
|
|
|
case "boolean":
|
|
|
|
t = "checkbox"; c = 'value="true"' + (o ? ' checked' : '');
|
|
|
|
break;
|
|
|
|
case "number":
|
|
|
|
c = `value="${o}"`;
|
|
|
|
if (f.substr(-3)==="pin") {
|
|
|
|
c += ` max="${d.max_gpio}" min="-1" class="s"`;
|
|
|
|
t = "int";
|
|
|
|
} else {
|
|
|
|
c += ' step="any" class="xxl"';
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
t = "text"; c = `value="${o}" style="width:250px;"`;
|
|
|
|
break;
|
|
|
|
}
|
2022-11-26 03:44:04 +01:00
|
|
|
urows += ` ${initCap(f)} `; //only show field (key is shown in grouping)
|
2022-09-10 19:27:00 +02:00
|
|
|
// https://stackoverflow.com/questions/11657123/posting-both-checked-and-unchecked-checkboxes
|
|
|
|
if (t=="checkbox") urows += `<input type="hidden" name="${k}:${f}${a?"[]":""}" value="false">`;
|
|
|
|
else if (!a) urows += `<input type="hidden" name="${k}:${f}${a?"[]":""}" value="${t}">`;
|
|
|
|
urows += `<input type="${t==="int"?"number":t}" name="${k}:${f}${a?"[]":""}" ${c} oninput="check(this,'${k.substr(k.indexOf(":")+1)}')"><br>`;
|
|
|
|
}
|
|
|
|
}
|
2023-06-10 20:43:27 +02:00
|
|
|
function pinDropdowns() {
|
|
|
|
for (let i of d.Sf.elements) {
|
|
|
|
if (i.type === "number" && (i.name.includes("pin") || ["SDA","SCL","MOSI","MISO","SCLK"].includes(i.name))) { //select all pin select elements
|
|
|
|
let v = parseInt(i.value);
|
|
|
|
let sel = addDropdown(i.name,0);
|
|
|
|
for (var j = -1; j <= d.max_gpio; j++) {
|
|
|
|
if (d.rsvd.includes(j)) continue;
|
|
|
|
let foundPin = pins.indexOf(j);
|
|
|
|
let txt = (j === -1) ? "unused" : `${j}`;
|
|
|
|
if (foundPin >= 0 && j !== v) txt += ` ${pinO[foundPin]=="if"?"global":pinO[foundPin]}`; // already reserved pin
|
|
|
|
if (d.ro_gpio.includes(j)) txt += " (R/O)";
|
|
|
|
let opt = addOption(sel, txt, j);
|
|
|
|
if (j === v) opt.selected = true; // this is "our" pin
|
|
|
|
else if (pins.includes(j)) opt.disabled = true; // someone else's pin
|
|
|
|
}
|
2023-06-16 22:06:26 +02:00
|
|
|
let um = i.name.split(":")[0];
|
|
|
|
d.extra.forEach((o)=>{
|
|
|
|
if (o[um] && o[um].pin) o[um].pin.forEach((e)=>{
|
|
|
|
let opt = addOption(sel,e[0],e[1]);
|
|
|
|
if (e[1]==v) opt.selected = true;
|
|
|
|
});
|
|
|
|
});
|
2023-06-10 20:43:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function UI(e) {
|
|
|
|
// update changed select options across all usermods
|
|
|
|
let oldV = parseInt(e.dataset.val);
|
|
|
|
e.dataset.val = e.value;
|
|
|
|
let txt = e.name.split(":")[e.name.split(":").length-2];
|
|
|
|
let selects = d.Sf.querySelectorAll("select[class='pin']");
|
|
|
|
for (let sel of selects) {
|
|
|
|
if (sel == e) continue
|
|
|
|
Array.from(sel.options).forEach((i)=>{
|
|
|
|
if (!(i.value==oldV || i.value==e.value)) return;
|
|
|
|
if (i.value == -1) {
|
|
|
|
i.text = "unused";
|
|
|
|
return
|
|
|
|
}
|
2023-06-16 22:06:26 +02:00
|
|
|
if (i.value<100) { // TODO remove this hack and use d.extra
|
|
|
|
i.text = i.value;
|
|
|
|
if (i.value==oldV) {
|
|
|
|
i.disabled = false;
|
|
|
|
}
|
|
|
|
if (i.value==e.value) {
|
|
|
|
i.disabled = true;
|
|
|
|
i.text += ` ${txt}`;
|
|
|
|
}
|
|
|
|
if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)";
|
2023-06-10 20:43:27 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2022-09-10 19:27:00 +02:00
|
|
|
// https://stackoverflow.com/questions/39729741/javascript-change-input-text-to-select-option
|
|
|
|
function addDropdown(um,fld) {
|
|
|
|
let sel = d.createElement('select');
|
2023-06-10 20:43:27 +02:00
|
|
|
if (typeof(fld) === "string") { // parameter from usermod (field name)
|
|
|
|
if (fld.includes("pin")) sel.classList.add("pin");
|
|
|
|
um += ":"+fld;
|
|
|
|
} else if (typeof(fld) === "number") sel.classList.add("pin"); // a hack to add a class
|
|
|
|
let arr = d.getElementsByName(um);
|
|
|
|
let idx = arr[0].type==="hidden"?1:0; // ignore hidden field
|
|
|
|
if (arr.length > 2) {
|
|
|
|
// we have array of values (usually pins)
|
|
|
|
for (let i of arr) {
|
|
|
|
if (i.type === "number") break;
|
|
|
|
idx++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let inp = arr[idx];
|
2022-09-10 19:27:00 +02:00
|
|
|
if (inp && inp.tagName === "INPUT" && (inp.type === "text" || inp.type === "number")) { // may also use nodeName
|
|
|
|
let v = inp.value;
|
|
|
|
let n = inp.name;
|
|
|
|
// copy the existing input element's attributes to the new select element
|
|
|
|
for (var i = 0; i < inp.attributes.length; ++ i) {
|
|
|
|
var att = inp.attributes[i];
|
|
|
|
// type and value don't apply, so skip them
|
|
|
|
// ** you might also want to skip style, or others -- modify as needed **
|
2023-06-16 22:06:26 +02:00
|
|
|
if (att.name != 'type' && att.name != 'value' && att.name != 'class' && att.name != 'style' && att.name != 'oninput' && att.name != 'max' && att.name != 'min') {
|
2022-09-10 19:27:00 +02:00
|
|
|
sel.setAttribute(att.name, att.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sel.setAttribute("data-val", v);
|
2023-06-10 20:43:27 +02:00
|
|
|
sel.setAttribute("onchange", "UI(this)");
|
2022-09-10 19:27:00 +02:00
|
|
|
// finally, replace the old input element with the new select element
|
|
|
|
inp.parentElement.replaceChild(sel, inp);
|
|
|
|
return sel;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
function addOption(sel,txt,val) {
|
|
|
|
if (sel===null) return; // select object missing
|
|
|
|
let opt = d.createElement("option");
|
|
|
|
opt.value = val;
|
|
|
|
opt.text = txt;
|
|
|
|
sel.appendChild(opt);
|
|
|
|
for (let i=0; i<sel.childNodes.length; i++) {
|
|
|
|
let c = sel.childNodes[i];
|
|
|
|
if (c.value == sel.dataset.val) sel.selectedIndex = i;
|
|
|
|
}
|
2023-06-10 20:43:27 +02:00
|
|
|
return opt;
|
2022-09-10 19:27:00 +02:00
|
|
|
}
|
|
|
|
// https://stackoverflow.com/questions/26440494/insert-text-after-this-input-element-with-javascript
|
2022-11-26 03:44:04 +01:00
|
|
|
function addInfo(name,el,txt, txt2="") {
|
2022-09-10 19:27:00 +02:00
|
|
|
let obj = d.getElementsByName(name);
|
|
|
|
if (!obj.length) return;
|
|
|
|
if (typeof el === "string" && obj[0]) obj[0].placeholder = el;
|
2022-11-26 03:44:04 +01:00
|
|
|
else if (obj[el]) {
|
|
|
|
if (txt!="") obj[el].insertAdjacentHTML('afterend', ' '+txt);
|
|
|
|
if (txt2!="") obj[el].insertAdjacentHTML('beforebegin', txt2 + ' '); //add pre texts
|
|
|
|
}
|
2022-09-10 19:27:00 +02:00
|
|
|
}
|
2023-06-10 20:43:27 +02:00
|
|
|
// add Help Button
|
|
|
|
function addHB(um) {
|
|
|
|
addInfo(um + ':help',0,`<button onclick="location.href='https://kno.wled.ge/usermods/${um}'" type="button">?</button>`);
|
|
|
|
}
|
2022-09-10 19:27:00 +02:00
|
|
|
// load settings and insert values into DOM
|
|
|
|
function ldS() {
|
2023-06-04 18:40:29 +02:00
|
|
|
fetch(getURL('/cfg.json'), {
|
2022-09-10 19:27:00 +02:00
|
|
|
method: 'get'
|
|
|
|
})
|
|
|
|
.then(res => {
|
|
|
|
if (!res.ok) gId('lserr').style.display = "inline";
|
|
|
|
return res.json();
|
|
|
|
})
|
|
|
|
.then(json => {
|
|
|
|
umCfg = json.um;
|
|
|
|
getPins(json);
|
|
|
|
urows="";
|
|
|
|
if (isO(umCfg)) {
|
|
|
|
for (const [k,o] of Object.entries(umCfg)) {
|
|
|
|
urows += `<hr><h3>${k}</h3>`;
|
|
|
|
addField(k,'unknown',o);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (urows==="") urows = "Usermods configuration not found.<br>Press <i>Save</i> to initialize defaults.";
|
|
|
|
gId("um").innerHTML = urows;
|
2023-06-04 18:40:29 +02:00
|
|
|
loadJS(getURL('/settings/s.js?p=8'), false); // If we set async false, file is loaded and executed, then next statement is processed
|
2022-09-10 19:27:00 +02:00
|
|
|
})
|
|
|
|
.catch((error)=>{
|
|
|
|
gId('lserr').style.display = "inline";
|
|
|
|
console.log(error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
function svS(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
<style>@import url("style.css");</style>
|
2021-05-07 12:41:39 +02:00
|
|
|
</head>
|
|
|
|
|
|
|
|
<body onload="S()">
|
|
|
|
<form id="form_s" name="Sf" method="post" onsubmit="svS(event)">
|
|
|
|
<div class="toprow">
|
|
|
|
<div class="helpB"><button type="button" onclick="H()">?</button></div>
|
|
|
|
<button type="button" onclick="B()">Back</button><button type="submit">Save</button><br>
|
|
|
|
<span id="lssuc" style="color:green; display:none">✔ Configuration saved!</span>
|
|
|
|
<span id="lserr" style="color:red; display:none">⚠ Could not load configuration.</span><hr>
|
|
|
|
</div>
|
|
|
|
<h2>Usermod Setup</h2>
|
2022-09-10 19:27:00 +02:00
|
|
|
Global I<sup>2</sup>C GPIOs (HW)<br>
|
2023-05-30 19:36:14 +02:00
|
|
|
<i class="warn">(change requires reboot!)</i><br>
|
2022-09-11 00:18:33 +02:00
|
|
|
SDA:<input type="number" min="-1" max="48" name="SDA" onchange="check(this,'if')" class="s" placeholder="SDA">
|
|
|
|
SCL:<input type="number" min="-1" max="48" name="SCL" onchange="check(this,'if')" class="s" placeholder="SCL">
|
2022-11-11 20:20:11 +01:00
|
|
|
<hr class="sml">
|
2022-09-10 19:27:00 +02:00
|
|
|
Global SPI GPIOs (HW)<br>
|
2023-05-30 19:36:14 +02:00
|
|
|
<i class="warn">(only changable on ESP32, change requires reboot!)</i><br>
|
2022-09-11 00:18:33 +02:00
|
|
|
MOSI:<input type="number" min="-1" max="48" name="MOSI" onchange="check(this,'if')" class="s" placeholder="MOSI">
|
|
|
|
MISO:<input type="number" min="-1" max="48" name="MISO" onchange="check(this,'if')" class="s" placeholder="MISO">
|
|
|
|
SCLK:<input type="number" min="-1" max="48" name="SCLK" onchange="check(this,'if')" class="s" placeholder="SCLK">
|
2023-05-30 19:36:14 +02:00
|
|
|
<hr class="sml">
|
|
|
|
Reboot after save? <input type="checkbox" name="RBT"><br>
|
2022-09-10 19:27:00 +02:00
|
|
|
<div id="um">Loading settings...</div>
|
2021-05-07 12:41:39 +02:00
|
|
|
<hr><button type="button" onclick="B()">Back</button><button type="submit">Save</button>
|
|
|
|
</form>
|
|
|
|
</body>
|
|
|
|
|
|
|
|
</html>
|