diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index b6e5730b66..08de3f73a8 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -11,7 +11,7 @@ #include "esphome/core/wake.h" #include "esphome/core/log.h" #ifdef USE_OTA -#include "esphome/core/wake.h" // inline esphome_wake_ota_component_any_context() lives here +#include "esphome/core/application.h" // for App.wake_ota_component_any_context() #endif #ifdef USE_ESP8266 @@ -861,9 +861,11 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { // Mark the OTA component loop to be re-enabled if it disabled itself while idle. // This MUST happen before wake_loop_any_context() below — otherwise the main loop // could wake, run a full iteration, and finish before we set the pending-enable - // flags, losing the wake event. Inline hook (wake.h) — two volatile stores, no - // function call. Safe from RP2040's low-priority user IRQ context. - esphome_wake_ota_component_any_context(); + // flags, losing the wake event. accept_fn_ only fires for the specific listener + // pcb it was registered on, so there's implicit filtering here (no "false wakes" + // from unrelated sockets, unlike the fast-select path). Safe from RP2040's + // low-priority user IRQ context — only writes a volatile bool, no locks. + esphome::App.wake_ota_component_any_context(); #endif // Wake the main loop immediately so it can accept the new connection. esphome::wake_loop_any_context(); diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 21d3ef4494..ffb374b321 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -2,7 +2,6 @@ #include "esphome/core/build_info_data.h" #include "esphome/core/log.h" #include "esphome/core/progmem.h" -#include "esphome/core/wake.h" #include #ifdef USE_ESP8266 @@ -450,27 +449,12 @@ void Application::enable_pending_loops_() { } } -// Storage for the inline OTA wake hook (see esphome/core/wake.h). Set in -// Application::set_ota_wake_component() and read from the lwip fast-select callback -// on every NETCONN_EVT_RCVPLUS. Kept as raw C globals so the .c file can inline the -// wake body (two volatile stores) without a function-call round trip. Defined -// unconditionally so wake.h's inline compiles the same whether or not OTA is in the -// build — when OTA is absent nothing ever calls set_ota_wake_component() and the -// pointers stay nullptr, collapsing the inline to a single null check. -extern "C" { -volatile bool *esphome_ota_pending_enable_loop_ptr = nullptr; -volatile bool *esphome_ota_has_pending_requests_ptr = nullptr; -} - -#ifdef USE_OTA -void Application::set_ota_wake_component(Component *component) { - // Application is a friend of Component — can take the address of its protected - // pending_enable_loop_ field. The C-side inline hook writes through that pointer when - // any monitored socket fires RCVPLUS, making the flags visible to enable_pending_loops_() - // on the next main loop iteration. - esphome_ota_pending_enable_loop_ptr = &component->pending_enable_loop_; - esphome_ota_has_pending_requests_ptr = &this->has_pending_enable_loop_requests_; -} +#if defined(USE_OTA) && defined(USE_LWIP_FAST_SELECT) +// Extern-C trampoline for the lwip fast-select callback to reach the C++ wake method. +// Invoked only when the listener filter in lwip_fast_select.c matches — i.e. only on +// actual incoming connections to the OTA listen socket — so the function-call overhead +// here is paid at most once per real OTA attempt, not on every monitored-socket event. +extern "C" void esphome_wake_ota_component_any_context() { App.wake_ota_component_any_context(); } #endif #ifdef USE_LWIP_FAST_SELECT @@ -580,10 +564,12 @@ void Application::yield_with_select_(uint32_t delay_ms) { if (ret >= 0) [[likely]] { #ifdef USE_OTA // Dead code today — host does not currently support the esphome OTA platform, - // so the inline hook's pointers are always NULL and this is a no-op. Kept so the - // wake path works out of the box if host ever gains OTA support. + // so ota_wake_component_ is always null and this is a no-op. Kept so the wake + // path works out of the box if host ever gains OTA support. Host has no listener + // filter (that lives in lwip_fast_select.c, ESP32/LibreTiny only), so any + // ret > 0 fires this — harmless when the pointer is null. if (ret > 0) { - esphome_wake_ota_component_any_context(); + this->wake_ota_component_any_context(); } #endif // Yield if zero timeout since select(0) only polls without yielding diff --git a/esphome/core/application.h b/esphome/core/application.h index baf8e21a99..7a3595ac49 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -561,13 +561,22 @@ class Application { #ifdef USE_OTA /// Register the OTA component so socket-wake paths can enable its loop when a new - /// connection arrives on the listening socket. Captures the addresses of the component's - /// pending_enable_loop_ flag and Application's has_pending_enable_loop_requests_ flag - /// into extern-C globals consumed by the inline wake hook in wake.h. Defined out-of-line - /// in application.cpp so application.h doesn't need to pull in wake.h. OTA calls this - /// once from setup(); the component itself then self-disables its loop on its first - /// idle tick. - void set_ota_wake_component(Component *component); + /// connection arrives on the listening socket. OTA calls this once from setup(); + /// the component self-disables its loop on its first idle tick and is re-enabled + /// via wake_ota_component_any_context() when the fast-select filter in + /// lwip_fast_select.c matches an incoming connection on the OTA listener. + void set_ota_wake_component(Component *component) { this->ota_wake_component_ = component; } + /// Mark the registered OTA component pending loop-enable. Called from the LwIP + /// TCP/IP task (fast-select callback), raw-TCP accept callback, and host select + /// return path — all task / user-IRQ context, not a real ISR. Does NOT wake the + /// main task itself: every caller already does so separately. Application is a + /// friend of Component, so we set pending_enable_loop_ directly. + void wake_ota_component_any_context() { + if (this->ota_wake_component_ != nullptr) { + this->ota_wake_component_->pending_enable_loop_ = true; + this->has_pending_enable_loop_requests_ = true; + } + } #endif protected: @@ -645,6 +654,9 @@ class Application { // Pointer-sized members first Component *current_component_{nullptr}; +#ifdef USE_OTA + Component *ota_wake_component_{nullptr}; // Set by ESPHomeOTAComponent to receive socket-wake notifications +#endif // std::vector (3 pointers each: begin, end, capacity) // Partitioned vector design for looping components diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 60a1cf60f1..9f23401aba 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -124,9 +124,6 @@ #include "esphome/core/lwip_fast_select.h" #include "esphome/core/main_task.h" -#ifdef ESPHOME_USE_OTA -#include "esphome/core/wake.h" // inline esphome_wake_ota_component_any_context() lives here -#endif #include @@ -164,15 +161,17 @@ static netconn_callback s_original_callback = NULL; // OTA listener netconn, captured via esphome_fast_select_set_ota_listener_sock() at OTA // setup(). The wake hook only fires when the callback's `conn` argument matches this // pointer — i.e. only for RCVPLUS events on the OTA listen socket (new accepts). Avoids -// paying the inline wake hook's cost (two volatile stores + memw barriers) on every API -// client data packet, mDNS query, etc. +// paying the wake-hook function call on every API client data packet, mDNS query, etc. static struct netconn *s_ota_listener_conn = NULL; -// Inline wake hook defined in esphome/core/wake.h. ESPHOME_USE_OTA (not USE_OTA) because -// USE_OTA only lives in defines.h, and this .c file cannot include defines.h — macros.h → -// Arduino.h would break the C compile under Arduino builds. ota/__init__.py emits -// -DESPHOME_USE_OTA as a build flag so this file can see it without a name collision with -// the defines.h USE_OTA entry. -#include "esphome/core/wake.h" + +// Extern-C trampoline defined in application.cpp — calls App.wake_ota_component_any_context() +// to mark the OTA component pending loop-enable. Out-of-line (not inlined) because with the +// listener filter above, this only fires on actual OTA connection attempts — the function-call +// cost is paid at most once per real wake, not on every monitored-socket RCVPLUS. +// ESPHOME_USE_OTA (not USE_OTA) because USE_OTA only lives in defines.h, and this .c file +// cannot include defines.h — macros.h → Arduino.h would break the C compile under Arduino +// builds. ota/__init__.py emits -DESPHOME_USE_OTA as a build flag so this file can see it. +extern void esphome_wake_ota_component_any_context(void); void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock) { s_ota_listener_conn = (sock != NULL) ? sock->conn : NULL; diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 9ad761e8aa..a8c9b7ad08 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -3,63 +3,6 @@ /// @file wake.h /// Platform-specific main loop wake primitives. /// Always available on all platforms — no opt-in needed. -/// -/// This file has two sections: -/// 1. A C-compatible section at the top (the inline OTA wake hook) that .c files -/// like lwip_fast_select.c can include. Uses only /. -/// 2. A C++ section (everything after #ifdef __cplusplus below) with the existing -/// platform wake primitives and wakeable_delay() implementations. - -// ============================================================================ -// C-compatible section: inline OTA wake hook -// ============================================================================ -// -// Called from lwip_fast_select.c (and lwip_raw_tcp_impl.cpp / host select) on every -// NETCONN_EVT_RCVPLUS so a disabled OTA loop can be re-enabled when a monitored -// socket signals activity. Static inline (not an extern-C trampoline) so the .c -// callback pays zero function-call overhead — just a pointer load, null check, and -// two volatile bool stores inlined into the callback body. -// -// The two pointers are captured once in Application::set_ota_wake_component(): they -// point at Component::pending_enable_loop_ and Application::has_pending_enable_loop_requests_. -// When OTA is not compiled into the build, nothing calls set_ota_wake_component(), -// the pointers stay NULL, and the inline collapses to a single null check. - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -// Address of the registered OTA component's pending_enable_loop_ flag, captured at -// registration time. NULL when no OTA component has registered (including when OTA -// is not compiled in at all). -extern volatile bool *esphome_ota_pending_enable_loop_ptr; -// Address of Application::has_pending_enable_loop_requests_. Set in tandem with the -// pending_enable pointer above — single null check covers both. -extern volatile bool *esphome_ota_has_pending_requests_ptr; - -// Mark the registered OTA component pending loop-enable. Safe from the LwIP TCP/IP -// task and raw-TCP IRQ context — only volatile stores, no locks. Callers MUST invoke -// this BEFORE waking the main task, so the flags are visible on the next iteration. -static inline void esphome_wake_ota_component_any_context(void) { - volatile bool *pending_enable = esphome_ota_pending_enable_loop_ptr; - if (pending_enable != NULL) { - *pending_enable = true; - *esphome_ota_has_pending_requests_ptr = true; - } -} - -#ifdef __cplusplus -} // extern "C" -#endif - -// ============================================================================ -// C++ section: platform wake primitives -// ============================================================================ - -#ifdef __cplusplus #include "esphome/core/defines.h" #include "esphome/core/hal.h" @@ -181,5 +124,3 @@ inline void wakeable_delay(uint32_t ms) { #endif } // namespace esphome - -#endif // __cplusplus