diff --git a/src/helpers/ESP32Board.cpp b/src/helpers/ESP32Board.cpp index b5591fc66..b7d4834df 100644 --- a/src/helpers/ESP32Board.cpp +++ b/src/helpers/ESP32Board.cpp @@ -1413,12 +1413,15 @@ void ESP32Board::powerOff() { #if MESH_ENABLE_EMERGENCY_POWER bool ESP32Board::emergencySleepMilliseconds(uint32_t millisecs) { - // The caller owns radio suspension and reversible wireless shutdown. Never - // force a live WiFi/Bluetooth stack through explicit light sleep. if (millisecs == 0 || isRadioTestActive() || xPortInIsrContext() - || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING - || !mesh::esp32::manualLightSleepReady()) return false; + || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING) return false; + // Native USB loses its connection in light sleep, even without an open CDC + // reader. Retain the ordinary physical-host/loss-grace policy here too. + // RF remains suspended, but an attached host gets frequent command service. + const bool usb_held = isUsbSleepHeld(); + if (!usb_held && !mesh::esp32::manualLightSleepReady()) return false; if (millisecs > 10000UL) millisecs = 10000UL; + if (usb_held && millisecs > 100UL) millisecs = 100UL; const esp_err_t watchdog_status = esp_task_wdt_status(nullptr); const bool watched = watchdog_status == ESP_OK; if (!watched && watchdog_status != ESP_ERR_NOT_FOUND @@ -1427,7 +1430,15 @@ bool ESP32Board::emergencySleepMilliseconds(uint32_t millisecs) { if (watched && millisecs > uint32_t(CONFIG_ESP_TASK_WDT_TIMEOUT_S) * 500UL) millisecs = uint32_t(CONFIG_ESP_TASK_WDT_TIMEOUT_S) * 500UL; #endif + const TickType_t usb_ticks = usb_held ? pdMS_TO_TICKS(millisecs) : 0; + if (usb_held && usb_ticks == 0) return false; if (watched && esp_task_wdt_reset() != ESP_OK) return false; + if (usb_held) { + // This also remains safe during asynchronous wireless teardown. Do not + // arm or clear wake sources while yielding with the USB peripheral live. + vTaskDelay(usb_ticks); + return !watched || esp_task_wdt_reset() == ESP_OK; + } if (esp_sleep_enable_timer_wakeup(uint64_t(millisecs) * 1000ULL) != ESP_OK) return false; // Add no radio/GPIO wake. Existing button sources remain untouched. The diff --git a/test/test_emergency_sleep_backends.py b/test/test_emergency_sleep_backends.py index 3d065f62e..0f4c18e19 100644 --- a/test/test_emergency_sleep_backends.py +++ b/test/test_emergency_sleep_backends.py @@ -22,6 +22,7 @@ HARNESS = r''' #include static std::vector events; static bool radio_test = false, interrupt_context = false, wireless_ready = true; +static bool usb_held = false; static unsigned primask = 0, basepri = 0, faultmask = 0; static unsigned __get_IPSR() { return interrupt_context; } static unsigned __get_PRIMASK() { return primask; } @@ -102,6 +103,7 @@ struct NRF52Board : Base { }; struct ESP32Board : Base { bool isRadioTestActive() const { return radio_test; } + bool isUsbSleepHeld() const { return usb_held; } @ESP_SUPPORT@ bool emergencySleepMilliseconds(uint32_t millisecs) override; }; @@ -109,6 +111,7 @@ struct ESP32Board : Base { @ESP_SLEEP@ static void reset() { events.clear(); radio_test = interrupt_context = false; wireless_ready = true; + usb_held = false; primask = basepri = faultmask = 0; nrf_nvic_state.__cr_flag = 0; scheduler = taskSCHEDULER_RUNNING; tick_ms = 1; delayed_ticks = nrf_feeds = 0; @@ -165,6 +168,29 @@ int main() { reset(); wireless_ready = false; assert(!esp.emergencySleepMilliseconds(100)); assert(events.empty()); + reset(); usb_held = true; wireless_ready = false; timer_us = 555; + assert(esp.emergencySleepMilliseconds(UINT32_MAX)); + assert(delayed_ticks == 100 && events == std::vector{"delay"}); + assert(!timer_calls && !sleep_calls && !clear_calls && timer_us == 555); + assert(other_wake_sources == 0x71); // USB, RF and existing wake ownership are untouched. + usb_held = false; events.clear(); + assert(!esp.emergencySleepMilliseconds(100) && events.empty()); // Teardown is still live. + wireless_ready = true; + assert(esp.emergencySleepMilliseconds(100)); // Recheck the host on every call; detach permits light sleep. + assert(last_armed_us == 100000 && sleep_calls == 1 && !timer_us); + reset(); usb_held = true; task_watchdog_status = ESP_OK; + assert(esp.emergencySleepMilliseconds(250)); + assert(delayed_ticks == 100 && task_watchdog_feeds == 2); + assert((events == std::vector{"task_feed", "delay", "task_feed"})); + assert(!timer_calls && !sleep_calls && !clear_calls); + reset(); usb_held = true; tick_ms = 1000; + assert(!esp.emergencySleepMilliseconds(100) && events.empty()); + for (unsigned failure : {1U, 2U}) { + reset(); usb_held = true; task_watchdog_status = ESP_OK; fail_watchdog_feed = failure; + assert(!esp.emergencySleepMilliseconds(100)); + assert(delayed_ticks == (failure == 1 ? 0U : 100U)); + assert(!timer_calls && !sleep_calls && !clear_calls); + } reset(); task_watchdog_status = 99; assert(!esp.emergencySleepMilliseconds(100)); assert(events.empty()); reset(); assert(esp.emergencySleepMilliseconds(UINT32_MAX));