diff --git a/tools/cdata.js b/tools/cdata.js
index 3b2f3fafc4..df49e0d28b 100644
--- a/tools/cdata.js
+++ b/tools/cdata.js
@@ -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"
diff --git a/wled00/const.h b/wled00/const.h
index e6abd2b5db..95e69d855b 100644
--- a/wled00/const.h
+++ b/wled00/const.h
@@ -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
diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm
index 8e704b9727..ce3c246bad 100644
--- a/wled00/data/settings.htm
+++ b/wled00/data/settings.htm
@@ -42,6 +42,7 @@
+
diff --git a/wled00/data/settings_pininfo.htm b/wled00/data/settings_pininfo.htm
new file mode 100644
index 0000000000..7fe4e889bb
--- /dev/null
+++ b/wled00/data/settings_pininfo.htm
@@ -0,0 +1,101 @@
+
+
+
+
+
+ Pin Info
+
+
+
+
+
+ Pin Info
+ Loading...
+
+
+
diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h
index 84b6da9e61..1c0b5a7a0d 100644
--- a/wled00/fcn_declare.h
+++ b/wled00/fcn_declare.h
@@ -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);
diff --git a/wled00/json.cpp b/wled00/json.cpp
index 9ad0cfa848..d7521de42c 100644
--- a/wled00/json.cpp
+++ b/wled00/json.cpp
@@ -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(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)
{
@@ -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;
@@ -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);
@@ -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");
diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp
index 06b8126d56..84eaaec1fc 100644
--- a/wled00/pin_manager.cpp
+++ b/wled00/pin_manager.cpp
@@ -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(owner) > 0 && !(static_cast(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
+}
diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h
index a488d24f70..5f774bb47d 100644
--- a/wled00/pin_manager.h
+++ b/wled00/pin_manager.h
@@ -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;
@@ -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);
diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp
index bcf7487fcf..560bd106d9 100644
--- a/wled00/wled_server.cpp
+++ b/wled00/wled_server.cpp
@@ -694,7 +694,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;
}
@@ -734,6 +734,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
@@ -827,6 +828,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);
diff --git a/wled00/xml.cpp b/wled00/xml.cpp
index 6200dab4d1..dceebbdf09 100644
--- a/wled00/xml.cpp
+++ b/wled00/xml.cpp
@@ -169,6 +169,22 @@ static void appendGPIOinfo(Print& settingsScript)
// add info about max. # of pins
settingsScript.printf_P(PSTR("d.max_gpio=%d;"),WLED_NUM_PINS);
+
+ // add info about touch-capable GPIO (ESP32 only, not on C3)
+ #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3)
+ settingsScript.print(F("d.touch=["));
+ firstPin = true;
+ for (unsigned i = 0; i < WLED_NUM_PINS; i++) {
+ if (digitalPinToTouchChannel(i) >= 0) {
+ if (!firstPin) settingsScript.print(',');
+ settingsScript.print(i);
+ firstPin = false;
+ }
+ }
+ settingsScript.print(F("];"));
+ #else
+ settingsScript.print(F("d.touch=[];"));
+ #endif
}
//get values for settings form in javascript
@@ -177,7 +193,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
//0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec
DEBUG_PRINTF_P(PSTR("settings resp %u\n"), (unsigned)subPage);
- if (subPage <0 || subPage >10) return;
+ if (subPage <0 || subPage >SUBPAGE_LAST) return;
char nS[32];
if (subPage == SUBPAGE_MENU)
@@ -723,4 +739,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
settingsScript.print(F("gId(\"somp\").remove(1);")); // remove 2D option from dropdown
#endif
}
+
+ if (subPage == SUBPAGE_PINS) // pins info
+ {
+ appendGPIOinfo(settingsScript);
+ }
}