diff --git a/wled00/data/common.js b/wled00/data/common.js index 03103798c2..e6cea4d526 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -222,3 +222,138 @@ function sendDDP(ws, start, len, colors) { } return true; } + +// Pin utilities +function getOwnerName(o,t,n) { + // Use firmware-provided name if available + if(n) return n; + if(!o) return "System"; // no owner provided + if(o===0x85){ return getBtnTypeName(t); } // button pin + return "UM #"+o; +} +function getBtnTypeName(t) { + var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"]; + var label = n[t] || "?"; + return 'Button '+label+''; +} +function getCaps(p,c) { + var r=[]; + // Use touch info from settings endpoint + if(d.touch && d.touch.includes(p)) r.push("Touch"); + if(d.ro_gpio && d.ro_gpio.includes(p)) r.push("Input Only"); + // Use other caps from JSON (Analog, Boot, Input Only) + if(c&0x02) r.push("Analog"); + if(c&0x08) r.push("Flash Boot"); + if(c&0x10) r.push("Bootstrap"); + return r.length?r.join(", "):"-"; +} + +// Fetch GPIO caps (/settings/s.js?p=11) then pin occupancy (/json/pins) with retry. +// Caches result in d.pinsData. Calls cb() when ready (or on failure). +// If page already loaded its own s.js (d.max_gpio set), skips caps load and goes straight to pins fetch. +function fetchPinInfo(cb, retries=5) { + if (d.pinsData) { cb&&cb(); return; } + var done=false, fr=retries; + function doFetch() { + fetch(getURL('/json/pins')) + .then(r=>r.json()) + .then(j=>{ if(!done){done=true; d.pinsData=j.pins||[]; cb&&cb();} }) + .catch(()=>{ fr-->0 ? setTimeout(doFetch,100) : (!done&&(done=true,d.pinsData=[],cb&&cb())); }); + } + if (d.max_gpio) { doFetch(); return; } + // Load GPIO caps from s.js?p=11 first (sets d.rsvd/ro_gpio/max_gpio/touch/adc/um_p) + d.max_gpio=50; d.rsvd=[]; d.ro_gpio=[]; d.touch=[]; d.adc=[]; d.um_p=[]; + var cr=retries; + function tryCaps() { + var s=cE("script"); s.src=getURL('/settings/s.js?p=11'); + d.body.appendChild(s); + s.onload=function(){ GetV(); doFetch(); }; + s.onerror=function(){ cr-->0 ? setTimeout(tryCaps,100) : doFetch(); }; + } + tryCaps(); +} + +// Pin dropdown utilities +// Create or rebuild a pin or existing back to +function unmakePinSelect(name) { + let sel = gN(name); + if (!sel || sel.tagName !== "SELECT") return null; + let inp = cE('input'); + inp.type = "number"; + inp.name = sel.name; + inp.value = sel.value; + inp.className = "s"; + if (sel.required) inp.required = true; + let oc = sel.getAttribute("onchange"); + if (oc) inp.setAttribute("onchange", oc); + sel.parentElement.replaceChild(inp, sel); + return inp; +} +// Add option to select, auto-select matching data-val +function addOption(sel, txt, val) { + if (!sel) return null; + let opt = cE("option"); + opt.value = val; + opt.text = txt; + sel.appendChild(opt); + if (sel.dataset.val !== undefined) { + for (let i = 0; i < sel.options.length; i++) { + if (sel.options[i].value == sel.dataset.val) { sel.selectedIndex = i; break; } + } + } + return opt; +} diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index c10affc545..67abbefae3 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -49,7 +49,7 @@ setABL(); d.Sf.addEventListener("submit", trySubmit); if (d.um_p[0]==-1) d.um_p.shift(); - pinDropdowns(); + fetchPinInfo(pinDropdowns); }); // If we set async false, file is loaded and executed, then next statement is processed if (loc) d.Sf.action = getURL('/settings/leds'); } @@ -70,9 +70,10 @@ function isS2() { return chipID == 2; } function isS3() { return chipID == 3; } function is32() { return chipID == 4; } + function pinsOK() { var ok = true; - var nList = d.Sf.querySelectorAll("#mLC input[name^=L]"); + var nList = Array.from(d.Sf.querySelectorAll("#mLC input[name^=L], #mLC select.pin[name^=L]")); // include both input types (numeric & dropdown) nList.forEach((LC,i)=>{ if (!ok) return; // prevent iteration after conflict let nm = LC.name.substring(0,2); // field name : /L./ @@ -84,19 +85,28 @@ } // ignore IP address if (isNet(t)) return; + // LED pin(s) must be assigned for non-virtual types + let pIdx = parseInt(LC.name.charAt(1)); // determine pin index (0 for L0, 1 for L1, etc.) + if (pIdx < numPins(t) && !isVir(t) && (LC.value === "" || LC.value === "-1")) { + LC.focus(); + ok = false; + return; + } //check for pin conflicts if (LC.value!="" && LC.value!="-1") { let p = d.rsvd.concat(d.um_p); // used pin array - d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay + //d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay + // only non-LED selects (buttons, IR, relay) — LED pins check against each other below + d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(!/^L[0-4]/.test(e.name) && e.value>-1)p.push(parseInt(e.value));}) if (p.some((e)=>e==parseInt(LC.value))) { alert(`Sorry, pins ${JSON.stringify(p)} can't be used.`); - LC.value=""; + if (LC.tagName==="SELECT") LC.value="-1"; else LC.value=""; LC.focus(); ok = false; return; } else if (d.ro_gpio.some((e)=>e==parseInt(LC.value))) { alert(`Sorry, pins ${JSON.stringify(d.ro_gpio)} are input only.`); - LC.value=""; + if (LC.tagName==="SELECT") LC.value="-1"; else LC.value=""; LC.focus(); ok = false; return; @@ -107,10 +117,9 @@ let m = nList[j].name.substring(2,3); // bus number (0-Z) let t2 = parseInt(gN("LT"+m).value, 10); if (isVir(t2)) continue; - if (nList[j].value!="" && nList[i].value==nList[j].value) { + if (nList[j].value!="" && nList[j].value!="-1" && LC.value==nList[j].value) { alert(`Pin conflict between ${LC.name}/${nList[j].name}!`); - nList[j].value=""; - nList[j].focus(); + if (nList[j].tagName==="SELECT") nList[j].value="-1"; else { nList[j].value=""; nList[j].focus(); } ok = false; return; } @@ -303,14 +312,28 @@ gId("p0d"+n).innerText = p0d; gId("p1d"+n).innerText = p1d; gId("off"+n).innerText = off; - // secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off) - let pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 4*isHub75(t); // fixes network pins to 4 + // convert pin fields so show/hide applies to the correct element type + for (let p=0; p<5; p++) { + let nm2 = "L"+p+n; + let el = d.Sf[nm2]; + if (!el) continue; + if (isVir(t) || isHub75(t)) { + if (el.tagName === "SELECT") unmakePinSelect(nm2); // see common.js + } else { + if (el.tagName === "INPUT" && el.type === "number") { + makePinSelect(nm2, 1); // see common.js + d.pinUpdPending = true; + } + } + } + // show/hide secondary pins on whatever element type now exists + let pins = Math.max(gT(t).t.length,1) + 3*isNet(t) + 4*isHub75(t); for (let p=1; p<5; p++) { var LK = d.Sf["L"+p+n]; if (!LK) continue; LK.style.display = (p < pins) ? "inline" : "none"; LK.required = (p < pins); - if (p >= pins) LK.value=""; + if (p >= pins) LK.value = (LK.tagName === "SELECT") ? "-1" : ""; } } @@ -415,25 +438,6 @@ LC.style.color="#fff"; return; // do not check conflicts } - // check for pin conflicts & color fields - if (nm.search(/^L[0-4]/) == 0) // pin fields - if (LC.value!="" && LC.value!="-1") { - let p = d.rsvd.concat(d.um_p); // used pin array - d.Sf.querySelectorAll("select.pin").forEach((e)=>{if(e.value>-1)p.push(parseInt(e.value));}) // buttons, IR & relay - for (j=0; je==parseInt(LC.value))) LC.style.color = "red"; - else LC.style.color = d.ro_gpio.some((e)=>e==parseInt(LC.value)) ? "orange" : "#fff"; - } else LC.style.color = "#fff"; }); // Use helper function to calculate channel usage @@ -510,6 +514,11 @@ gId('fpsNone').style.display = (d.Sf.FR.value == 0) ? 'block':'none'; gId('fpsWarn').style.display = (d.Sf.FR.value == 0) || (d.Sf.FR.value >= 80) ? 'block':'none'; gId('fpsHigh').style.display = (d.Sf.FR.value >= 80) ? 'block':'none'; + + if (d.pinUpdPending) { + d.pinUpdPending = false; + d.Sf.querySelectorAll("select.pin").forEach((e) => { pinUpd(e); }); + } } function lastEnd(i) { @@ -671,11 +680,18 @@ gId("com_rem").style.display = (i>0) ? "inline":"none"; } + // get pin dropdown flags for button type: touch=2, ADC=4, any=0 + function btnPinFlags(t) { return (t==6||t==9) ? 2 : (t==7||t==8) ? 4 : 0; } + function btnPinDd(s) { + let t = parseInt(d.Sf["BE"+s].value); + makePinSelect("BT"+s, btnPinFlags(t)); + d.Sf.querySelectorAll("select.pin").forEach(e => pinUpd(e)); + } function addBtn(i,p,t) { var b = gId("btns"); var s = chrID(i); var c = `
#${i} GPIO: `; - c += ` ` c += ``; c += ``; c += ``; @@ -805,101 +821,70 @@ } } function pinDropdowns() { - let fields = ["IR","RL"]; // IR & relay - gId("btns").querySelectorAll('input[type="number"]').forEach((e)=>{fields.push(e.name);}) // buttons - for (let i of d.Sf.elements) { - if (i.type === "number" && fields.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 = d.um_p.indexOf(j); - let txt = (j === -1) ? "unused" : `${j}`; - if (foundPin >= 0 && j !== v) txt += ` used`; // 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 (d.um_p.includes(j) && j > -1) opt.disabled = true; // someone else's pin - } - } - } - // update select options - d.Sf.querySelectorAll("select.pin").forEach((e)=>{pinUpd(e);}); - // add dataset values for LED GPIO pins - d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i)=>{ - if (i.value!=="" && i.value>=0) + // Rebuild LED GPIO selects now that d.pinsData is available. + d.Sf.querySelectorAll(".iST input.s[name^=L], .iST select.pin[name^=L]").forEach((e) => { + let n = e.name.substring(2, 3); + let t = parseInt(d.Sf["LT"+n].value, 10); + if (!isVir(t) && !isHub75(t)) makePinSelect(e.name, 1); + }); + // IR (any pin including input-only) + let irSel = makePinSelect("IR", 0); + if (irSel) irSel.onchange = function() { UI(); pinUpd(this); }; + // Relay (output required) + let rlSel = makePinSelect("RL", 1); + if (rlSel) rlSel.onchange = function() { UI(); pinUpd(this); }; + // Buttons (flags depend on button type: touch=2, ADC=4) + gId("btns").querySelectorAll('input[type="number"], select.pin[name^=BT]').forEach((e) => { + let s = e.name.substring(2); + let t = parseInt(d.Sf["BE"+s]?.value) || 0; + let bSel = makePinSelect(e.name, btnPinFlags(t)); + if (bSel) bSel.onchange = function() { UI(); pinUpd(this); }; + }); + // cross-update all pin selects + d.Sf.querySelectorAll("select.pin").forEach((e) => { pinUpd(e); }); + // add dataset values for remaining LED GPIO inputs (virtual/HUB75) + d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i) => { + if (i.value !== "" && parseInt(i.value, 10) >= 0) i.dataset.val = i.value; }); } function pinUpd(e) { - // update changed select options across all usermods + // update changed select options across all pin selects let oldV = parseInt(e.dataset.val); e.dataset.val = e.value; - let txt = e.name; + let label = /^L[0-4].$/.test(e.name) ? 'LED' : e.name; let pins = []; - d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i)=>{ - if (i.value!=="" && i.value>=0 && i.max<255) - pins.push(i.value); + // collect LED bus pin values from remaining inputs (virtual/HUB75) + d.Sf.querySelectorAll(".iST input.s[name^=L]").forEach((i) => { + let busN = i.name.substring(2, 3); + let t = parseInt(d.Sf["LT"+busN].value, 10); + let p = parseInt(i.name.charAt(1), 10); + if (isVir(t) || isHub75(t) || p >= numPins(t)) return; + if (i.value !== "" && parseInt(i.value, 10) >= 0) pins.push(i.value); + }); + // collect LED bus pin values from selects + d.Sf.querySelectorAll(".iST select.pin[name^=L]").forEach((s) => { + if (s !== e && parseInt(s.value) >= 0) + pins.push(s.value); }); let selects = d.Sf.querySelectorAll("select.pin"); for (let sel of selects) { - if (sel == e) continue - Array.from(sel.options).forEach((i)=>{ + if (sel == e) continue; + Array.from(sel.options).forEach((i) => { + if (i.value == sel.dataset.val) return; // skip sel's own selected pin let led = pins.includes(i.value); - if (!(i.value==oldV || i.value==e.value || led)) return; - if (i.value == -1) { - i.text = "unused"; - return - } + if (!(i.value == oldV || i.value == e.value || led)) return; + if (i.value == -1) { i.text = "unused"; return; } i.text = i.value; - if (i.value==oldV) { - i.disabled = false; - } - if (i.value==e.value || led) { + if (i.value == oldV) { i.disabled = false; } + if (i.value == e.value || led) { i.disabled = true; - i.text += ` ${led?'LED':txt}`; + i.text += ` ${led ? 'LED' : label}`; } - if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)"; + //if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)"; // read only pin note: removed as pin is not shown for outputs }); } } - // https://stackoverflow.com/questions/39729741/javascript-change-input-text-to-select-option - function addDropdown(field) { - let sel = cE('select'); - sel.classList.add("pin"); - let inp = d.getElementsByName(field)[0]; - 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); - sel.setAttribute("onchange", "pinUpd(this)"); - // 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 = cE("option"); - opt.value = val; - opt.text = txt; - sel.appendChild(opt); - for (let i=0; i{ - d.um_p = []; - d.rsvd = []; - d.ro_gpio = []; - d.max_gpio = 50; - d.touch = []; - }, ()=>{ - // Load extended GPIO info and start pin polling - loadPins(); - pinsTimer = setInterval(loadPins, 250); - }); + fetchPinInfo(()=>{ loadPins(); pinsTimer=setInterval(loadPins,250); }); } function B(){window.open(getURL('/settings'),'_self');} // back button - function getOwnerName(o,t,n) { - // Use firmware-provided name if available - if(n) return n; - if(!o) return "System"; // no owner provided - if(o===0x85){ return getBtnTypeName(t); } // button pin - return "UM #"+o; - } - function getBtnTypeName(t) { - var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"]; - var label = n[t] || "?"; - return 'Button '+label+''; - } - function getCaps(p,c) { - var r=[]; - // Use touch info from settings endpoint - if(d.touch && d.touch.includes(p)) r.push("Touch"); - if(d.ro_gpio && d.ro_gpio.includes(p)) r.push("Input Only"); - // Use other caps from JSON (Analog, Boot, Input Only) - if(c&0x02) r.push("Analog"); - if(c&0x08) r.push("Flash Boot"); - if(c&0x10) r.push("Bootstrap"); - return r.length?r.join(", "):"-"; - } function loadPins() { fetch(getURL('/json/pins'),{method:'get'}) .then(r=>r.json()) diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index 43baaf6718..12eb3e8d07 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -42,12 +42,30 @@ function SetVal(){switch(parseInt(d.Sf.EP.value)){case 5568: d.Sf.DI.value = 5568; break; case 6454: d.Sf.DI.value = 6454; break; case 4048: d.Sf.DI.value = 4048; break; }; SP();FC();} function S(){ getLoc(); - loadJS(getURL('/settings/s.js?p=4'), false, undefined, ()=>{SetVal();}); // If we set async false, file is loaded and executed, then next statement is processed + loadJS(getURL('/settings/s.js?p=4'), false, undefined, ()=>{SetVal(); fetchPinInfo(pinDropdowns);}); // If we set async false, file is loaded and executed, then next statement is processed if (loc) d.Sf.action = getURL('/settings/sync'); } function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; } + const dmxNs = ["IDMR","IDMT","IDME"]; + let dmxOwnPins; // track dmx pins to update dropdown restrictions on pin change + function pinDropdowns() { + dmxNs.forEach((n,i) => { const s = makePinSelect(n, i?1:0); if (s) s.onchange = dmxPinUpd; }); // note on the i?1:0 -> RX pin is allowed input only but TX/EN pins must be GPIO + dmxOwnPins = dmxNs.map(n => parseInt(gN(n)?.value??-1)); + dmxPinUpd(); + } + function dmxPinUpd() { + const vs = dmxNs.map(n => parseInt(gN(n)?.value??-1)); + dmxNs.forEach((n,i) => { + for (const o of gN(n)?.options??[]) { + const p = parseInt(o.value); if (p<0) continue; + // unlock old selected pin, lock newly selected one + o.disabled = (p!==vs[i] && !dmxOwnPins.includes(p) && !!d.pinsData?.find(pi=>pi.p===p)?.a) || vs.some((v,j)=>j!==i&&v===p); + if (!o.disabled) o.text = String(p); // clear owner name i.e. (DMX input) + } + }); + } function hideDMXInput(){gId("dmxInput").style.display="none";} function hideNoDMXInput(){gId("dmxInputOff").style.display="none";} function hideNoDMXOutput(){gId("dmxOnOffOutput").style.display="none";} @@ -126,8 +144,8 @@

Send

Send notifications on button press or IR:
Send Alexa notifications:
Send Philips Hue change notifications:
-UDP packet retransmissions:

-Reboot required to apply changes. +UDP packet retransmissions:
+Reboot required to apply changes.

Instance List

@@ -139,7 +157,7 @@

Realtime

Receive UDP realtime:
Use main segment only:
Respect LED Maps:

-Network DMX input
+

Network DMX input


Type:
Realtime LED offset:
-

Wired DMX Input Pins

- DMX RX: RO
- DMX TX: DI
- DMX Enable: RE+DE
+
+

Wired DMX Input

+ DMX RX Pin: RO
+ DMX TX Pin: DI
+ DMX Enable Pin: RE+DE
DMX Port:
-
+ Reboot required to apply changes. +

This firmware build does not include DMX Input support.
@@ -223,7 +243,7 @@

MQTT

Group Topic:
Publish on button press:
Retain brightness & color messages:
-Reboot required to apply changes. MQTT info +Reboot required to apply changes. MQTT info
diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index bb2113ddbc..0a795088d7 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -7,7 +7,6 @@