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); + } }