Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fb46770
Add pin manager feature with serializePins() API and UI
Copilot Nov 27, 2025
3be3b13
Address code review feedback: improve readability and polling interval
Copilot Nov 27, 2025
583b508
Remove accidentally committed codeql symlink
Copilot Nov 27, 2025
1609269
Address feedback: show button types, only allocated pins, remove HIGH…
Copilot Nov 27, 2025
adf9ba9
Fix pin display: show available pins, fix touch button state, add Mul…
Copilot Nov 27, 2025
8237c66
Fix touch button state display: add fallback for non-touch-capable pi…
Copilot Nov 27, 2025
a075d6b
Fix touch button state display: add fallback for button-owned pins no…
Copilot Nov 27, 2025
a697e6a
Use usermod names from controller, consolidate JS for smaller flash size
Copilot Nov 27, 2025
2a0aa2c
Move Pin Manager UI to separate settings_pins.htm page, remove from i…
Copilot Nov 28, 2025
b926ab5
Use isButtonPressed() from button.cpp and simplify pin capabilities d…
Copilot Nov 28, 2025
5bd7e03
Use existing s.js?p=2 for touch capability, extend appendGPIOinfo()
Copilot Nov 29, 2025
c887379
Fix button handling for new Button struct vector API
Copilot Nov 29, 2025
31155a1
Fix ESP8266 GPIO17, add touch raw value (right-shift on S2/S3)
Copilot Nov 30, 2025
eaabd5e
major improvements of the AI code, cleanup
DedeHai Nov 30, 2025
1e7c8b9
omit ADC2 pins as analog, fixed some pin definitions on ESP8266
DedeHai Dec 1, 2025
ee983d9
rename to pininfo, rename booloader to flash boot
DedeHai Dec 1, 2025
11f93e5
Move pin owner name lookup to firmware, pass as string in JSON
Copilot Dec 23, 2025
8689b5b
Revert button type lookup to UI, keep only owner lookup in firmware
Copilot Dec 27, 2025
e357fb4
cleanup AI slack, minor improvements to UI, add sequential resource l…
DedeHai Dec 27, 2025
4f97755
fix rabbit suggestions
DedeHai Dec 27, 2025
227d539
rename and bugfix
DedeHai Feb 8, 2026
815b557
add analog button raw value display
DedeHai Feb 8, 2026
8396135
minor fixes
DedeHai Feb 8, 2026
73e9fd6
fix indentation, explicit ESP32 #ifdef, remove A0 for ESP8266 as not …
DedeHai Feb 8, 2026
634d30f
update comment about ESP8266 A0 deficiency
DedeHai Feb 8, 2026
dfd540c
moved pin capabilities to pinmanager
DedeHai Feb 16, 2026
9cad9d3
Merge branch 'main' into pinInfo
DedeHai Feb 20, 2026
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 @@ -380,6 +380,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 @@ -498,6 +498,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 & Network</button>
<button type="submit" onclick="window.location=getURL('/settings/leds')">LED & Hardware</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
101 changes: 101 additions & 0 deletions wled00/data/settings_pininfo.htm
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<!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);
});
}

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>Pin Notes</th></tr>';
for(var p of pins){
var st=""; // button state indicator
var rv=""; // raw value (touch / analog)
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') rv=' <span class="rv">'+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+rv+'</td><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 */
.rv{display:inline-block;font-size:10px;color:#888;min-width:6ch;text-align:right;} /* raw value (touch / analog) */
</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 @@ -174,6 +174,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
119 changes: 118 additions & 1 deletion wled00/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,120 @@ void serializeNodes(JsonObject root)
}
}

void serializePins(JsonObject root)
{
JsonArray pins = root.createNestedArray(F("pins"));
#ifdef ESP8266
constexpr int ENUM_PINS = WLED_NUM_PINS; // GPIO0-16 (A0 (17) is analog input only and always assigned to any analog input, even if set "unused") TODO: can currently not be handled
#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
if (PinManager::isAnalogPin(gpio)) caps |= PIN_CAP_ADC;

// 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
#elif defined(CONFIG_IDF_TARGET_ESP32) // 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)
int buttonIndex = PinManager::getButtonIndex(gpio); // returns -1 if not a button pin, otherwise returns index in buttons array

// 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"] = PinManager::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 (buttonIndex >= 0) {
pinObj["m"] = 0; // mode: input
pinObj["t"] = buttons[buttonIndex].type; // 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 (buttons[buttonIndex].type == BTN_TYPE_TOUCH || buttons[buttonIndex].type == 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); // send raw value
#endif
}
}
#endif
// for analog buttons, get raw reading value
if (buttons[buttonIndex].type == BTN_TYPE_ANALOG || buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) {
int analogRaw = 0;
#ifdef ESP8266
analogRaw = analogRead(A0) >> 2; // convert 10bit read to 8bit, ESP8266 only has one analog pin
#else
if (digitalPinToAnalogChannel(gpio) >= 0) {
analogRaw = (analogRead(gpio)>>4); // right shift to match button value (8bit) see button.cpp
}
#endif
if (buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) analogRaw = 255 - analogRaw;
pinObj["r"] = analogRaw; // send raw value
}
}
// 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); // read state from hardware (digitalRead returns output state for output pins)
}
}
}
}

// deserializes mode data string into JsonArray
void serializeModeData(JsonArray fxdata)
{
Expand Down Expand Up @@ -1107,7 +1221,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 @@ -1121,6 +1235,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 @@ -1164,6 +1279,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
63 changes: 63 additions & 0 deletions wled00/pin_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,66 @@ void PinManager::deallocateLedc(byte pos, byte channels)
}
}
#endif

// Convert PinOwner enum to string for allocated pins
const char* PinManager::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 (high bit not set)
if (static_cast<uint8_t>(owner) > 0 && !(static_cast<uint8_t>(owner) & 0x80)) {
return "Usermod";
}
return "Unknown";
}
}

int PinManager::getButtonIndex(byte gpio) {
for (size_t b = 0; b < buttons.size(); b++) {
if (buttons[b].pin == gpio && buttons[b].type != BTN_TYPE_NONE) {
return b;
}
}
return -1;
}

bool PinManager::isAnalogPin(byte gpio) {
#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) return true;
#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) return true;
#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) return true;
#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) return true;
#endif
#endif
return false; // not an analog pin if it doesn't have ADC capability, ESP8266 has only one ADC pin (A0) which is handled separately in button.cpp, so return false for all pins here
}
10 changes: 10 additions & 0 deletions wled00/pin_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
#define WLED_NUM_PINS (GPIO_PIN_COUNT)
#endif

// 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)

typedef struct PinManagerPinType {
int8_t pin;
bool isOutput;
Expand Down Expand Up @@ -100,8 +107,11 @@ namespace PinManager {
bool isPinOk(byte gpio, bool output = true);

bool isReadOnlyPin(byte gpio);
int getButtonIndex(byte gpio); // returns button index if pin is used for button, otherwise -1
bool isAnalogPin(byte gpio); // returns true if pin has ADC capability, otherwise false

PinOwner getPinOwner(byte gpio);
const char* getPinOwnerName(uint8_t gpio);

#ifdef ARDUINO_ARCH_ESP32
byte allocateLedc(byte channels);
Expand Down
Loading