diff --git a/src/helpers/ESP32Board.cpp b/src/helpers/ESP32Board.cpp index b7d4834df..37cba1c49 100644 --- a/src/helpers/ESP32Board.cpp +++ b/src/helpers/ESP32Board.cpp @@ -1432,11 +1432,23 @@ bool ESP32Board::emergencySleepMilliseconds(uint32_t millisecs) { #endif const TickType_t usb_ticks = usb_held ? pdMS_TO_TICKS(millisecs) : 0; if (usb_held && usb_ticks == 0) return false; + TickType_t usb_service_ticks = 0; +#if MESH_ESP32_USB_CONSOLE_COOPERATIVE + if (!usb_held) { + // SOF/connection state is sampled by RTOS hooks. Reserve awake service + // time after a timer wake so a newly attached host can become visible + // before the next sleep attempt. Keep the whole wait within this budget. + const uint32_t service_ms = millisecs > 100UL ? 100UL : millisecs; + usb_service_ticks = pdMS_TO_TICKS(service_ms); + if (usb_service_ticks == 0) return false; + millisecs -= service_ms; + } +#endif if (watched && esp_task_wdt_reset() != ESP_OK) return false; - if (usb_held) { + if (usb_held || millisecs == 0) { // 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); + vTaskDelay(usb_held ? usb_ticks : usb_service_ticks); return !watched || esp_task_wdt_reset() == ESP_OK; } if (esp_sleep_enable_timer_wakeup(uint64_t(millisecs) * 1000ULL) != ESP_OK) @@ -1445,6 +1457,8 @@ bool ESP32Board::emergencySleepMilliseconds(uint32_t millisecs) { // normal sleep path installs its own next timer; never leave ours armed. const esp_err_t slept = esp_light_sleep_start(); const esp_err_t cleared = esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_TIMER); + if (slept == ESP_OK && cleared == ESP_OK && usb_service_ticks != 0) + vTaskDelay(usb_service_ticks); const bool watchdog_ok = !watched || esp_task_wdt_reset() == ESP_OK; return slept == ESP_OK && cleared == ESP_OK && watchdog_ok; } diff --git a/test/test_emergency_sleep_backends.py b/test/test_emergency_sleep_backends.py index 0f4c18e19..973261138 100644 --- a/test/test_emergency_sleep_backends.py +++ b/test/test_emergency_sleep_backends.py @@ -223,9 +223,7 @@ int main() { class EmergencySleepBackendTests(unittest.TestCase): - def test_actual_platform_waits_and_cleanup(self): - compiler = shutil.which("g++") or shutil.which("clang++") - self.assertIsNotNone(compiler) + def platform_code(self): sources = {name: (ROOT / path).read_text() for name, path in ( ("core", "src/MeshCore.h"), ("nrf", "src/helpers/NRF52Board.cpp"), @@ -242,17 +240,63 @@ class EmergencySleepBackendTests(unittest.TestCase): ("NRF_SLEEP", "nrf", "bool NRF52Board::emergencySleepMilliseconds("), ("ESP_SLEEP", "esp", "bool ESP32Board::emergencySleepMilliseconds(")): code = code.replace("@" + marker + "@", extract_braced(sources[source], signature)) + return code + + def run_code(self, code, flags=()): + compiler = shutil.which("g++") or shutil.which("clang++") + self.assertIsNotNone(compiler) with tempfile.TemporaryDirectory(prefix="emergency-sleep-backends-") as directory: work = Path(directory) cpp, binary = work / "test.cpp", work / "test" cpp.write_text(code) subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", - "-DCONFIG_ESP_TASK_WDT_TIMEOUT_S=4", *SANITIZERS, + "-DCONFIG_ESP_TASK_WDT_TIMEOUT_S=4", *flags, *SANITIZERS, str(cpp), "-o", str(binary)], check=True, capture_output=True, text=True, timeout=60) subprocess.run([str(binary)], check=True, capture_output=True, text=True, timeout=20) + def test_actual_platform_waits_and_cleanup(self): + self.run_code(self.platform_code()) + + def test_native_usb_timer_wake_reserves_enumeration_service_without_extending_budget(self): + code = self.platform_code().split("int main() {", 1)[0] + r''' +int main() { + ESP32Board esp; + reset(); assert(esp.emergencySleepMilliseconds(10000)); + assert(last_armed_us == 9900000ULL && delayed_ticks == 100); + assert((events == std::vector{"timer", "sleep", "clear_timer", "delay"})); + assert(timer_us == 0 && other_wake_sources == 0x71); + reset(); task_watchdog_status = ESP_OK; + assert(esp.emergencySleepMilliseconds(10000)); + assert(last_armed_us == 1900000ULL && delayed_ticks == 100 && task_watchdog_feeds == 2); + assert((events == std::vector{"task_feed", "timer", "sleep", "clear_timer", "delay", "task_feed"})); + reset(); assert(esp.emergencySleepMilliseconds(125)); + assert(last_armed_us == 25000 && delayed_ticks == 100); + reset(); assert(esp.emergencySleepMilliseconds(30)); + assert(delayed_ticks == 30 && events == std::vector{"delay"}); + assert(!timer_calls && !sleep_calls && !clear_calls); + reset(); tick_ms = 10; + assert(esp.emergencySleepMilliseconds(129)); + assert(last_armed_us == 29000 && delayed_ticks == 10); // Timer + floored RTOS wait <=129ms. + reset(); tick_ms = 1000; + assert(!esp.emergencySleepMilliseconds(10000) && events.empty()); + for (int* failed : {&sleep_error, &clear_error}) { + reset(); *failed = 9; + assert(!esp.emergencySleepMilliseconds(10000)); + assert(!delayed_ticks && clear_calls == 1); + assert((events == std::vector{"timer", "sleep", "clear_timer"})); + } + reset(); timer_error = 9; + assert(!esp.emergencySleepMilliseconds(10000)); + assert(!delayed_ticks && !sleep_calls && !clear_calls); + reset(); usb_held = true; wireless_ready = false; + assert(esp.emergencySleepMilliseconds(10000)); + assert(delayed_ticks == 100 && !sleep_calls); // Existing host still bypasses physical USB sleep. +} +''' + self.run_code(code, ("-DMESH_ESP32_USB_CONSOLE_COOPERATIVE=1",)) + def test_disabled_feature_has_no_new_base_virtuals(self): compiler = shutil.which("g++") or shutil.which("clang++") self.assertIsNotNone(compiler) diff --git a/test/test_esp32_cpu_clock_board.py b/test/test_esp32_cpu_clock_board.py index 58096c34e..1318f675b 100644 --- a/test/test_esp32_cpu_clock_board.py +++ b/test/test_esp32_cpu_clock_board.py @@ -182,8 +182,12 @@ int main(){ def test_actual_infrastructure_loop_services_saved_choice_and_quiet_deadline(self): for role in ("simple_repeater","simple_room_server","simple_sensor"): source=(ROOT/"examples"/role/"main.cpp").read_text() - start=source.index(" board.servicePowerTestControls();",source.index("void loop()")) - end=source.index("#endif",start) + loop_start=source.index("void loop()") + clock_service=source.index("mesh::esp32::serviceCpuFrequencyPolicy(",loop_start) + # Emergency maintenance also services UART restoration before its + # early return. Keep this fixture anchored to the clock policy. + start=source.rindex(" board.servicePowerTestControls();",loop_start,clock_service) + end=source.index("#endif",clock_service) service=source[start:end] tests="void role_service(){\n"+service+"}\n"+r''' int main(){