From 85a4b2cd913275663e2d68fadff5d3a0d8b7790d Mon Sep 17 00:00:00 2001 From: Greg Hanefeld Date: Sat, 15 Aug 2026 16:04:04 -0700 Subject: [PATCH 1/3] fix: keep boot buttons out of rescue admission --- test/boot_button_rescue_test.py | 14 +++++++++ wled00/wled.cpp | 53 +++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 test/boot_button_rescue_test.py diff --git a/test/boot_button_rescue_test.py b/test/boot_button_rescue_test.py new file mode 100644 index 0000000000..0e39cd1b47 --- /dev/null +++ b/test/boot_button_rescue_test.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +"""Source contracts for boot rescue admission and button diagnostics.""" +from pathlib import Path + +source = (Path(__file__).parents[1] / "wled00" / "wled.cpp").read_text() +assert "checkRescuePin" not in source +assert "checkRescueSerial" in source +assert "WLED_DISABLE_STUCK_BUTTON_DIAGNOSTICS" in source +assert "diagnoseBootButtons();" in source +assert "buttons.size()" in source +assert "STUCK_BUTTON" in source +assert "WLED button diagnostics: healthy" in source +assert "WLED button diagnostics: disabled" in source +print("boot rescue admission and stuck-button diagnostics: PASS") diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 00b8f9ab86..d89b57d31e 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -37,21 +37,6 @@ WLED::WLED() { } -static bool checkRescuePin() -{ -#if WLED_RESCUE_WINDOW_MS > 0 - constexpr int8_t rescuePins[] = {BTNPIN}; - constexpr size_t rescuePinCount = sizeof(rescuePins) / sizeof(rescuePins[0]); - if (rescuePinCount == 0 || rescuePins[0] < 0) return false; - - pinMode(rescuePins[0], INPUT_PULLUP); - delay(2); - return digitalRead(rescuePins[0]) == LOW; -#else - return false; -#endif -} - static bool checkRescueSerial() { #if WLED_RESCUE_WINDOW_MS > 0 @@ -80,12 +65,6 @@ static bool checkRescueSerial() static void detectRescueMode() { #if WLED_RESCUE_WINDOW_MS > 0 - if (checkRescuePin()) { - wledRescueMode = true; - Serial.println(F("WLED rescue mode: startup button held")); - return; - } - Serial.print(F("WLED rescue window: send '")); Serial.print(F(WLED_RESCUE_SERIAL_TOKEN)); Serial.println(F("' to skip config, usermods, WiFi, and ESP-NOW")); @@ -96,6 +75,35 @@ static void detectRescueMode() #endif } +// AI: below section was generated by an AI +// Report configured digital inputs that remain active after configuration. +static void diagnoseBootButtons() +{ +#ifdef WLED_DISABLE_STUCK_BUTTON_DIAGNOSTICS + Serial.println(F("WLED button diagnostics: disabled (WLED_DISABLE_STUCK_BUTTON_DIAGNOSTICS)")); + return; +#else + bool stuck = false; + for (size_t b = 0; b < buttons.size(); b++) { + const auto &button = buttons[b]; + if (button.pin < 0 || button.type == BTN_TYPE_NONE || button.type == BTN_TYPE_ANALOG || + button.type == BTN_TYPE_ANALOG_INVERTED || button.type == BTN_TYPE_TOUCH || + button.type == BTN_TYPE_TOUCH_SWITCH || button.type == BTN_TYPE_SWITCH || + button.type == BTN_TYPE_PIR_SENSOR) continue; + const bool active = (button.type == BTN_TYPE_PUSH_ACT_HIGH) ? + (digitalRead(button.pin) == HIGH) : (digitalRead(button.pin) == LOW); + if (active) { + stuck = true; + Serial.printf("WLED button diagnostic: active at boot index=%u gpio=%d type=%u\\n", + (unsigned)b, button.pin, (unsigned)button.type); + } + } + Serial.println(stuck ? F("WLED button diagnostics: STUCK_BUTTON") : + F("WLED button diagnostics: healthy")); +#endif +} +// AI: end + static void handleRescueMode() { static bool announced = false; @@ -615,7 +623,8 @@ void WLED::setup() briLast = 0; transitionDelay = 0; } - DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); + if (!wledRescueMode) diagnoseBootButtons(); + DEBUG_PRINTF_P(PSTR("heap %u\\n"), getFreeHeapSize()); #if defined(STATUSLED) && STATUSLED>=0 if (!PinManager::isPinAllocated(STATUSLED)) { From a300eb38bfce43f75560d3ae52547ef25f9fa044 Mon Sep 17 00:00:00 2001 From: Greg Hanefeld Date: Sat, 15 Aug 2026 16:09:43 -0700 Subject: [PATCH 2/3] fix: make stuck-button diagnostics sustained and bounded --- test/boot_button_rescue_test.py | 16 ++++++--- wled00/wled.cpp | 63 +++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/test/boot_button_rescue_test.py b/test/boot_button_rescue_test.py index 0e39cd1b47..f1e533aaf2 100644 --- a/test/boot_button_rescue_test.py +++ b/test/boot_button_rescue_test.py @@ -6,9 +6,15 @@ assert "checkRescuePin" not in source assert "checkRescueSerial" in source assert "WLED_DISABLE_STUCK_BUTTON_DIAGNOSTICS" in source -assert "diagnoseBootButtons();" in source -assert "buttons.size()" in source -assert "STUCK_BUTTON" in source -assert "WLED button diagnostics: healthy" in source +assert source.count("diagnoseBootButtons();") >= 2 +assert "stuckButtonInterval = 10000UL" in source +assert "activeSince[b] = 0" in source +assert "BTN_TYPE_PUSH && button.type != BTN_TYPE_PUSH_ACT_HIGH" in source +assert "active at boot" not in source +assert "sustained active" in source +assert "WLED button diagnostics: STUCK_BUTTON" in source +assert "WLED button diagnostics: HEALTHY/AVAILABLE" in source assert "WLED button diagnostics: disabled" in source -print("boot rescue admission and stuck-button diagnostics: PASS") +assert 'DEBUG_PRINTF_P(PSTR("heap %u\\n")' in source +assert 'PSTR("heap %u\\\\n")' not in source +print("boot rescue admission and sustained stuck-button diagnostics: PASS") diff --git a/wled00/wled.cpp b/wled00/wled.cpp index d89b57d31e..5c0a2780f9 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -76,30 +76,64 @@ static void detectRescueMode() } // AI: below section was generated by an AI -// Report configured digital inputs that remain active after configuration. +// Report configured digital momentary inputs only after sustained activity. static void diagnoseBootButtons() { #ifdef WLED_DISABLE_STUCK_BUTTON_DIAGNOSTICS - Serial.println(F("WLED button diagnostics: disabled (WLED_DISABLE_STUCK_BUTTON_DIAGNOSTICS)")); + static bool reported = false; + if (!reported) { + Serial.println(F("WLED button diagnostics: disabled (WLED_DISABLE_STUCK_BUTTON_DIAGNOSTICS)")); + reported = true; + } return; #else + constexpr unsigned long stuckButtonInterval = 10000UL; + static unsigned long activeSince[WLED_MAX_BUTTONS] = {0}; + static bool reportedStuck[WLED_MAX_BUTTONS] = {false}; + bool available = false; bool stuck = false; - for (size_t b = 0; b < buttons.size(); b++) { + const unsigned long now = millis(); + for (size_t b = 0; b < buttons.size() && b < WLED_MAX_BUTTONS; b++) { const auto &button = buttons[b]; - if (button.pin < 0 || button.type == BTN_TYPE_NONE || button.type == BTN_TYPE_ANALOG || - button.type == BTN_TYPE_ANALOG_INVERTED || button.type == BTN_TYPE_TOUCH || - button.type == BTN_TYPE_TOUCH_SWITCH || button.type == BTN_TYPE_SWITCH || - button.type == BTN_TYPE_PIR_SENSOR) continue; + if (button.pin < 0 || (button.type != BTN_TYPE_PUSH && button.type != BTN_TYPE_PUSH_ACT_HIGH)) continue; + available = true; const bool active = (button.type == BTN_TYPE_PUSH_ACT_HIGH) ? (digitalRead(button.pin) == HIGH) : (digitalRead(button.pin) == LOW); - if (active) { + if (!active) { + activeSince[b] = 0; + reportedStuck[b] = false; + continue; + } + if (activeSince[b] == 0) activeSince[b] = now; + if (now - activeSince[b] >= stuckButtonInterval) { stuck = true; - Serial.printf("WLED button diagnostic: active at boot index=%u gpio=%d type=%u\\n", - (unsigned)b, button.pin, (unsigned)button.type); + if (!reportedStuck[b]) { + Serial.printf("WLED button diagnostic: sustained active index=%u gpio=%d type=%u\n", + (unsigned)b, button.pin, (unsigned)button.type); + reportedStuck[b] = true; + } } } - Serial.println(stuck ? F("WLED button diagnostics: STUCK_BUTTON") : - F("WLED button diagnostics: healthy")); + static bool reportedHealthy = false; + static bool reportedUnavailable = false; + static bool reportedStatus = false; + if (stuck) { + if (!reportedStatus) { + Serial.println(F("WLED button diagnostics: STUCK_BUTTON")); + reportedStatus = true; + } + return; + } + reportedStatus = false; + if (available && !reportedHealthy) { + Serial.println(F("WLED button diagnostics: HEALTHY/AVAILABLE")); + reportedHealthy = true; + reportedUnavailable = false; + } else if (!available && !reportedUnavailable) { + Serial.println(F("WLED button diagnostics: disabled (no configured digital momentary buttons)")); + reportedUnavailable = true; + reportedHealthy = false; + } #endif } // AI: end @@ -186,6 +220,7 @@ void WLED::loop() return; } + diagnoseBootButtons(); handleTime(); #ifndef WLED_DISABLE_INFRARED handleIR(); // 2nd call to function needed for ESP32 to return valid results -- should be good for ESP8266, too @@ -623,8 +658,8 @@ void WLED::setup() briLast = 0; transitionDelay = 0; } - if (!wledRescueMode) diagnoseBootButtons(); - DEBUG_PRINTF_P(PSTR("heap %u\\n"), getFreeHeapSize()); + diagnoseBootButtons(); + DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); #if defined(STATUSLED) && STATUSLED>=0 if (!PinManager::isPinAllocated(STATUSLED)) { From 912684e711f30a275a39810ac80107f6038ca5c7 Mon Sep 17 00:00:00 2001 From: Greg Hanefeld Date: Sat, 15 Aug 2026 16:14:22 -0700 Subject: [PATCH 3/3] fix: reset boot button diagnostics on reconfiguration --- test/boot_button_rescue_test.py | 7 ++++++- wled00/wled.cpp | 33 +++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/test/boot_button_rescue_test.py b/test/boot_button_rescue_test.py index f1e533aaf2..62de7fce5c 100644 --- a/test/boot_button_rescue_test.py +++ b/test/boot_button_rescue_test.py @@ -9,11 +9,16 @@ assert source.count("diagnoseBootButtons();") >= 2 assert "stuckButtonInterval = 10000UL" in source assert "activeSince[b] = 0" in source +assert "activeInitialized[b] = false" in source +assert "if (!activeInitialized[b])" in source +assert "diagnosticPin[b] != pin" in source +assert "diagnosticType[b] != type" in source assert "BTN_TYPE_PUSH && button.type != BTN_TYPE_PUSH_ACT_HIGH" in source assert "active at boot" not in source assert "sustained active" in source assert "WLED button diagnostics: STUCK_BUTTON" in source -assert "WLED button diagnostics: HEALTHY/AVAILABLE" in source +assert "WLED button diagnostics: AVAILABLE/INACTIVE" in source +assert "HEALTHY/AVAILABLE" not in source assert "WLED button diagnostics: disabled" in source assert 'DEBUG_PRINTF_P(PSTR("heap %u\\n")' in source assert 'PSTR("heap %u\\\\n")' not in source diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 5c0a2780f9..1b075093de 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -89,22 +89,47 @@ static void diagnoseBootButtons() #else constexpr unsigned long stuckButtonInterval = 10000UL; static unsigned long activeSince[WLED_MAX_BUTTONS] = {0}; + static bool activeInitialized[WLED_MAX_BUTTONS] = {false}; static bool reportedStuck[WLED_MAX_BUTTONS] = {false}; + static int diagnosticPin[WLED_MAX_BUTTONS] = {0}; + static uint8_t diagnosticType[WLED_MAX_BUTTONS] = {0}; + static bool diagnosticIdentityInitialized[WLED_MAX_BUTTONS] = {false}; bool available = false; bool stuck = false; const unsigned long now = millis(); - for (size_t b = 0; b < buttons.size() && b < WLED_MAX_BUTTONS; b++) { + for (size_t b = 0; b < WLED_MAX_BUTTONS; b++) { + if (b >= buttons.size()) { + activeSince[b] = 0; + activeInitialized[b] = false; + reportedStuck[b] = false; + diagnosticIdentityInitialized[b] = false; + continue; + } const auto &button = buttons[b]; - if (button.pin < 0 || (button.type != BTN_TYPE_PUSH && button.type != BTN_TYPE_PUSH_ACT_HIGH)) continue; + const int pin = button.pin; + const uint8_t type = (uint8_t)button.type; + if (!diagnosticIdentityInitialized[b] || diagnosticPin[b] != pin || diagnosticType[b] != type) { + diagnosticPin[b] = pin; + diagnosticType[b] = type; + diagnosticIdentityInitialized[b] = true; + activeSince[b] = 0; + activeInitialized[b] = false; + reportedStuck[b] = false; + } + if (pin < 0 || (button.type != BTN_TYPE_PUSH && button.type != BTN_TYPE_PUSH_ACT_HIGH)) continue; available = true; const bool active = (button.type == BTN_TYPE_PUSH_ACT_HIGH) ? (digitalRead(button.pin) == HIGH) : (digitalRead(button.pin) == LOW); if (!active) { activeSince[b] = 0; + activeInitialized[b] = false; reportedStuck[b] = false; continue; } - if (activeSince[b] == 0) activeSince[b] = now; + if (!activeInitialized[b]) { + activeSince[b] = now; + activeInitialized[b] = true; + } if (now - activeSince[b] >= stuckButtonInterval) { stuck = true; if (!reportedStuck[b]) { @@ -126,7 +151,7 @@ static void diagnoseBootButtons() } reportedStatus = false; if (available && !reportedHealthy) { - Serial.println(F("WLED button diagnostics: HEALTHY/AVAILABLE")); + Serial.println(F("WLED button diagnostics: AVAILABLE/INACTIVE")); reportedHealthy = true; reportedUnavailable = false; } else if (!available && !reportedUnavailable) {