From 46bff3f3eb52b18da6d93c0e3e5dcd0dd3462901 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Feb 2026 10:48:33 -1000 Subject: [PATCH] [core] Wake main loop from ISR in enable_loop_soon_any_context() enable_loop_soon_any_context() sets volatile flags but does not wake the main loop from ulTaskNotifyTake() sleep. This means components using ISR-driven state changes (e.g. GPIO binary sensors) wait up to ~16ms for the select timeout before their loop runs. Add Application::wake_loop_isrsafe(nullptr) to immediately wake the main loop when called from ISR context on platforms with fast select. Also relax the wake_loop_isrsafe() guard from requiring both USE_WAKE_LOOP_THREADSAFE and USE_LWIP_FAST_SELECT to just USE_LWIP_FAST_SELECT, since the ISR wake path uses vTaskNotifyGiveFromISR directly and does not depend on the UDP socket mechanism. --- esphome/core/application.h | 2 +- esphome/core/component.cpp | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 13e0f638856..f3c09591ac3 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -501,7 +501,7 @@ class Application { void wake_loop_threadsafe(); #endif -#if defined(USE_WAKE_LOOP_THREADSAFE) && defined(USE_LWIP_FAST_SELECT) +#ifdef USE_LWIP_FAST_SELECT /// Wake the main event loop from an ISR. /// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe. /// Only available on platforms with fast select (ESP32, LibreTiny). diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 1fd621ea83a..36eef1e6c8d 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -323,6 +323,13 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; +#ifdef USE_LWIP_FAST_SELECT + // Wake the main loop if sleeping in ulTaskNotifyTake(). Without this, + // the main loop would not wake until the select timeout expires (~16ms). + // vTaskNotifyGiveFromISR(NULL) is safe here — it skips the yield flag + // and the context switch happens at the next scheduler tick (<1ms). + Application::wake_loop_isrsafe(nullptr); +#endif } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {