Compare commits

...

11 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
9dd3759ca0 Use isButtonPressed() from button.cpp and simplify pin capabilities display
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-28 17:29:10 +00:00
copilot-swe-agent[bot]
54db224085 Move Pin Manager UI to separate settings_pins.htm page, remove from info tab
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-28 11:53:16 +00:00
copilot-swe-agent[bot]
ccf0702713 Use usermod names from controller, consolidate JS for smaller flash size
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-27 18:03:08 +00:00
copilot-swe-agent[bot]
113cdf33ea Fix touch button state display: add fallback for button-owned pins not in btnPin array
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-27 17:30:29 +00:00
copilot-swe-agent[bot]
1e77bc0f8d Fix touch button state display: add fallback for non-touch-capable pins and platforms
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-27 16:48:31 +00:00
copilot-swe-agent[bot]
1071bab711 Fix pin display: show available pins, fix touch button state, add Multi Relay support
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-27 16:37:05 +00:00
copilot-swe-agent[bot]
b596fcad69 Address feedback: show button types, only allocated pins, remove HIGH/LOW text, rename Caps to Functions
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-27 16:22:24 +00:00
copilot-swe-agent[bot]
66b6aa4e0a Remove accidentally committed codeql symlink
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-27 15:33:33 +00:00
copilot-swe-agent[bot]
a2e5f5595d Address code review feedback: improve readability and polling interval
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-27 15:33:10 +00:00
copilot-swe-agent[bot]
fbf7af5cb8 Add pin manager feature with serializePins() API and UI
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-11-27 15:27:20 +00:00
copilot-swe-agent[bot]
8dd0d77ca8 Initial plan 2025-11-27 15:15:01 +00:00
7 changed files with 247 additions and 1 deletions

View File

@@ -358,6 +358,12 @@ writeChunks(
name: "PAGE_settings_pin",
method: "gzip",
filter: "html-minify"
},
{
file: "settings_pins.htm",
name: "PAGE_settings_pins",
method: "gzip",
filter: "html-minify"
}
],
"wled00/html_settings.h"

View File

@@ -457,6 +457,7 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
#define SUBPAGE_UM 8
#define SUBPAGE_UPDATE 9
#define SUBPAGE_2D 10
#define SUBPAGE_PINS 11
#define SUBPAGE_LOCK 251
#define SUBPAGE_PINREQ 252
#define SUBPAGE_CSS 253

View File

@@ -40,6 +40,7 @@
<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>
<button type="submit" onclick="window.location=getURL('/settings/pins')">Pin Manager</button>
<button id="2dbtn" type="submit" onclick="window.location=getURL('/settings/2D')">2D Configuration</button>
<button type="submit" onclick="window.location=getURL('/settings/ui')">User Interface</button>
<button id="dmxbtn" style="display:none;" type="submit" onclick="window.location=getURL('/settings/dmx')">DMX Output</button>

View File

