diff --git a/test/boot_button_rescue_test.py b/test/boot_button_rescue_test.py new file mode 100644 index 0000000000..62de7fce5c --- /dev/null +++ b/test/boot_button_rescue_test.py @@ -0,0 +1,25 @@ +#!/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 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: 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 +print("boot rescue admission and sustained stuck-button diagnostics: PASS") diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 00b8f9ab86..1b075093de 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,94 @@ static void detectRescueMode() #endif } +// AI: below section was generated by an AI +// Report configured digital momentary inputs only after sustained activity. +static void diagnoseBootButtons() +{ +#ifdef 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 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 < 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]; + 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 (!activeInitialized[b]) { + activeSince[b] = now; + activeInitialized[b] = true; + } + if (now - activeSince[b] >= stuckButtonInterval) { + stuck = true; + 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; + } + } + } + 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: AVAILABLE/INACTIVE")); + 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 + static void handleRescueMode() { static bool announced = false; @@ -178,6 +245,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 @@ -615,6 +683,7 @@ void WLED::setup() briLast = 0; transitionDelay = 0; } + diagnoseBootButtons(); DEBUG_PRINTF_P(PSTR("heap %u\n"), getFreeHeapSize()); #if defined(STATUSLED) && STATUSLED>=0