[esphome.ota] Move OTA wake pointer out of Application into ota_esphome.cpp

Following the zephyr_mcumgr precedent, keep the single-instance pointer
as a file-scope static inside ota_esphome.cpp instead of plumbing an
ota_wake_component_ slot and setter through Application. The extern "C"
wake trampoline also lives in the same TU now, so nothing in core/
touches OTA-specific state.

Guard the C and C++ call sites on USE_OTA_PLATFORM_ESPHOME (added as a
-D flag from components/esphome/ota/__init__.py) instead of the broader
ESPHOME_USE_OTA that base ota/ used to emit — the trampoline symbol only
exists when the esphome OTA platform is actually compiled in.

Also drop the dead host yield_with_select_ wake hook (host has no OTA
platform today).
This commit is contained in:
J. Nick Koston
2026-04-11 08:17:49 -10:00
parent 82699a7ce3
commit e67fac704c
7 changed files with 36 additions and 40 deletions
@@ -155,6 +155,10 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
cg.add_define("USE_OTA_PASSWORD")
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
# Build flag (not a define in defines.h) so lwip_fast_select.c — a .c file that
# can't include defines.h — can still gate its wake hook on the esphome OTA
# platform being compiled in.
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
await cg.register_component(var, config)
await ota_to_code(var, config)
+17 -1
View File
@@ -31,6 +31,22 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
// File-scope pointer to the single ESPHomeOTAComponent instance. The final_validate in
// __init__.py rejects multi-port configs, so only one exists. Set in setup() and used
// by the extern "C" wake trampoline below, which is called from socket wake paths
// (lwip_fast_select listener filter, raw-TCP accept_fn_).
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static ESPHomeOTAComponent *global_esphome_ota_component = nullptr;
// Trampoline called from C and C++ socket wake paths. Must be safe from any context
// (LwIP TCP/IP task, user-level IRQ on RP2040). enable_loop_soon_any_context() uses
// volatile flags only — no locks, no allocations.
extern "C" void esphome_wake_ota_component_any_context() {
if (global_esphome_ota_component != nullptr) {
global_esphome_ota_component->enable_loop_soon_any_context();
}
}
void ESPHomeOTAComponent::setup() {
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->server_ == nullptr) {
@@ -70,7 +86,7 @@ void ESPHomeOTAComponent::setup() {
}
// loop() self-disables on its first idle tick; no explicit disable_loop() needed here.
App.set_ota_wake_component(this);
global_esphome_ota_component = this;
#ifdef USE_LWIP_FAST_SELECT
// Filter fast-select wakes to this listener only. If the sock lookup returns nullptr,
// no wakes fire and loop() falls back to the self-disable safety net.
-3
View File
@@ -103,9 +103,6 @@ BASE_OTA_SCHEMA = cv.Schema(
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config):
cg.add_define("USE_OTA")
# Separate -D flag so .c files can see it — .c can't include defines.h (macros.h
# pulls in Arduino.h). Different name avoids USE_OTA redefinition warnings.
cg.add_build_flag("-DESPHOME_USE_OTA")
CORE.add_job(final_step)
if CORE.is_rp2040 and CORE.using_arduino:
@@ -10,8 +10,11 @@
#include "esphome/core/helpers.h"
#include "esphome/core/wake.h"
#include "esphome/core/log.h"
#ifdef USE_OTA
#include "esphome/core/application.h"
#ifdef USE_OTA_PLATFORM_ESPHOME
// Defined in components/esphome/ota/ota_esphome.cpp. Marks the ESPHome OTA component
// pending-enable from the raw-TCP accept callback (per-pcb, implicitly filtered).
extern "C" void esphome_wake_ota_component_any_context();
#endif
#ifdef USE_ESP8266
@@ -857,10 +860,11 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) {
tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn);
tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn);
LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_);
#ifdef USE_OTA
#ifdef USE_OTA_PLATFORM_ESPHOME
// Mark OTA pending-enable before the wake below — flags must be visible before the
// main task runs. accept_fn_ is per-pcb, so filtering is implicit here.
esphome::App.wake_ota_component_any_context();
// main task runs. accept_fn_ is per-pcb, so filtering is implicit here (only fires
// for completed handshakes to whichever listener owns newpcb).
esphome_wake_ota_component_any_context();
#endif
// Wake the main loop immediately so it can accept the new connection.
esphome::wake_loop_any_context();
-11
View File
@@ -449,11 +449,6 @@ void Application::enable_pending_loops_() {
}
}
#if defined(USE_OTA) && defined(USE_LWIP_FAST_SELECT)
// C trampoline called from lwip_fast_select.c when the listener filter matches.
extern "C" void esphome_wake_ota_component_any_context() { App.wake_ota_component_any_context(); }
#endif
#ifdef USE_LWIP_FAST_SELECT
bool Application::register_socket(struct lwip_sock *sock) {
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
@@ -559,12 +554,6 @@ void Application::yield_with_select_(uint32_t delay_ms) {
// ret > 0: socket(s) have data ready - normal and expected
// ret == 0: timeout occurred - normal and expected
if (ret >= 0) [[likely]] {
#ifdef USE_OTA
// No-op today — host has no esphome OTA platform, so ota_wake_component_ is null.
if (ret > 0) {
this->wake_ota_component_any_context();
}
#endif
// Yield if zero timeout since select(0) only polls without yielding
if (delay_ms == 0) [[unlikely]] {
yield();
-15
View File
@@ -559,18 +559,6 @@ class Application {
/// Wake from any context (ISR, thread, callback).
static void IRAM_ATTR wake_loop_any_context() { esphome::wake_loop_any_context(); }
#ifdef USE_OTA
void set_ota_wake_component(Component *component) { this->ota_wake_component_ = component; }
// Marks OTA pending-enable from socket wake paths. Does NOT wake the main task —
// every caller already does. Callable from LwIP TCP/IP task and user-IRQ context.
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:
friend Component;
#ifdef USE_HOST
@@ -646,9 +634,6 @@ class Application {
// Pointer-sized members first
Component *current_component_{nullptr};
#ifdef USE_OTA
Component *ota_wake_component_{nullptr};
#endif
// std::vector (3 pointers each: begin, end, capacity)
// Partitioned vector design for looping components
+6 -5
View File
@@ -157,11 +157,12 @@ _Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVEN
// Saved original event_callback pointer — written once in first hook_socket(), read from TCP/IP task.
static netconn_callback s_original_callback = NULL;
#ifdef ESPHOME_USE_OTA
// ESPHOME_USE_OTA (not USE_OTA): this .c file can't include defines.h — macros.h →
// Arduino.h would break the C compile. ota/__init__.py emits -DESPHOME_USE_OTA.
#ifdef USE_OTA_PLATFORM_ESPHOME
// This .c file can't include defines.h — macros.h → Arduino.h would break the C
// compile. esphome/ota/__init__.py emits -DUSE_OTA_PLATFORM_ESPHOME via
// cg.add_build_flag so both this TU and C++ call sites can see the same name.
static struct netconn *s_ota_listener_conn = NULL;
extern void esphome_wake_ota_component_any_context(void); // trampoline in application.cpp
extern void esphome_wake_ota_component_any_context(void); // defined in ota_esphome.cpp
void esphome_fast_select_set_ota_listener_sock(struct lwip_sock *sock) {
s_ota_listener_conn = (sock != NULL) ? sock->conn : NULL;
@@ -184,7 +185,7 @@ static void esphome_socket_event_callback(struct netconn *conn, enum netconn_evt
// (rcvevent++ with a NULL pbuf or error in recvmbox), so error conditions
// already wake the main loop through the RCVPLUS path.
if (evt == NETCONN_EVT_RCVPLUS) {
#ifdef ESPHOME_USE_OTA
#ifdef USE_OTA_PLATFORM_ESPHOME
// Mark OTA pending-enable only for events on its listen socket. MUST happen
// before xTaskNotifyGive so the flags are visible when the main task wakes.
if (conn == s_ota_listener_conn) {