From 6e6786451061b190bdf5a8ade02ac113ffd30c05 Mon Sep 17 00:00:00 2001 From: Farmer-shin Date: Sat, 11 Apr 2026 13:27:25 +0200 Subject: [PATCH 1/3] [epaper_spi] Add Waveshare 3.97inch E-Paper Display (#15466) --- esphome/components/epaper_spi/models/ssd1677.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/epaper_spi/models/ssd1677.py b/esphome/components/epaper_spi/models/ssd1677.py index f7e012f162..bad33a6a02 100644 --- a/esphome/components/epaper_spi/models/ssd1677.py +++ b/esphome/components/epaper_spi/models/ssd1677.py @@ -43,3 +43,11 @@ wave_4_26.extend( }, }, ) + + +ssd1677.extend( + "waveshare-3.97in", + width=800, + height=480, + mirror_x=True, +) From e67fac704c7512538c72e7eebdd8399344179ac5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Apr 2026 08:17:49 -1000 Subject: [PATCH 2/3] [esphome.ota] Move OTA wake pointer out of Application into ota_esphome.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- esphome/components/esphome/ota/__init__.py | 4 ++++ esphome/components/esphome/ota/ota_esphome.cpp | 18 +++++++++++++++++- esphome/components/ota/__init__.py | 3 --- .../components/socket/lwip_raw_tcp_impl.cpp | 14 +++++++++----- esphome/core/application.cpp | 11 ----------- esphome/core/application.h | 15 --------------- esphome/core/lwip_fast_select.c | 11 ++++++----- 7 files changed, 36 insertions(+), 40 deletions(-) diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index c8cb9920ba..de2e1e4aad 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -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) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 041aa4bbf9..39275d5fcd 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -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. diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index e0de31f239..8f31eb5cdd 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -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: diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 04605f1e55..0022809dc1 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -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(); diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c847c173f1..cd75859880 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -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(); diff --git a/esphome/core/application.h b/esphome/core/application.h index 8c9789774b..6b2969b490 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -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 diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index f5cfe7f0fc..e29ea5684f 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -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) { From 30de0c17abe522dbff783c439605fe9985ebf11c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Apr 2026 08:21:11 -1000 Subject: [PATCH 3/3] [esphome.ota] Trim verbose comments from simplification --- esphome/components/esphome/ota/__init__.py | 4 +--- esphome/components/esphome/ota/ota_esphome.cpp | 9 ++------- esphome/components/socket/lwip_raw_tcp_impl.cpp | 6 +----- esphome/core/lwip_fast_select.c | 5 +---- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index de2e1e4aad..5d35910fbd 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -155,9 +155,7 @@ 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. + # Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it. cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME") await cg.register_component(var, config) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 39275d5fcd..47f661a8ea 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -31,16 +31,11 @@ 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_). +// Single-instance pointer — multi-port configs are rejected in final_validate. // 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. +// Called from any context (LwIP TCP/IP task, RP2040 user-IRQ). extern "C" void esphome_wake_ota_component_any_context() { if (global_esphome_ota_component != nullptr) { global_esphome_ota_component->enable_loop_soon_any_context(); diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 0022809dc1..c6692b0165 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -12,8 +12,6 @@ #include "esphome/core/log.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 @@ -861,9 +859,7 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) { tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn); LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_); #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 (only fires - // for completed handshakes to whichever listener owns newpcb). + // Must run before wake_loop_any_context() so flags are visible when the main task wakes. esphome_wake_ota_component_any_context(); #endif // Wake the main loop immediately so it can accept the new connection. diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index e29ea5684f..36000d4e77 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -158,11 +158,8 @@ _Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVEN static netconn_callback s_original_callback = NULL; #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); // defined in ota_esphome.cpp +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;