diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index cfd8cf0daa..b35338a2bf 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/application.h" +#include "esphome/core/ota_wake_hook.h" #endif #ifdef USE_ESP8266 @@ -861,9 +861,9 @@ 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. Safe from RP2040's low-priority user IRQ context: - // it only writes volatile bools, no heap or locks. - esphome::App.wake_ota_component_any_context(); + // flags, losing the wake event. Inline hook (ota_wake_hook.h) — two volatile stores, + // no function call. Safe from RP2040's low-priority user IRQ context. + esphome_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 d99b7f28d9..b2b2e0fdbe 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -2,6 +2,9 @@ #include "esphome/core/build_info_data.h" #include "esphome/core/log.h" #include "esphome/core/progmem.h" +#ifdef USE_OTA +#include "esphome/core/ota_wake_hook.h" +#endif #include #ifdef USE_ESP8266 @@ -449,14 +452,24 @@ void Application::enable_pending_loops_() { } } -#if defined(USE_OTA) && defined(USE_LWIP_FAST_SELECT) -// Called from the LwIP TCP/IP task via esphome_socket_event_callback() on NETCONN_EVT_RCVPLUS, -// BEFORE that callback calls xTaskNotifyGive() — pending-enable flags must be visible before -// the main task wakes, or the main loop can run a full iteration without seeing the request. -// Only marks the OTA component as pending loop-enable; does not itself wake the main task. -// OTA's __init__.py emits USE_OTA as both a cg.add_define (for static analyzers) AND a -// cg.add_build_flag (so the .c fast-select file also sees it). -extern "C" void esphome_wake_ota_component_any_context() { App.wake_ota_component_any_context(); } +#ifdef USE_OTA +// Storage for the inline OTA wake hook (see esphome/core/ota_wake_hook.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 into application.cpp. +extern "C" { +volatile bool *esphome_ota_pending_enable_loop_ptr = nullptr; +volatile bool *esphome_ota_has_pending_requests_ptr = nullptr; +} + +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_; +} #endif #ifdef USE_LWIP_FAST_SELECT @@ -566,11 +579,10 @@ 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 ota_wake_component_ is never set and this call is a no-op. Kept so the - // wake path works out of the box if host ever gains OTA support. Harmless - // cost: a single nullptr check per select() return. + // 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. if (ret > 0) { - this->wake_ota_component_any_context(); + esphome_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 67ef1d666f..48fc620cdc 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -560,23 +560,14 @@ class Application { static void IRAM_ATTR wake_loop_any_context() { esphome::wake_loop_any_context(); } #ifdef USE_OTA - /// Register the OTA component so socket-wake paths can enable its loop when - /// a new connection arrives on the listening socket. OTA disables its own - /// loop while idle to avoid per-tick dispatch overhead. - void set_ota_wake_component(Component *component) { this->ota_wake_component_ = component; } - /// Mark the registered OTA component (if any) for loop re-enable. Intentionally - /// does NOT call wake_loop_any_context() — every caller (lwip fast-select - /// callback, raw-tcp accept callback, host select() return path) has already - /// woken the main loop, so a second wake here would be redundant. Application - /// is a friend of Component, so we set the pending-enable flags directly. - /// Not IRAM_ATTR: all callers run in task / user-IRQ context, not a real ISR, - /// and the LwIP event callbacks that invoke this are not IRAM-resident either. - 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; - } - } + /// Register the OTA component so socket-wake paths can enable its loop when a new + /// connection arrives on the listening socket. Captures the address of the component's + /// pending_enable_loop_ flag and the Application has_pending_enable_loop_requests_ flag + /// into extern C globals consumed by the inline wake hook in ota_wake_hook.h. Defined + /// out-of-line in application.cpp so application.h doesn't need to pull in the hook + /// header. 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); #endif protected: @@ -654,9 +645,6 @@ 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 613d1f29ec..f0cafc2dbe 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -124,6 +124,9 @@ #include "esphome/core/lwip_fast_select.h" #include "esphome/core/main_task.h" +#ifdef ESPHOME_USE_OTA +#include "esphome/core/ota_wake_hook.h" +#endif #include diff --git a/esphome/core/ota_wake_hook.h b/esphome/core/ota_wake_hook.h new file mode 100644 index 0000000000..06ed1623df --- /dev/null +++ b/esphome/core/ota_wake_hook.h @@ -0,0 +1,45 @@ +#pragma once + +// Inline OTA wake hook, called from lwip_fast_select.c on every NETCONN_EVT_RCVPLUS so a +// disabled OTA loop can be re-enabled when a monitored socket signals activity. +// +// Defined as a static inline here (rather than an out-of-line extern "C" shim into +// application.cpp) so the fast-select callback pays zero function-call overhead per +// socket event: the two volatile stores below are cheaper inlined than dispatched. +// +// The two pointers are set once in Application::set_ota_wake_component() to the addresses +// of Component::pending_enable_loop_ and Application::has_pending_enable_loop_requests_. +// Accessing those C++ members by raw address is safe: Application is a friend of Component +// (granting access at registration time), and volatile writes through a bool* see the same +// storage the C++ side reads. + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Address of the registered OTA component's pending_enable_loop_ flag. NULL until +// Application::set_ota_wake_component() is called. When non-NULL, the has-pending +// pointer below is also non-NULL, so a single null check covers both. +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. +extern volatile bool *esphome_ota_has_pending_requests_ptr; + +// Mark the registered OTA component pending loop-enable. Safe to call from LwIP TCP/IP +// task context and raw-TCP IRQ context — only writes to volatile bools, no locks. +// Callers must invoke this BEFORE waking the main task, so the flags are visible to the +// main loop's 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 +} +#endif