@@ -0,0 +1,96 @@
<!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>Pin Manager</title>
<script src="common.js" type="text/javascript"></script>
<script>
var d=document, loc=false, pinsTimer=null;
function gId(s){return d.getElementById(s);}
function gN(s){return d.getElementsByName(s)[0];}
function S(){
getLoc();
loadPins();
pinsTimer = setInterval(loadPins, 250);
}
function B(){window.open(getURL('/settings'),'_self');}
function getOwnerName(o,t,n){
if(!o) return "Unknown";
if(o&0x80){
switch(o){
case 0x81: return "Ethernet";
case 0x82: return "LED Digital";
case 0x83: return "LED On/Off";
case 0x84: return "LED PWM";
case 0x85: return getBtnTypeName(t);
case 0x86: return "IR Receiver";
case 0x87: return "Relay";
case 0x88: return "SPI RAM";
case 0x89: return "Debug";
case 0x8A: return "DMX Output";
case 0x8B: return "I2C";
case 0x8C: return "SPI";
case 0x8D: return "DMX Input";
case 0x8E: return "HUB75";
}
}
return n || ("UM #"+o);
}
function getBtnTypeName(t){
var n=["None","Reserved","Push","Push High","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"];
return "Button ("+(n[t]||"?")+")";
}
function getCaps(c){
var r=[],f=["Touch","Analog","PWM","Boot","In Only"];
for(var i=0;i<5;i++)if(c&(1<<i))r.push(f[i]);
return r.length?r.join(", "):"-";
}
function loadPins(){
fetch(getURL('/json/pins'),{method:'get'})
.then(r=>r.json())
.then(j=>{
var cn="",pins=j.pins||[];
if(!pins.length){
cn="No pins available.";
}else{
cn='<table><tr><th>Pin</th><th>Owner</th><th>Functions</th><th>State</th></tr>';
for(var p of pins){
var st="";
if(typeof p.s!=='undefined'){
st='<span class="ps" style="background:'+(p.s?'#0b0':'#b00')+'"></span>';
}
var ow=p.a?getOwnerName(p.o,p.t,p.n):'<span style="color:#0b0">Available</span>';
if(typeof p.u!=='undefined')ow+=p.u?' (PU)':' (No PU)';
cn+='<tr><td>GPIO '+p.p+'</td><td>'+ow+'</td><td>'+getCaps(p.c||0)+'</td><td>'+st+'</td></tr>';
}
cn+='</table>';
}
gId('pins').innerHTML=cn;
})
.catch(e=>{gId('pins').innerHTML='Error loading pin info';});
}
</script>
<style>
@import url("style.css");
body{text-align:center;background:#222;margin:0;padding:10px}
h2{margin:10px 0;color:#fff;font-family:Verdana,sans-serif}
table{width:100%;border-collapse:collapse;margin:10px 0;font-family:Verdana,sans-serif;font-size:14px}
th,td{padding:8px;border:1px solid #444;color:#fff}
th{background:#333}
tr:nth-child(even){background:#2a2a2a}
tr:nth-child(odd){background:#252525}
.ps{display:inline-block;width:12px;height:12px;border-radius:50%}
button{background:#333;color:#fff;font-family:Verdana,sans-serif;border:1px solid #444;border-radius:20px;padding:8px 16px;margin:5px;cursor:pointer}
button:hover{background:#444}
#pins{min-height:200px}
</style>
</head>
<body onload="S()">
<h2>Pin Manager</h2>
<div id="pins">Loading...</div>
<hr>
<button type="button" onclick="loadPins()">Refresh</button>
<button type="button" onclick="B()">Back</button>
</body>
</html>

View File

@@ -160,6 +160,7 @@ void serializeState(JsonObject root, bool forPreset = false, bool includeBri = t
void serializeInfo(JsonObject root);
void serializeModeNames(JsonArray arr);
void serializeModeData(JsonArray fxdata);
void serializePins(JsonObject root);
void serveJson(AsyncWebServerRequest* request);
#ifdef WLED_ENABLE_JSONLIVE
bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0);

View File

@@ -1045,6 +1045,142 @@ void serializeNodes(JsonObject root)
}
}
// Pin capability flags - only "special" capabilities useful for debugging
#define PIN_CAP_TOUCH 0x01 // has touch capability
#define PIN_CAP_ADC 0x02 // has ADC capability (analog input)
#define PIN_CAP_PWM 0x04 // can be used for PWM (analog LED output)
#define PIN_CAP_BOOT 0x08 // bootloader/strapping pin (affects boot mode)
#define PIN_CAP_INPUT_ONLY 0x10 // input only pin (cannot be used as output)
void serializePins(JsonObject root)
{
JsonArray pins = root.createNestedArray(F("pins"));
for (int gpio = 0; gpio < WLED_NUM_PINS; gpio++) {
bool canInput = PinManager::isPinOk(gpio, false);
bool canOutput = PinManager::isPinOk(gpio, true);
bool isAllocated = PinManager::isPinAllocated(gpio);
// Skip pins that are neither usable nor allocated (truly unusable pins)
if (!canInput && !canOutput && !isAllocated) continue;
JsonObject pinObj = pins.createNestedObject();
pinObj["p"] = gpio; // pin number
// Calculate capabilities - only "special" ones for debugging
uint8_t caps = 0;
#ifdef ARDUINO_ARCH_ESP32
// Check ADC capability
if (digitalPinToAnalogChannel(gpio) >= 0) caps |= PIN_CAP_ADC;
// Check touch capability (not available on C3)
#if !defined(CONFIG_IDF_TARGET_ESP32C3)
if (digitalPinToTouchChannel(gpio) >= 0) caps |= PIN_CAP_TOUCH;
#endif
// PWM - all output-capable GPIO can do PWM on ESP32
if (canOutput) caps |= PIN_CAP_PWM;
// Input-only pins (ESP32 classic: GPIO34-39)
if (canInput && !canOutput) caps |= PIN_CAP_INPUT_ONLY;
// Bootloader/strapping pins
#if defined(CONFIG_IDF_TARGET_ESP32S3)
if (gpio == 0 || gpio == 3 || gpio == 45 || gpio == 46) caps |= PIN_CAP_BOOT;
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
if (gpio == 0 || gpio == 45 || gpio == 46) caps |= PIN_CAP_BOOT;
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
if (gpio == 2 || gpio == 8 || gpio == 9) caps |= PIN_CAP_BOOT;
#else // ESP32 classic
if (gpio == 0 || gpio == 2 || gpio == 12 || gpio == 15) caps |= PIN_CAP_BOOT;
#endif
#else
// ESP8266
if (gpio < 16) caps |= PIN_CAP_PWM; // all GPIO 0-15 support PWM
if (gpio == 17) caps |= PIN_CAP_ADC; // Only A0 (GPIO17) has ADC on ESP8266
// ESP8266 strapping pins
if (gpio == 0 || gpio == 2 || gpio == 15) caps |= PIN_CAP_BOOT;
// GPIO16 is input-only on ESP8266
if (gpio == 16) caps |= PIN_CAP_INPUT_ONLY;
#endif
pinObj["c"] = caps; // capabilities
// Add allocated status and owner
PinOwner owner = PinManager::getPinOwner(gpio);
pinObj["a"] = isAllocated; // allocated status
if (isAllocated) {
uint8_t ownerVal = static_cast<uint8_t>(owner);
pinObj["o"] = ownerVal; // owner ID
// For usermod owners (low bit not set), try to get the usermod name
if (!(ownerVal & 0x80) && ownerVal > 0) {
Usermod* um = UsermodManager::lookup(ownerVal);
if (um) {
// Get usermod name by calling addToConfig and extracting the key
StaticJsonDocument<256> tmpDoc;
JsonObject tmpObj = tmpDoc.to<JsonObject>();
um->addToConfig(tmpObj);
// The first key in the object is the usermod name
JsonObject::iterator it = tmpObj.begin();
if (it != tmpObj.end()) {
pinObj["n"] = it->key().c_str(); // usermod name
}
}
}
}
// For button pins, check if internal pullup/pulldown would be used and get state
bool isButton = false;
int buttonIndex = -1;
for (int b = 0; b < WLED_MAX_BUTTONS; b++) {
if (btnPin[b] >= 0 && btnPin[b] == gpio && buttonType[b] != BTN_TYPE_NONE) {
isButton = true;
buttonIndex = b;
break;
}
}
// For relay pin, get state
if (isAllocated && gpio == rlyPin) {
pinObj["m"] = 1; // mode: output/relay
// Relay state: when LEDs are on (bri > 0), relay is in active mode
// rlyMde: true = active high, false = active low
bool relayActive = bri > 0;
bool relayState = relayActive ? rlyMde : !rlyMde;
pinObj["s"] = relayState ? 1 : 0;
}
// For button pins, get state and type using isButtonPressed() from button.cpp
else if (isAllocated && isButton && buttonIndex >= 0) {
pinObj["m"] = 0; // mode: input/button
pinObj["t"] = buttonType[buttonIndex]; // button type
// Use isButtonPressed() which handles all button types correctly
bool state = isButtonPressed(buttonIndex);
pinObj["s"] = state ? 1 : 0; // state
// Pullup status (when not using touch or analog)
if (buttonType[buttonIndex] != BTN_TYPE_TOUCH &&
buttonType[buttonIndex] != BTN_TYPE_TOUCH_SWITCH &&
buttonType[buttonIndex] != BTN_TYPE_ANALOG &&
buttonType[buttonIndex] != BTN_TYPE_ANALOG_INVERTED) {
pinObj["u"] = disablePullUp ? 0 : 1; // pullup enabled
}
}
// For other allocated output pins that are simple GPIO (BusOnOff, Multi Relay, etc.)
else if (isAllocated && (owner == PinOwner::BusOnOff || owner == PinOwner::UM_MultiRelay)) {
pinObj["m"] = 1; // mode: output
pinObj["s"] = digitalRead(gpio); // state
}
// Fallback for button-owned pins not found in btnPin array (show digitalRead state)
else if (isAllocated && owner == PinOwner::Button) {
pinObj["m"] = 0; // mode: input
pinObj["s"] = digitalRead(gpio) == LOW ? 1 : 0; // state (assume active low)
}
}
}
// deserializes mode data string into JsonArray
void serializeModeData(JsonArray fxdata)
{
@@ -1103,7 +1239,7 @@ class LockedJsonResponse: public AsyncJsonResponse {
void serveJson(AsyncWebServerRequest* request)
{
enum class json_target {
all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config
all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config, pins
};
json_target subJson = json_target::all;
@@ -1117,6 +1253,7 @@ void serveJson(AsyncWebServerRequest* request)
else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata;
else if (url.indexOf(F("net")) > 0) subJson = json_target::networks;
else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config;
else if (url.indexOf(F("pins")) > 0) subJson = json_target::pins;
#ifdef WLED_ENABLE_JSONLIVE
else if (url.indexOf("live") > 0) {
serveLiveLeds(request);
@@ -1160,6 +1297,8 @@ void serveJson(AsyncWebServerRequest* request)
serializeNetworks(lDoc); break;
case json_target::config:
serializeConfig(lDoc); break;
case json_target::pins:
serializePins(lDoc); break;
case json_target::state_info:
case json_target::all:
JsonObject state = lDoc.createNestedObject("state");

View File

@@ -715,6 +715,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
#ifndef WLED_DISABLE_2D
else if (url.indexOf( "2D") > 0) subPage = SUBPAGE_2D;
#endif
else if (url.indexOf(F("pins")) > 0) subPage = SUBPAGE_PINS;
else if (url.indexOf(F("lock")) > 0) subPage = SUBPAGE_LOCK;
}
else if (url.indexOf("/update") >= 0) subPage = SUBPAGE_UPDATE; // update page, for PIN check
@@ -808,6 +809,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
#ifndef WLED_DISABLE_2D
case SUBPAGE_2D : content = PAGE_settings_2D; len = PAGE_settings_2D_length; break;
#endif
case SUBPAGE_PINS : content = PAGE_settings_pins; len = PAGE_settings_pins_length; break;
case SUBPAGE_LOCK : {
correctPIN = !strlen(settingsPIN); // lock if a pin is set
serveMessage(request, 200, strlen(settingsPIN) > 0 ? PSTR("Settings locked") : PSTR("No PIN set"), FPSTR(s_redirecting), 1);