Skip to content

Commit 43e86d0

Browse files
DedeHaiCopilot
andauthored
Add Pin Info: overview of used and available Pins (#5361)
* Add pin manager feature with serializePins() API and UI * update comment about ESP8266 A0 deficiency * moved pin capabilities to pinmanager Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
1 parent 29e9c73 commit 43e86d0

File tree

10 files changed

+327
-3
lines changed

10 files changed

+327
-3
lines changed

tools/cdata.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,12 @@ writeChunks(
394394
name: "PAGE_settings_pin",
395395
method: "gzip",
396396
filter: "html-minify"
397+
},
398+
{
399+
file: "settings_pininfo.htm",
400+
name: "PAGE_settings_pininfo",
401+
method: "gzip",
402+
filter: "html-minify"
397403
}
398404
],
399405
"wled00/html_settings.h"

wled00/const.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
498498
#define SUBPAGE_UM 8
499499
#define SUBPAGE_UPDATE 9
500500
#define SUBPAGE_2D 10
501+
#define SUBPAGE_PINS 11
502+
#define SUBPAGE_LAST SUBPAGE_PINS
501503
#define SUBPAGE_LOCK 251
502504
#define SUBPAGE_PINREQ 252
503505
#define SUBPAGE_CSS 253

wled00/data/settings.htm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<button type=submit id="b" onclick="window.location=getURL('/')">Back</button>
4343
<button type="submit" onclick="window.location=getURL('/settings/wifi')">WiFi & Network</button>
4444
<button type="submit" onclick="window.location=getURL('/settings/leds')">LED & Hardware</button>
45+
<button type="submit" onclick="window.location=getURL('/settings/pins')">Pin Info</button>
4546
<button id="2dbtn" type="submit" onclick="window.location=getURL('/settings/2D')">2D Configuration</button>
4647
<button type="submit" onclick="window.location=getURL('/settings/ui')">User Interface</button>
4748
<button id="dmxbtn" style="display:none;" type="submit" onclick="window.location=getURL('/settings/dmx')">DMX Output</button>

