Files
WLED_MM_Infinity/wled00/data/settings_um.htm
Frank b7d81e7fca bugfix for wrongly disabled PINs
Skips pin arrays for special bus types where pin array doesn't contain GPIO numbers, but allows all other entries
2026-02-05 00:07:22 +01:00

402 lines
16 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<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>
var d = document;
d.max_gpio = 39;
d.um_p = [];
d.rsvd = [];
d.ro_gpio = [];
d.h_pins = [];
d.x_pins = [];
var umCfg = {};
var pins = [], pinO = [], owner;
var loc = false, locip;
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://mm.kno.wled.ge/usermods/AutoPlaylist/"); }
function B() { window.open("/settings","_self"); }
// 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");
scE.setAttribute("src", FILE_URL);
scE.setAttribute("type", "text/javascript");
scE.setAttribute("async", async);
d.body.appendChild(scE);
// success event
scE.addEventListener("load", () => {
GetV();
for (let k=0; k<d.rsvd.length; k++) { pins.push(d.rsvd[k]); pinO.push("rsvd"); }
if (d.um_p[0]==-1) d.um_p.shift();
});
// error event
scE.addEventListener("error", (ev) => {
console.log("Error on loading file", ev);
alert("Loading of configuration script failed.\nIncomplete page data!");
});
}
function S() {
if (window.location.protocol == "file:") {
loc = true;
locip = localStorage.getItem('locIp');
if (!locip) {
locip = prompt("File Mode. Please enter WLED IP!");
localStorage.setItem('locIp', locip);
}
}
ldS();
// if (!numM) gId("um").innerHTML = ""; //WLEDMM: Do not display no usermods installed
}
// 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) {} //WLEDMM not needed as we use dropdowns
function getPins(o) {
if (isO(o)) {
// If this object is a bus instance, extract the "type" field
let busType = o.type !== undefined ? o.type : -1;
for (const [k,v] of Object.entries(o)) {
if (isO(v)) {
owner = k;
getPins(v);
continue;
}
if (k.replace("[]","").substr(-3)=="pin") {
// Skip pin arrays for special bus types where pin array doesn't contain GPIO numbers, but allow all other entries
if (busType >= 80 && busType < 96) continue; // Network buses - pin array stores IP address
if (busType >= 100 && busType <= 110) continue; // HUB75 buses - pin array stores chain length
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]);
}
}
}
}
//WLEDMM util function
function initCap(s) {
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
}
function addField(k,f,o,a=false) { //key, field, (sub)object, isArray
if (isO(o)) {
urows += '<hr class="sml">';
if (f!=='unknown' && !k.includes(":")) urows += `<p><u>${initCap(f)}</u></p>`; //WLEDMM show group title
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") { //WLEDMM: this will not be used and should never happen as pins are now dropdowns
c += ` max="50" min="-1" class="s"`; //WLEDMM: hardcoded to 50 as d.max_gpio is not calculated yet here (done in appendGPIOinfo)
t = "int";
} else {
c += ' step="any" class="xxl"';
}
break;
default:
t = "text"; c = `value="${o}" style="width:250px;"`;
break;
}
urows += ` ${initCap(f)} `; //WLEDMM only show field (key is shown in grouping)
// 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}">`;
// WLEDMM make a dropdown for pin variables
if (f.includes("pin")) {
var n = this.name.replace("[]","").substr(-3);
urows += `<select name="${k}:${f}${a?"[]":""}" style="width:200px;max-width:90%;">`; //WLEDMM width capped. to be moved to css one day
for (var j=-1; j<=50; j++) { // all possible pins WLEDMM: hardcoded to 50 as d.max_gpio is not calculated yet here (done in appendGPIOinfo)
let foundPin = -1;
for (var i=0; i<pins.length; i++) { // check if pin is reserved
if (pins[i] == j) foundPin = i;
}
urows += `<option value="${j}"`;
if (j==o) urows += ` selected`; //add selected value
if (foundPin >=0) {// already reserved pin
if (!k.includes(pinO[foundPin])) urows += ` disabled`; // && pinO[foundPin] != "if"
}
if (j==-1) urows += `>undefined`; else urows += `>${j}`; // don't show -1
if (foundPin >=0) {// already reserved pin
urows += ` ${pinO[foundPin]=="if"?"global":pinO[foundPin]}`;
if (!k.includes(pinO[foundPin])) urows += " 🔴"; else urows += " 🟢"; //add pins assigned here || pinO[foundPin] == "if"
}
urows += `</option>`;
// if (j==5) //exclude pin 6 to 11
// j=11
}
urows += `</select>`;
}
else
urows += `<input type="${t==="int"?"number":t}" name="${k}:${f}${a?"[]":""}" ${c}>`; //WLEDMM no need for oninput="check(this,'${k.substr(k.indexOf(":")+1)}')"
urows += `<br>`;
}
}
function adF(k,f,o,a=false) { //shortcut for addField(key, field, (sub)object, isArray)
return addField(k,f,o,a);
}
// https://stackoverflow.com/questions/39729741/javascript-change-input-text-to-select-option
function addDropdown(um,fld) {
let sel = d.createElement('select');
let arr = d.getElementsByName(um+":"+fld);
let inp = arr[1]; // assume 1st field to be hidden (type)
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 **
if (att.name != 'type' && att.name != 'value' && att.name != 'class' && att.name != 'style') {
sel.setAttribute(att.name, att.value);
}
}
sel.setAttribute("data-val", v);
// finally, replace the old input element with the new select element
inp.parentElement.replaceChild(sel, inp);
return sel;
}
return null;
}
function adD(um,fld) { // shortcut for addDropdown(um,fld)
return addDropdown(um,fld);
}
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;
}
}
function adO(sel,txt,val) { // shortcut for addOption(sel,txt,val)
return addOption(sel,txt,val);
}
//WLEDMM: replace Option to set globals
function rOpt(name,el,txt,val) {
let obj = d.getElementsByName(name);
var select = obj;
if (obj[el]) select = obj[el];
for (let i=0; i<select.childNodes.length; i++) {
let c = select.childNodes[i];
if (c.value == val) c.text = txt;
}
}
//WLEDMM: extend Option to set build flag defaults
function xOpt(name,el,txt,val) {
let obj = d.getElementsByName(name);
var select = obj;
if (obj[el]) select = obj[el];
for (let i=0; i<select.childNodes.length; i++) {
let c = select.childNodes[i];
if (c.value == val) c.text += txt;
}
}
//WLEDMM: delete Options to remove options e.g. mclk
function dOpt(name,el,valFrom,valTo) {
let obj = d.getElementsByName(name);
var select = obj;
if (obj[el]) select = obj[el];
for (let i=0; i<select.options.length; i++) {
let c = select.options[i];
if (c.value >= valFrom && c.value <= valTo) {
select.removeChild(c);
i--; //decrease i by one because the index has been adjusted
}
//https://www.javascripttutorial.net/javascript-dom/javascript-add-remove-options/
//https://www.javascripttutorial.net/javascript-dom/javascript-remove-items-from-a-select-conditionally/
}
}
//WLEDMM: analog options only
function aOpt(name,el) {
let obj = d.getElementsByName(name);
var select = obj;
if (obj[el]) select = obj[el];
for (let i=0; i<select.options.length; i++) {
let c = select.options[i];
found = false;
for (let jj=0; jj<d.a_pins.length; jj++) if (d.a_pins[jj] == c.value) found = true; //value -1 or analog pins
if (c.value != -1 && !found) {
select.removeChild(c);
i--; //decrease i by one because the index has been adjusted
}
//https://www.javascripttutorial.net/javascript-dom/javascript-add-remove-options/
//https://www.javascripttutorial.net/javascript-dom/javascript-remove-items-from-a-select-conditionally/
}
}
//WLEDMM disable read only pins
function dRO(name,el) {
let obj = d.getElementsByName(name);
var select = obj;
if (obj[el]) select = obj[el];
// console.log("dRO", name, el, obj, "s", select, d.ro_gpio);
for (let i=0; i<select.options.length; i++) {
let c = select.options[i];
// console.log("dRO option", c, c.value, d.ro_gpio.includes(c.value));
for (let j=0; j<d.ro_gpio.length; j++) if (d.ro_gpio[j] == c.value) c.disabled=true; //if (d.ro_gpio.includes(c.value))
}
}
//WLEDMM read only pins 🟠, reserved pins 🟣 and disabled, and remove pins > max_gpio
function pinPost() {
// console.log('pinPost', d.max_gpio, d.ro_gpio, d.rsvd);
var elements = gId("form_s").elements;
for (var i = 0, select; select = elements[i++];) {
if (select.name.includes("pin") && select.options!=null) { //select all pin select elements
// console.log(element);
for (let i=0; i<select.options.length; i++) {
let c = select.options[i];
// console.log("pinPost option", c, c.value, d.ro_gpio.includes(c.value));
for (let j=0; j<d.ro_gpio.length; j++) if (d.ro_gpio[j] == c.value) c.text += " read only 🟠"; //if (d.ro_gpio.includes(c.value)) not working ???
for (let j=0; j<d.rsvd.length; j++) if (d.rsvd[j] == c.value) {c.text += " reserved 🟣"; c.disabled=true;} //now always disabled as post is done last if (d.rsvd.includes(c.value))
for (let j=0; j<d.h_pins.length; j++) if (d.h_pins[j] == c.value && c.text.length <= 4) {c.text += " HUB75 🔴"; c.disabled=true;} // HUB75 pins
for (let j=0; j<d.x_pins.length; j++) if (d.x_pins[j] == c.value && c.text.length <= 4) {c.text += " DMX 🔴"; c.disabled=true;} // DMX pins
//remove pins > max_gpio
if (c.value > d.max_gpio) {
select.removeChild(c);
i--; //decrease i by one because the index has been adjusted
}
//https://www.javascripttutorial.net/javascript-dom/javascript-add-remove-options/
//https://www.javascripttutorial.net/javascript-dom/javascript-remove-items-from-a-select-conditionally/
if (c.text.length <= 4) c.text += " 🟢"; //2 digit number space and ⍼/⎌. If no reserved/read only/other um, then pin can be freely used (green)
for (let jj=0; jj<d.dt_pins.length; jj++) if (d.dt_pins[jj] == c.value) c.text += ((jj<9)?` D${jj}`:((jj==9)?` RX`:` TX`)); //WLEDMM: Add D0-D8, RX/TX to name
for (let jj=0; jj<d.a_pins.length; jj++) if (d.a_pins[jj] == c.value) c.text += `   A${jj}`; //WLEDMM: Add A0-A10
}
}
}
}
// https://stackoverflow.com/questions/26440494/insert-text-after-this-input-element-with-javascript
function addInfo(name,el,txt, txt2="") {
let obj = d.getElementsByName(name);
if (!obj.length) return;
if (typeof el === "string" && obj[0]) obj[0].placeholder = el;
else if (obj[el]) {
if (txt!="") obj[el].insertAdjacentHTML('afterend', '&nbsp;'+txt);
if (txt2!="") obj[el].insertAdjacentHTML('beforebegin', txt2 + '&nbsp;'); //add pre texts
}
}
function adI(name,el,txt, txt2="") { // shortcut for addInfo(name,el,txt, txt2="")
return addInfo(name,el,txt, txt2);
}
// add Help Button
function addHB(um)
{
addInfo(um + ':help',0,'<button onclick=\"location.href=&quot;https://mm.kno.wled.ge/usermods/' + um + '&quot;\" type=\"button\">?</button>')
}
// load settings and insert values into DOM
function ldS() {
var url = (loc?`http://${locip}`:'') + '/cfg.json';
fetch(url, {
method: 'get'
})
.then(res => {
if (!res.ok) gId('lserr').style.display = "inline";
return res.json();
})
.then(json => {
umCfg = json.um;
getPins(json);
urows="";
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const userMod = urlParams.get('um');
if (userMod == null) {
urows+="<hr><h2>Usermods (Pin Manager)</h2>";
urows+="<button onclick=\"location.href=&quot;https://mm.kno.wled.ge/usermods/pinmanager&quot;\" type=\"button\">?</button>";
urows+="<hr class=\"sml\">";
urows+="<i style=\"color:orange\">(change requires reboot!)</i>";
urows+="<hr class=\"sml\">";
urows+="<p><u>Global I2C GPIOs (HW)</u></p>";
addField("if:SDA", "pin", -1, false);
addField("if:SCL", "pin", -1, false);
urows+="<hr class=\"sml\">";
urows+="<p><u>Global SPI GPIOs (HW)</u></p>";
addField("if:MOSI", "pin", -1, false);
addField("if:MISO", "pin", -1, false);
addField("if:SCLK", "pin", -1, false);
}
if (isO(umCfg)) {
//WLEDMM: read url parameter. e.g. um=AudioReactive and if set only add the usermod with the same name
for (const [k,o] of Object.entries(umCfg)) {
if (userMod == k) {
urows += `<hr><h3>${k}</h3><div name="${k}:help"></div>`;
addField(k,'unknown',o);
}
}
if (userMod != null && urows==="") urows = "Usermods configuration not found.<br>Press <i>Save</i> to initialize defaults.";
if (userMod == null) {
urows += `<hr> `;
urows += `<div style="margin: auto;">`;
urows += `<table style="margin-left:auto; margin-right:auto;">`;
urows += `<tr><th>Usermod</th><th>Enabled</th></tr>`
for (const [k,o] of Object.entries(umCfg)) {
urows += `<tr><td>${k}</td><td>${o.enabled}</td></tr>`;
}
urows += `</table>`;
urows += `</div>`;
}
}
gId("um").innerHTML = urows;
var url = (loc?`http://${locip}`:'') + '/settings/s.js?p=8';
if (userMod != null) url+= '&um=' + userMod; //WLEDMM add userMod as parameter
loadJS(url, false); // If we set async false, file is loaded and executed, then next statement is processed
})
.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>
</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">&#10004; Configuration saved!</span>
<span id="lserr" style="color:red; display:none">&#9888; Could not load configuration.</span>
</div>
<!-- WLEDMM: no GPIOs here as it is generated -->
<div id="um">Loading settings...</div>
<div name="errorMessage"></div>
<hr><button type="button" onclick="B()">Back</button><button type="submit">Save</button>
</form>
</body>
</html>