mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
[esphome.ota] Revert inline wake hook to a single Component * + extern C call
With the listener filter from the previous commit, the wake hook only fires on actual OTA connection attempts — no more spurious wakes from API client data packets or other monitored-socket traffic. That removes the motivation for inlining the hook at three call sites. Collapse back to the simpler shape: - Application gains Component *ota_wake_component_ (one pointer, 4 bytes, gated on USE_OTA) and a wake_ota_component_any_context() inline method. - lwip_fast_select.c reaches the method via an extern-C trampoline (esphome_wake_ota_component_any_context) defined in application.cpp. One call_n instruction per actual wake, which now happens at most once per real OTA upload attempt. - Raw-TCP (LWIPRawListenImpl::accept_fn_) and host select paths call App.wake_ota_component_any_context() directly — both are .cpp files. - wake.h reverts to its pre-PR state (no C-compatible section, no extern pointer globals, no inline OTA hook). All wake-related state still lives in Application. Net effect versus the inline approach: - RAM: -4 bytes (one pointer vs two) - Flash: ~-60 bytes (no 3x duplication of the inlined hook body) - CPU: function-call overhead (~10 cycles) paid only on actual OTA wakes, which happen ~0 times/sec in steady state. Inline was premature once filtering reduced the fire rate to "rare intentional events."
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 <cstring>
|
||||
|
||||
#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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <stddef.h>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <stdbool.h>/<stddef.h>.
|
||||
/// 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 <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#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
|
||||
|
||||
Reference in New Issue
Block a user