wled00/data/settings_pininfo.htm

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
6+
<title>Pin Info</title>
7+
<script>
8+
// load common.js with retry on error
9+
(function loadFiles() {
10+
const l = document.createElement('script');
11+
l.src = 'common.js';
12+
l.onload = () => loadResources(['style.css'], S); // load style.css then call S()
13+
l.onerror = () => setTimeout(loadFiles, 100);
14+
document.head.appendChild(l);
15+
})();
16+
var pinsTimer=null, gpioInfo={};
17+
function S() {
18+
getLoc();
19+
loadJS(getURL('/settings/s.js?p=11'), false, ()=>{
20+
d.um_p = [];
21+
d.rsvd = [];
22+
d.ro_gpio = [];
23+
d.max_gpio = 50;
24+
d.touch = [];
25+
}, ()=>{
26+
// Load extended GPIO info and start pin polling
27+
loadPins();
28+
pinsTimer = setInterval(loadPins, 250);
29+
});
30+
}
31+
32+
function B(){window.open(getURL('/settings'),'_self');} // back button
33+
34+
function getOwnerName(o,t,n) {
35+
// Use firmware-provided name if available
36+
if(n) return n;
37+
if(!o) return "System"; // no owner provided
38+
if(o===0x85){ return getBtnTypeName(t); } // button pin
39+
return "UM #"+o;
40+
}
41+
function getBtnTypeName(t) {
42+
var n=["None","Reserved","Push","Push Inv","Switch","PIR","Touch","Analog","Analog Inv","Touch Switch"];
43+
var label = n[t] || "?";
44+
return 'Button <span style="font-size:10px;color:#888">'+label+'</span>';
45+
}
46+
function getCaps(p,c) {
47+
var r=[];
48+
// Use touch info from settings endpoint
49+
if(d.touch && d.touch.includes(p)) r.push("Touch");
50+
if(d.ro_gpio && d.ro_gpio.includes(p)) r.push("Input Only");
51+
// Use other caps from JSON (Analog, Boot, Input Only)
52+
if(c&0x02) r.push("Analog");
53+
if(c&0x08) r.push("Flash Boot");
54+
if(c&0x10) r.push("Bootstrap");
55+
return r.length?r.join(", "):"-";
56+
}
57+
function loadPins() {
58+
fetch(getURL('/json/pins'),{method:'get'})
59+
.then(r=>r.json())
60+
.then(j=>{
61+
var cn="",pins=j.pins||[];
62+
if(!pins.length) {
63+
cn="No pins available.";
64+
}else{
65+
cn='<table><tr><th>Pin</th><th>Used by</th><th>Pin Notes</th></tr>';
66+
for(var p of pins){
67+
var st=""; // button state indicator
68+
var rv=""; // raw value (touch / analog)
69+
if(typeof p.s!=='undefined'){
70+
st='<span class="bs" style="background:'+(p.s?'#0B4':'#666')+'"></span> '; // button state dot, gray=off, green=on
71+
if(typeof p.r!=='undefined') rv=' <span class="rv">'+p.r+'</span>'; // add raw touch reading if available
72+
}
73+
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>';
74+
//if(typeof p.u!=='undefined')ow+=p.u?' (PU)':' (No PU)';
75+
cn+='<tr><td>GPIO'+p.p+'</td><td>'+st+ow+rv+'</td><td>'+getCaps(p.p,p.c||0)+'</td></tr>';
76+
}
77+
cn+='</table>';
78+
}
79+
gId('pins').innerHTML=cn;
80+
})
81+
.catch(e=>{gId('pins').innerHTML='Error loading pin info';});
82+
}
83+
</script>
84+
<style>
85+
body{text-align:center;background:#222;margin:auto;padding:10px;max-width: 550px}
86+
table{width:100%;border-collapse:collapse;margin:10px 0;font-size:14px;border-radius:6px;overflow:hidden;}
87+
th,td{padding:8px;border:3px solid #444;color:#fff}
88+
th{background:#444}
89+
tr:nth-child(even){background:#222}
90+
tr:nth-child(odd){background:#111}
91+
.bs{display:inline-block;width:14px;height:14px;border-radius:50%} /* button state dot */
92+
.rv{display:inline-block;font-size:10px;color:#888;min-width:6ch;text-align:right;} /* raw value (touch / analog) */
93+
</style>
94+
</head>
95+
<body>
96+
<button type="button" onclick="B()">Back</button>
97+
<h2>Pin Info</h2>
98+
<div id="pins">Loading...</div>
99+
<button type="button" onclick="B()">Back</button>
100+
</body>
101+
</html>

wled00/fcn_declare.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ void serializeState(JsonObject root, bool forPreset = false, bool includeBri = t
174174
void serializeInfo(JsonObject root);
175175
void serializeModeNames(JsonArray arr);
176176
void serializeModeData(JsonArray fxdata);
177+
void serializePins(JsonObject root);
177178
void serveJson(AsyncWebServerRequest* request);
178179
#ifdef WLED_ENABLE_JSONLIVE
179180
bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0);

wled00/json.cpp

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,120 @@ void serializeNodes(JsonObject root)
10491049
}
10501050
}
10511051

1052+
void serializePins(JsonObject root)
1053+
{
1054+
JsonArray pins = root.createNestedArray(F("pins"));
1055+
#ifdef ESP8266
1056+
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
1057+
#else
1058+
constexpr int ENUM_PINS = WLED_NUM_PINS;
1059+
#endif
1060+
for (int gpio = 0; gpio < ENUM_PINS; gpio++) {
1061+
bool canInput = PinManager::isPinOk(gpio, false);
1062+
bool canOutput = PinManager::isPinOk(gpio, true);
1063+
bool isAllocated = PinManager::isPinAllocated(gpio);
1064+
// Skip pins that are neither usable nor allocated (truly unusable pins)
1065+
if (!canInput && !canOutput && !isAllocated) continue;
1066+
1067+
JsonObject pinObj = pins.createNestedObject();
1068+
pinObj["p"] = gpio; // pin number
1069+
1070+
// Pin capabilities
1071+
// Touch capability is provided by appendGPIOinfo() via d.touch
1072+
uint8_t caps = 0;
1073+
1074+
#ifdef ARDUINO_ARCH_ESP32
1075+
if (PinManager::isAnalogPin(gpio)) caps |= PIN_CAP_ADC;
1076+
1077+
// PWM on all ESP32 variants: all output pins can use ledc PWM so this is redundant
1078+
//if (canOutput) caps |= PIN_CAP_PWM;
1079+
1080+
// Input-only pins (ESP32 classic: GPIO34-39)
1081+
if (canInput && !canOutput) caps |= PIN_CAP_INPUT_ONLY;
1082+
1083+
// Bootloader/strapping pins
1084+
#if defined(CONFIG_IDF_TARGET_ESP32S3)
1085+
if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode
1086+
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
1087+
#elif defined(CONFIG_IDF_TARGET_ESP32S2)
1088+
if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode
1089+
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
1090+
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
1091+
if (gpio == 9) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode
1092+
if (gpio == 2 || gpio == 8) caps |= PIN_CAP_BOOTSTRAP; // both GPIO2 and GPIO8 must be high to enter bootloader mode
1093+
#elif defined(CONFIG_IDF_TARGET_ESP32) // ESP32 classic
1094+
if (gpio == 0) caps |= PIN_CAP_BOOT; // pull low to enter bootloader mode
1095+
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
1096+
#endif
1097+
#else
1098+
// ESP8266: GPIO 0-16 + GPIO17=A0
1099+
// if (gpio < 16) caps |= PIN_CAP_PWM; // software PWM available on all GPIO except GPIO16
1100+
// ESP8266 strapping pins
1101+
if (gpio == 0) caps |= PIN_CAP_BOOT;
1102+
if (gpio == 2 || gpio == 15) caps |= PIN_CAP_BOOTSTRAP; // GPIO2 must be high, GPIO15 low to boot normally
1103+
if (gpio == 17) caps = PIN_CAP_INPUT_ONLY | PIN_CAP_ADC; // TODO: display as A0 pin
1104+
#endif
1105+
1106+
pinObj["c"] = caps; // capabilities
1107+
1108+
// Add allocated status and owner
1109+
pinObj["a"] = isAllocated; // allocated status
1110+
1111+
// check if this pin is used as a button (need to get button type for owner name)
1112+
int buttonIndex = PinManager::getButtonIndex(gpio); // returns -1 if not a button pin, otherwise returns index in buttons array
1113+
1114+
// Add owner ID and name
1115+
PinOwner owner = PinManager::getPinOwner(gpio);
1116+
if (isAllocated) {
1117+
pinObj["o"] = static_cast<uint8_t>(owner); // owner ID (can be used for UI lookup)
1118+
pinObj["n"] = PinManager::getPinOwnerName(gpio); // owner name (string)
1119+
1120+
// Relay pin
1121+
if (owner == PinOwner::Relay) {
1122+
pinObj["m"] = 1; // mode: output
1123+
pinObj["s"] = digitalRead(rlyPin); // read state from hardware (digitalRead returns output state for output pins)
1124+
}
1125+
// Button pins, get type and state using isButtonPressed()
1126+
else if (buttonIndex >= 0) {
1127+
pinObj["m"] = 0; // mode: input
1128+
pinObj["t"] = buttons[buttonIndex].type; // button type
1129+
pinObj["s"] = isButtonPressed(buttonIndex) ? 1 : 0; // state
1130+
1131+
// for touch buttons, get raw reading value (useful for debugging threshold)
1132+
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
1133+
if (buttons[buttonIndex].type == BTN_TYPE_TOUCH || buttons[buttonIndex].type == BTN_TYPE_TOUCH_SWITCH) {
1134+
if (digitalPinToTouchChannel(gpio) >= 0) {
1135+
#ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3
1136+
pinObj["r"] = touchRead(gpio) >> 4; // Touch V2 returns larger values, right shift by 4 to match threshold range, see set.cpp
1137+
#else
1138+
pinObj["r"] = touchRead(gpio); // send raw value
1139+
#endif
1140+
}
1141+
}
1142+
#endif
1143+
// for analog buttons, get raw reading value
1144+
if (buttons[buttonIndex].type == BTN_TYPE_ANALOG || buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) {
1145+
int analogRaw = 0;
1146+
#ifdef ESP8266
1147+
analogRaw = analogRead(A0) >> 2; // convert 10bit read to 8bit, ESP8266 only has one analog pin
1148+
#else
1149+
if (digitalPinToAnalogChannel(gpio) >= 0) {
1150+
analogRaw = (analogRead(gpio)>>4); // right shift to match button value (8bit) see button.cpp
1151+
}
1152+
#endif
1153+
if (buttons[buttonIndex].type == BTN_TYPE_ANALOG_INVERTED) analogRaw = 255 - analogRaw;
1154+
pinObj["r"] = analogRaw; // send raw value
1155+
}
1156+
}
1157+
// other allocated output pins that are simple GPIO (BusOnOff, Multi Relay, etc.) TODO: expand for other pin owners as needed
1158+
else if (owner == PinOwner::BusOnOff || owner == PinOwner::UM_MultiRelay) {
1159+
pinObj["m"] = 1; // mode: output
1160+
pinObj["s"] = digitalRead(gpio); // read state from hardware (digitalRead returns output state for output pins)
1161+
}
1162+
}
1163+
}
1164+
}
1165+
10521166
// deserializes mode data string into JsonArray
10531167
void serializeModeData(JsonArray fxdata)
10541168
{
@@ -1107,7 +1221,7 @@ class LockedJsonResponse: public AsyncJsonResponse {
11071221
void serveJson(AsyncWebServerRequest* request)
11081222
{
11091223
enum class json_target {
1110-
all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config
1224+
all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config, pins
11111225
};
11121226
json_target subJson = json_target::all;
11131227

@@ -1121,6 +1235,7 @@ void serveJson(AsyncWebServerRequest* request)
11211235
else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata;
11221236
else if (url.indexOf(F("net")) > 0) subJson = json_target::networks;
11231237
else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config;
1238+
else if (url.indexOf(F("pins")) > 0) subJson = json_target::pins;
11241239
#ifdef WLED_ENABLE_JSONLIVE
11251240
else if (url.indexOf("live") > 0) {
11261241
serveLiveLeds(request);
@@ -1164,6 +1279,8 @@ void serveJson(AsyncWebServerRequest* request)
11641279
serializeNetworks(lDoc); break;
11651280
case json_target::config:
11661281
serializeConfig(lDoc); break;
1282+
case json_target::pins:
1283+
serializePins(lDoc); break;
11671284
case json_target::state_info:
11681285
case json_target::all:
11691286
JsonObject state = lDoc.createNestedObject("state");

wled00/pin_manager.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,66 @@ void PinManager::deallocateLedc(byte pos, byte channels)
315315
}
316316
}
317317
#endif
318+
319+
// Convert PinOwner enum to string for allocated pins
320+
const char* PinManager::getPinOwnerName(uint8_t gpio) {
321+
PinOwner owner = PinManager::getPinOwner(gpio); // returns "none" if allocated by system, unallocated or unavailable
322+
switch (owner) {
323+
case PinOwner::None: return PinManager::isPinAllocated(gpio) ? "System" : "Unknown";
324+
case PinOwner::Ethernet: return "Ethernet";
325+
case PinOwner::BusDigital: return "LED Digital";
326+
case PinOwner::BusOnOff: return "LED On/Off";
327+
case PinOwner::BusPwm: return "LED PWM";
328+
case PinOwner::Button: return "Button";
329+
case PinOwner::IR: return "IR Receiver";
330+
case PinOwner::Relay: return "Relay";
331+
case PinOwner::SPI_RAM: return "SPI RAM";
332+
case PinOwner::DebugOut: return "Debug";
333+
case PinOwner::DMX: return "DMX Output";
334+
case PinOwner::HW_I2C: return "I2C";
335+
case PinOwner::HW_SPI: return "SPI";
336+
case PinOwner::DMX_INPUT: return "DMX Input";
337+
case PinOwner::HUB75: return "HUB75";
338+
// Usermods - return generic name for now
339+
// TODO: Get actual usermod name from UsermodManager
340+
default:
341+
// Check if it's a usermod (high bit not set)
342+
if (static_cast<uint8_t>(owner) > 0 && !(static_cast<uint8_t>(owner) & 0x80)) {
343+
return "Usermod";
344+
}
345+
return "Unknown";
346+
}
347+
}
348+
349+
int PinManager::getButtonIndex(byte gpio) {
350+
for (size_t b = 0; b < buttons.size(); b++) {
351+
if (buttons[b].pin == gpio && buttons[b].type != BTN_TYPE_NONE) {
352+
return b;
353+
}
354+
}
355+
return -1;
356+
}
357+
358+
bool PinManager::isAnalogPin(byte gpio) {
359+
#ifdef ARDUINO_ARCH_ESP32
360+
// Check ADC capability: only ADC1 channels can be used (ADC2 channels are not usable when WiFi is active)
361+
#if CONFIG_IDF_TARGET_ESP32
362+
// ESP32: ADC1 channels 0-7 (GPIO 36, 37, 38, 39, 32, 33, 34, 35)
363+
int adc_channel = digitalPinToAnalogChannel(gpio);
364+
if (adc_channel >= 0 && adc_channel <= 7) return true;
365+
#elif CONFIG_IDF_TARGET_ESP32S2
366+
// ESP32-S2: ADC1 channels 0-9 (GPIO 1-10)
367+
int adc_channel = digitalPinToAnalogChannel(gpio);
368+
if (adc_channel >= 0 && adc_channel <= 9) return true;
369+
#elif CONFIG_IDF_TARGET_ESP32S3
370+
// ESP32-S3: ADC1 channels 0-9 (GPIO 1-10)
371+
int adc_channel = digitalPinToAnalogChannel(gpio);
372+
if (adc_channel >= 0 && adc_channel <= 9) return true;
373+
#elif CONFIG_IDF_TARGET_ESP32C3
374+
// ESP32-C3: ADC1 channels 0-4 (GPIO 0-4)
375+
int adc_channel = digitalPinToAnalogChannel(gpio);
376+
if (adc_channel >= 0 && adc_channel <= 4) return true;
377+
#endif
378+
#endif
379+
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
380+
}

wled00/pin_manager.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
#define WLED_NUM_PINS (GPIO_PIN_COUNT)
1111
#endif
1212

13+
// Pin capability flags - only "special" capabilities useful for debugging (note: touch capability is provided by appendGPIOinfo() via d.touch)
14+
#define PIN_CAP_ADC 0x02 // has ADC capability (analog input)
15+
#define PIN_CAP_PWM 0x04 // can be used for PWM (analog LED output) -> unused, all pins can use ledc PWM
16+
#define PIN_CAP_BOOT 0x08 // bootloader pin
17+
#define PIN_CAP_BOOTSTRAP 0x10 // bootstrap pin (strapping pin affecting boot mode)
18+
#define PIN_CAP_INPUT_ONLY 0x20 // input only pin (cannot be used as output)
19+
1320
typedef struct PinManagerPinType {
1421
int8_t pin;
1522
bool isOutput;
@@ -100,8 +107,11 @@ namespace PinManager {
100107
bool isPinOk(byte gpio, bool output = true);
101108

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

104113
PinOwner getPinOwner(byte gpio);
114+
const char* getPinOwnerName(uint8_t gpio);
105115

106116
#ifdef ARDUINO_ARCH_ESP32
107117
byte allocateLedc(byte channels);

0 commit comments

Comments
 (0)