Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8dd0d77
Initial plan
Copilot Nov 27, 2025
fbf7af5
Add pin manager feature with serializePins() API and UI
Copilot Nov 27, 2025
a2e5f55
Address code review feedback: improve readability and polling interval
Copilot Nov 27, 2025
66b6aa4
Remove accidentally committed codeql symlink
Copilot Nov 27, 2025
b596fca
Address feedback: show button types, only allocated pins, remove HIGH…
Copilot Nov 27, 2025
1071bab
Fix pin display: show available pins, fix touch button state, add Mul…
Copilot Nov 27, 2025
1e77bc0
Fix touch button state display: add fallback for non-touch-capable pi…
Copilot Nov 27, 2025
113cdf3
Fix touch button state display: add fallback for button-owned pins no…
Copilot Nov 27, 2025
ccf0702
Use usermod names from controller, consolidate JS for smaller flash size
Copilot Nov 27, 2025
54db224
Move Pin Manager UI to separate settings_pins.htm page, remove from i…
Copilot Nov 28, 2025
9dd3759
Use isButtonPressed() from button.cpp and simplify pin capabilities d…
Copilot Nov 28, 2025
bb8deb5
Use existing s.js?p=2 for touch capability, extend appendGPIOinfo()
Copilot Nov 29, 2025
c0f065a
Merge branch 'main' into copilot/improve-pin-management-ux
DedeHai Nov 29, 2025
00aadf2
Fix button handling for new Button struct vector API
Copilot Nov 29, 2025
c281180
Fix ESP8266 GPIO17, add touch raw value (right-shift on S2/S3)
Copilot Nov 30, 2025
88d83e0
major improvements of the AI code, cleanup
DedeHai Nov 30, 2025
106c893
omit ADC2 pins as analog, fixed some pin definitions on ESP8266
DedeHai Dec 1, 2025
2a92f28
rename to pininfo, rename booloader to flash boot
DedeHai Dec 1, 2025
02b6855
Move pin owner name lookup to firmware, pass as string in JSON
Copilot Dec 23, 2025
84ea108
Revert button type lookup to UI, keep only owner lookup in firmware
Copilot Dec 27, 2025
84c9c13
Merge branch 'main' into copilot/improve-pin-management-ux
DedeHai Dec 27, 2025
727bb56
cleanup AI slack, minor improvements to UI, add sequential resource l…
DedeHai Dec 27, 2025
43ffce5
fix rabbit suggestions
DedeHai Dec 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions tools/cdata.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,12 @@ writeChunks(
name: "PAGE_settings_pin",
method: "gzip",
filter: "html-minify"
},
{
file: "settings_pininfo.htm",
name: "PAGE_settings_pininfo",
method: "gzip",
filter: "html-minify"
}
],
"wled00/html_settings.h"
Expand Down
2 changes: 2 additions & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@ 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_LAST SUBPAGE_PINS
#define SUBPAGE_LOCK 251
#define SUBPAGE_PINREQ 252
#define SUBPAGE_CSS 253
Expand Down
1 change: 1 addition & 0 deletions wled00/data/settings.htm
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,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 Info</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>
Expand Down
102 changes: 102 additions & 0 deletions wled00/data/settings_pininfo.htm
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<!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 Info</title>
<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 pinsTimer=null, gpioInfo={};
function S() {
getLoc();
loadJS(getURL('/settings/s.js?p=11'), false, ()=>{
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);
});
if (loc) d.Sf.action = getURL('/settings/pins');
}

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 <span style="font-size:10px;color:#888">'+label+'</span>';
}
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())
.then(j=>{
var cn="",pins=j.pins||[];
if(!pins.length) {
cn="No pins available.";
}else{
cn='<table><tr><th>Pin</th><th>Used by</th><th>Specialities</th></tr>';
for(var p of pins){
var st=""; // button state indicator
var tv=""; // touch value
if(typeof p.s!=='undefined'){
st='<span class="bs" style="background:'+(p.s?'#0B4':'#666')+'"></span> '; // button state dot, gray=off, green=on
if(typeof p.r!=='undefined') tv=' <span class="tv">'+p.r+'</span>'; // add raw touch reading if available
}
var ow=p.a?getOwnerName(p.o, p.t, p.n):(d.um_p && d.um_p.includes(p.p)) ? "Usermod":'<span style="color:#08d">Available</span>';
//if(typeof p.u!=='undefined')ow+=p.u?' (PU)':' (No PU)';
cn+='<tr><td>GPIO'+p.p+'</td><td>'+st+ow+tv+'<td>'+getCaps(p.p,p.c||0)+'</td></tr>';
}
cn+='</table>';
}
gId('pins').innerHTML=cn;
})
.catch(e=>{gId('pins').innerHTML='Error loading pin info';});
}
</script>
<style>
body{text-align:center;background:#222;margin:auto;padding:10px;max-width: 550px}
table{width:100%;border-collapse:collapse;margin:10px 0;font-size:14px;border-radius:6px;overflow:hidden;}
th,td{padding:8px;border:3px solid #444;color:#fff}
th{background:#444}
tr:nth-child(even){background:#222}
tr:nth-child(odd){background:#111}
.bs{display:inline-block;width:14px;height:14px;border-radius:50%} /* button state dot */
.tv{display:inline-block;font-size:10px;color:#888;min-width:6ch;text-align:right;} /* touch value */
</style>
</head>
<body>
<button type="button" onclick="B()">Back</button>
<h2>Pin Info</h2>
<div id="pins">Loading...</div>
<button type="button" onclick="B()">Back</button>
</body>
</html>
1 change: 1 addition & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
170 changes: 169 additions & 1 deletion wled00/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,171 @@ void serializeNodes(JsonObject root)
}
}

// Pin capability flags - only "special" capabilities useful for debugging (note: touch capability is provided by appendGPIOinfo() via d.touch)
#define PIN_CAP_ADC 0x02 // has ADC capability (analog input)
#define PIN_CAP_PWM 0x04 // can be used for PWM (analog LED output) -> unused, all pins can use ledc PWM
#define PIN_CAP_BOOT 0x08 // bootloader pin
#define PIN_CAP_BOOTSTRAP 0x10 // bootstrap pin (strapping pin affecting boot mode)
#define PIN_CAP_INPUT_ONLY 0x20 // input only pin (cannot be used as output)

// Convert PinOwner enum to string for allocated pins
const char* getPinOwnerName(uint8_t gpio) {
PinOwner owner = PinManager::getPinOwner(gpio); // returns "none" if allocated by system, unallocated or unavailable
switch (owner) {
case PinOwner::None: return PinManager::isPinAllocated(gpio) ? "System" : "Unknown";
case PinOwner::Ethernet: return "Ethernet";
case PinOwner::BusDigital: return "LED Digital";
case PinOwner::BusOnOff: return "LED On/Off";
case PinOwner::BusPwm: return "LED PWM";
case PinOwner::Button: return "Button";
case PinOwner::IR: return "IR Receiver";
case PinOwner::Relay: return "Relay";
case PinOwner::SPI_RAM: return "SPI RAM";
case PinOwner::DebugOut: return "Debug";
case PinOwner::DMX: return "DMX Output";
case PinOwner::HW_I2C: return "I2C";
case PinOwner::HW_SPI: return "SPI";
case PinOwner::DMX_INPUT: return "DMX Input";
case PinOwner::HUB75: return "HUB75";
// Usermods - return generic name for now
// TODO: Get actual usermod name from UsermodManager
default:
// Check if it's a usermod (low bit not set)
if (static_cast<uint8_t>(owner) > 0 && !(static_cast<uint8_t>(owner) & 0x80)) {
return "Usermod";
}
return "Unknown";
}
}

void serializePins(JsonObject root)
{
JsonArray pins = root.createNestedArray(F("pins"));
#ifdef ESP8266
constexpr int ENUM_PINS = 17; // GPIO0-16 + A0 (17)
#else
constexpr int ENUM_PINS = WLED_NUM_PINS;
#endif
for (int gpio = 0; gpio < ENUM_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

// Pin capabilities
// Touch capability is provided by appendGPIOinfo() via d.touch
uint8_t caps = 0;

#ifdef ARDUINO_ARCH_ESP32
// Check ADC capability: only ADC1 channels can be used (ADC2 channels are not usable when WiFi is active)
#if CONFIG_IDF_TARGET_ESP32
// ESP32: ADC1 channels 0-7 (GPIO 36, 37, 38, 39, 32, 33, 34, 35)
int adc_channel = digitalPinToAnalogChannel(gpio);
if (adc_channel >= 0 && adc_channel <= 7) caps |= PIN_CAP_ADC;
#elif CONFIG_IDF_TARGET_ESP32S2
// ESP32-S2: ADC1 channels 0-9 (GPIO 1-10)
int adc_channel = digitalPinToAnalogChannel(gpio);
if (adc_channel >= 0 && adc_channel <= 9) caps |= PIN_CAP_ADC;
#elif CONFIG_IDF_TARGET_ESP32S3
// ESP32-S3: ADC1 channels 0-9 (GPIO 1-10)
int adc_channel = digitalPinToAnalogChannel(gpio);
if (adc_channel >= 0 && adc_channel <= 9) caps |= PIN_CAP_ADC;
#elif CONFIG_IDF_TARGET_ESP32C3
// ESP32-C3: ADC1 channels 0-4 (GPIO 0-4)
int adc_channel = digitalPinToAnalogChannel(gpio);
if (adc_channel >= 0 && adc_channel <= 4) caps |= PIN_CAP_ADC;
#endif

// PWM on all ESP32 variants: all output pins can use ledc PWM so this is redundant
//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) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode
if (gpio == 45 || gpio == 46) caps |= PIN_CAP_BOOTSTRAP; // IO46 must be low to enter bootloader mode, IO45 controls flash voltage, keep low for 3.3V flash
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode
if (gpio == 45 || gpio == 46) caps |= PIN_CAP_BOOTSTRAP; // IO46 must be low to enter bootloader mode, IO45 controls flash voltage, keep low for 3.3V flash
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
if (gpio == 9) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode
if (gpio == 2 || gpio == 8) caps |= PIN_CAP_BOOTSTRAP; // both GPIO2 and GPIO8 must be high to enter bootloader mode
#else // ESP32 classic
if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode
if (gpio == 2 || gpio == 12) caps |= PIN_CAP_BOOTSTRAP; // note: if GPIO12 must be low at boot, (high=1.8V flash mode), GPIO 2 must be low or floating to enter bootloader mode
#endif
#else
// ESP8266: GPIO 0-16 + GPIO17=A0
// if (gpio < 16) caps |= PIN_CAP_PWM; // software PWM available on all GPIO except GPIO16
// ESP8266 strapping pins
if (gpio == 0) caps |= PIN_CAP_BOOT;
if (gpio == 2 || gpio == 15) caps |= PIN_CAP_BOOTSTRAP; // GPIO2 must be high, GPIO15 low to boot normally
if (gpio == 17) caps = PIN_CAP_INPUT_ONLY | PIN_CAP_ADC; // TODO: display as A0 pin
#endif

pinObj["c"] = caps; // capabilities

// Add allocated status and owner
pinObj["a"] = isAllocated; // allocated status

// check if this pin is used as a button (need to get button type for owner name)
bool isButton = false;
int buttonIndex = -1;
uint8_t btnType = BTN_TYPE_NONE;
for (size_t b = 0; b < buttons.size(); b++) {
if (buttons[b].pin == gpio && buttons[b].type != BTN_TYPE_NONE) {
isButton = true;
buttonIndex = b;
btnType = buttons[b].type;
break;
}
}

// Add owner ID and name
PinOwner owner = PinManager::getPinOwner(gpio);
if (isAllocated) {
pinObj["o"] = static_cast<uint8_t>(owner); // owner ID (can be used for UI lookup)
pinObj["n"] = getPinOwnerName(gpio); // owner name (string)

// Relay pin
if (owner == PinOwner::Relay) {
pinObj["m"] = 1; // mode: output
pinObj["s"] = digitalRead(rlyPin); // read state from hardware (digitalRead returns output state for output pins)
}
// Button pins, get type and state using isButtonPressed()
else if (isButton && buttonIndex >= 0) {
pinObj["m"] = 0; // mode: input
pinObj["t"] = btnType; // button type
pinObj["s"] = isButtonPressed(buttonIndex) ? 1 : 0; // state

// For touch buttons, get raw reading value (useful for debugging threshold)
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
if (btnType == BTN_TYPE_TOUCH || btnType == BTN_TYPE_TOUCH_SWITCH) {
if (digitalPinToTouchChannel(gpio) >= 0) {
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3
pinObj["r"] = touchRead(gpio) >> 4; // Touch V2 returns larger values, right shift by 4 to match threshold range, see set.cpp
#else
pinObj["r"] = touchRead(gpio);
#endif
}
}
#endif
}
// other allocated output pins that are simple GPIO (BusOnOff, Multi Relay, etc.) TODO: expand for other pin owners as needed
else if (owner == PinOwner::BusOnOff || owner == PinOwner::UM_MultiRelay) {
pinObj["m"] = 1; // mode: output
pinObj["s"] = digitalRead(gpio); // state
}
}
}
}

// deserializes mode data string into JsonArray
void serializeModeData(JsonArray fxdata)
{
Expand Down Expand Up @@ -1111,7 +1276,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;

Expand All @@ -1125,6 +1290,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);
Expand Down Expand Up @@ -1168,6 +1334,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");
Expand Down
4 changes: 3 additions & 1 deletion wled00/wled_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ void serveSettingsJS(AsyncWebServerRequest* request)
return;
}
byte subPage = request->arg(F("p")).toInt();
if (subPage > 10) {
if (subPage > SUBPAGE_LAST) {
request->send_P(501, FPSTR(CONTENT_TYPE_JAVASCRIPT), PSTR("alert('Settings for this request are not implemented.');"));
return;
}
Expand Down Expand Up @@ -725,6 +725,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
Expand Down Expand Up @@ -818,6 +819,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_pininfo; len = PAGE_settings_pininfo_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);
Expand Down
Loading