From e372440ed257674e7870f1e4578c5c9953d3608e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:25:26 -1000 Subject: [PATCH 01/45] [core] Move wake_loop out of socket component into core Wake primitives (esp_schedule, __sev, FreeRTOS task notifications) are platform SDK functions that don't depend on the socket/network stack. Move them to esphome/core/wake.h/.cpp so they work unconditionally on all platforms. This eliminates the require_wake_loop_threadsafe() boilerplate, the USE_WAKE_LOOP_THREADSAFE define, and the socket AUTO_LOAD entries that 12 components needed just to use wake. wake_loop_threadsafe() and wake_loop_any_context() now just work on every platform without opt-in. --- esphome/components/esp32_ble/__init__.py | 6 - esphome/components/esp32_ble/ble.cpp | 6 - esphome/components/esp32_camera/__init__.py | 5 +- .../components/esp32_camera/esp32_camera.cpp | 2 - esphome/components/espnow/__init__.py | 8 +- .../components/espnow/espnow_component.cpp | 8 +- .../components/micro_wake_word/__init__.py | 8 +- .../micro_wake_word/micro_wake_word.cpp | 2 - esphome/components/mixer/speaker/__init__.py | 5 +- .../mixer/speaker/mixer_speaker.cpp | 4 - esphome/components/mqtt/__init__.py | 6 - .../components/mqtt/mqtt_backend_esp32.cpp | 4 +- .../components/resampler/speaker/__init__.py | 5 +- .../resampler/speaker/resampler_speaker.cpp | 2 - esphome/components/socket/__init__.py | 45 ++----- .../components/socket/lwip_raw_tcp_impl.cpp | 9 +- esphome/components/uart/__init__.py | 14 -- .../components/usb_cdc_acm/usb_cdc_acm.cpp | 6 - esphome/components/usb_host/__init__.py | 8 +- .../components/usb_host/usb_host_client.cpp | 4 +- esphome/components/usb_uart/__init__.py | 7 +- esphome/components/usb_uart/usb_uart.cpp | 4 +- esphome/core/application.cpp | 24 ++-- esphome/core/application.h | 74 ++++------ esphome/core/component.cpp | 6 +- esphome/core/defines.h | 2 +- esphome/core/wake.cpp | 87 ++++++++++++ esphome/core/wake.h | 99 ++++++++++++++ .../socket/test_wake_loop_threadsafe.py | 127 ------------------ 29 files changed, 253 insertions(+), 334 deletions(-) create mode 100644 esphome/core/wake.cpp create mode 100644 esphome/core/wake.h delete mode 100644 tests/components/socket/test_wake_loop_threadsafe.py diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 2e5e3587536..974611c9b18 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,7 +7,6 @@ from typing import Any from esphome import automation import esphome.codegen as cg -from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32.const import VARIANT_ESP32C2 import esphome.config_validation as cv @@ -592,11 +591,6 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - # BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks - # This enables low-latency (~12μs) BLE event processing instead of waiting for - # select() timeout (0-16ms). The wake socket is shared across all components. - socket.require_wake_loop_threadsafe() - # Define max connections for use in C++ code (e.g., ble_server.h) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 2cd2ec67f7a..aab51c2356a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -594,9 +594,7 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); // Wake up main loop to process security event immediately -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif return; // Ignore these GAP events as they are not relevant for our use case @@ -617,9 +615,7 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat esp_ble_gatts_cb_param_t *param) { enqueue_ble_event(event, gatts_if, param); // Wake up main loop to process GATT event immediately -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } #endif @@ -628,9 +624,7 @@ void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gat esp_ble_gattc_cb_param_t *param) { enqueue_ble_event(event, gattc_if, param); // Wake up main loop to process GATT event immediately -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } #endif diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 66af321e4e2..5165956806f 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,7 +2,7 @@ import logging from esphome import automation, pins import esphome.codegen as cg -from esphome.components import i2c, socket +from esphome.components import i2c from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv @@ -29,7 +29,7 @@ from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) -AUTO_LOAD = ["camera", "socket"] +AUTO_LOAD = ["camera"] DEPENDENCIES = ["esp32"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") @@ -370,7 +370,6 @@ SETTERS = { async def to_code(config): cg.add_define("USE_CAMERA") - socket.require_wake_loop_threadsafe() var = cg.new_Pvariable(config[CONF_ID]) await setup_entity(var, config, "camera") await cg.register_component(var, config) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 085feb8c8a7..a7546476d89 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -521,11 +521,9 @@ void ESP32Camera::framebuffer_task(void *pv) { camera_fb_t *framebuffer = esp_camera_fb_get(); xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); // Only wake the main loop if there's a pending request to consume the frame -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (that->has_requested_image_()) { App.wake_loop_threadsafe(); } -#endif // return is no-op for config with 1 fb xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); esp_camera_fb_return(framebuffer); diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 00703bc2284..1c8d262810d 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -1,6 +1,6 @@ from esphome import automation, core import esphome.codegen as cg -from esphome.components import socket, wifi +from esphome.components import wifi from esphome.components.udp import CONF_ON_RECEIVE import esphome.config_validation as cv from esphome.const import ( @@ -17,7 +17,7 @@ from esphome.core import HexInt from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] -AUTO_LOAD = ["socket"] + byte_vector = cg.std_vector.template(cg.uint8) peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) @@ -124,10 +124,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks - # This enables low-latency event processing instead of waiting for select() timeout - socket.require_wake_loop_threadsafe() - cg.add_define("USE_ESPNOW") if wifi_channel := config.get(CONF_CHANNEL): cg.add(var.set_wifi_channel(wifi_channel)) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 0dc0f12e7e6..282287ca83d 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -92,10 +92,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. - // Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process ESP-NOW send event App.wake_loop_threadsafe(); -#endif } void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { @@ -115,10 +113,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. - // Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process ESP-NOW receive event App.wake_loop_threadsafe(); -#endif } ESPNowComponent::ESPNowComponent() { global_esp_now = this; } diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 372eb4c3b00..fae48630b55 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone, ota, socket +from esphome.components import esp32, microphone, ota import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -32,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] -AUTO_LOAD = ["socket"] + DOMAIN = "micro_wake_word" @@ -444,10 +444,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - # Enable wake_loop_threadsafe() for low-latency wake word detection - # The inference task queues detection events that need immediate processing - socket.require_wake_loop_threadsafe() - mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE]) cg.add(var.set_microphone_source(mic_source)) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index b93bf1b556c..f1aac875f13 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -431,9 +431,7 @@ void MicroWakeWord::process_probabilities_() { xQueueSend(this->detection_queue_, &wake_word_state, portMAX_DELAY); // Wake main loop immediately to process wake word detection -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif model->reset_probabilities(); #ifdef USE_MICRO_WAKE_WORD_VAD diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index 63b419cc98e..59a80d9297c 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import audio, esp32, socket, speaker +from esphome.components import audio, esp32, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -111,9 +111,6 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - # Enable wake_loop_threadsafe for immediate command processing from other tasks - socket.require_wake_loop_threadsafe() - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 0fabc68c705..741239a2dd9 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -245,11 +245,9 @@ void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { uint32_t event_bits = xEventGroupGetBits(this->event_group_); if (!(event_bits & command_bit)) { xEventGroupSetBits(this->event_group_, command_bit); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (wake_loop) { App.wake_loop_threadsafe(); } -#endif } } @@ -533,9 +531,7 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) { if (!(event_bits & MIXER_TASK_COMMAND_START)) { // Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } return ESP_OK; diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 817f99375ea..33a88c49cc7 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -69,9 +69,6 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(): if CORE.is_esp8266 or CORE.is_libretiny: return ["async_tcp", "json"] - # ESP32 needs socket for wake_loop_threadsafe() - if CORE.is_esp32: - return ["json", "socket"] return ["json"] @@ -348,10 +345,7 @@ async def to_code(config): # https://github.com/heman/async-mqtt-client/blob/master/library.json cg.add_library("heman/AsyncMqttClient-esphome", "2.0.0") - # MQTT on ESP32 uses wake_loop_threadsafe() to wake the main loop from the MQTT event handler - # This enables low-latency MQTT event processing instead of waiting for select() timeout if CORE.is_esp32: - socket.require_wake_loop_threadsafe() # Re-enable ESP-IDF's mqtt component (excluded by default to save compile time) # IDF 6.0 moved esp-mqtt to an external component if idf_version() >= cv.Version(6, 0, 0): diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index ab067c44182..499a3307306 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -202,10 +202,8 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b // allocate() returned non-null, the queue cannot be full. instance->mqtt_event_queue_.push(event); - // Wake main loop immediately to process MQTT event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process MQTT event App.wake_loop_threadsafe(); -#endif } } diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index 4e4705a8892..3134cf7646a 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import audio, esp32, socket, speaker +from esphome.components import audio, esp32, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BITS_PER_SAMPLE, @@ -77,9 +77,6 @@ FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility async def to_code(config): - # Enable wake_loop_threadsafe for immediate command processing from other tasks - socket.require_wake_loop_threadsafe() - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await speaker.register_speaker(var, config) diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index b737a2d39a0..3b50353ddc8 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -245,11 +245,9 @@ void ResamplerSpeaker::send_command_(uint32_t command_bit, bool wake_loop) { uint32_t event_bits = xEventGroupGetBits(this->event_group_); if (!(event_bits & command_bit)) { xEventGroupSetBits(this->event_group_, command_bit); -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) if (wake_loop) { App.wake_loop_threadsafe(); } -#endif } } diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 08cf3ea33c3..cbe61cb872e 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -31,9 +31,6 @@ MIN_UDP_SOCKETS = 6 # Minimum listening sockets — at least api + ota baseline. MIN_TCP_LISTEN_SOCKETS = 2 -# Wake loop threadsafe support tracking -KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" - class SocketType(StrEnum): TCP = "tcp" @@ -123,37 +120,17 @@ def get_socket_counts() -> SocketCounts: def require_wake_loop_threadsafe() -> None: - """Mark that wake_loop_threadsafe support is required by a component. + """Deprecated: wake loop support is now always available on all platforms. - Call this from components that need to wake the main event loop from background threads. - This enables the shared UDP loopback socket mechanism (~208 bytes RAM). - The socket is shared across all components that use this feature. - - This call is a no-op if networking is not enabled in the configuration. - - IMPORTANT: This is for background thread context only, NOT ISR context. - Socket operations are not safe to call from ISR handlers. - - On ESP32, FreeRTOS task notifications are used instead (no socket needed). - - Example: - from esphome.components import socket - - async def to_code(config): - socket.require_wake_loop_threadsafe() + This function is a no-op kept for backward compatibility with external components. + Remove before 2026.12.0. """ - - # Only set up once (idempotent - multiple components can call this) - if CORE.has_networking and not CORE.data.get( - KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False - ): - CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - cg.add_define("USE_WAKE_LOOP_THREADSAFE") - if not CORE.is_esp32 and not CORE.is_libretiny: - # Only platforms without fast select need a UDP socket for wake - # notifications. ESP32 and LibreTiny use FreeRTOS task notifications - # instead (no socket needed). - consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) + # Remove before 2026.12.0 + _LOGGER.warning( + "require_wake_loop_threadsafe() is deprecated and no longer needed. " + "Wake loop support is now always available. Remove this call. " + "This will be removed in 2026.12.0." + ) CONFIG_SCHEMA = cv.Schema( @@ -193,6 +170,10 @@ async def to_code(config): # Only when not using lwip_tcp, which does not provide select() support. if (CORE.is_esp32 or CORE.is_libretiny) and impl != IMPLEMENTATION_LWIP_TCP: cg.add_build_flag("-DUSE_LWIP_FAST_SELECT") + elif impl != IMPLEMENTATION_LWIP_TCP: + # Platforms with select() but without fast select (host) need a UDP + # loopback socket for wake_loop_threadsafe(). + consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) def FILTER_SOURCE_FILES() -> list[str]: diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 3bcbd880850..c320a6714ab 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -8,6 +8,7 @@ #include #include "esphome/core/helpers.h" +#include "esphome/core/wake.h" #include "esphome/core/log.h" #ifdef USE_ESP8266 @@ -42,7 +43,8 @@ void socket_delay(uint32_t ms) { void IRAM_ATTR socket_wake() { s_socket_woke = true; - esp_schedule(); + // Inline impl — this is IRAM_ATTR so the inlined code stays in IRAM + esphome::wake_loop_impl_(); } #elif defined(USE_RP2040) // RP2040 (non-FreeRTOS) socket wake using hardware WFE/SEV instructions. @@ -109,9 +111,8 @@ void socket_delay(uint32_t ms) { // callbacks via pendsv (not hard IRQ), so they execute from flash safely. void socket_wake() { s_socket_woke = true; - // Wake the main loop from __wfe() sleep. __sev() is a global event that - // wakes any core sleeping in __wfe(). This is ISR-safe. - __sev(); + // Also set core wake flag so Application::wakeable_delay_() breaks out + esphome::wake_loop_any_context(); } #endif diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 83649cc2092..70752287436 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -42,16 +42,6 @@ CODEOWNERS = ["@esphome/core"] DOMAIN = "uart" -def AUTO_LOAD() -> list[str]: - """Ideally, we would only auto-load socket only when wake_loop_on_rx is requested; - however, AUTO_LOAD is examined before wake_loop_on_rx is set, so instead, since ESP32 - always uses socket select support in the main app, we'll just ensure it's loaded here. - """ - if CORE.is_esp32: - return ["socket"] - return [] - - uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -527,10 +517,6 @@ async def final_step(): # Wake-on-RX is essentially free on ESP32 (just an ISR function pointer # registration) — enable by default to reduce RX buffer overflow risk # by waking the main loop immediately when data arrives. - # Requires networking for the wake_loop_isrsafe() infrastructure. - from esphome.components import socket - - socket.require_wake_loop_threadsafe() cg.add_define("USE_UART_WAKE_LOOP_ON_RX") diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp index 253626f0a3e..40f7f2e28bb 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -29,10 +29,7 @@ void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) { // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. this->event_queue_.push(event); - -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, @@ -53,10 +50,7 @@ void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_ // Push always succeeds: pool is sized to queue capacity (SIZE-1), so if // allocate() returned non-null, the queue cannot be full. this->event_queue_.push(event); - -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); -#endif } void USBCDCACMInstance::process_events_() { diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index 5eb0371e5c5..338bd8d5728 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg -from esphome.components import socket from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -14,7 +13,7 @@ from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component from esphome.types import ConfigType -AUTO_LOAD = ["bytebuffer", "socket"] +AUTO_LOAD = ["bytebuffer"] CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["esp32"] usb_host_ns = cg.esphome_ns.namespace("usb_host") @@ -76,11 +75,6 @@ async def to_code(config: ConfigType) -> None: max_requests = config[CONF_MAX_TRANSFER_REQUESTS] cg.add_define("USB_HOST_MAX_REQUESTS", max_requests) - # USB uses the socket wake_loop_threadsafe() mechanism to wake the main loop from USB task - # This enables low-latency (~12μs) USB event processing instead of waiting for - # select() timeout (0-16ms). The wake socket is shared across all components. - socket.require_wake_loop_threadsafe() - var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for device in config.get(CONF_DEVICES) or (): diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 18d938344c7..c34c7ef67d9 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -200,10 +200,8 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Re-enable component loop to process the queued event client->enable_loop_soon_any_context(); - // Wake main loop immediately to process USB event instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process USB event App.wake_loop_threadsafe(); -#endif } void USBClient::setup() { usb_host_client_config_t config{.is_synchronous = false, diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index 2d85723d722..0e8994a3ed2 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg -from esphome.components import socket from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS from esphome.components.uart import CONF_DEBUG_PREFIX, CONF_FLUSH_TIMEOUT, UARTComponent from esphome.components.usb_host import register_usb_client, usb_device_schema @@ -14,7 +13,7 @@ from esphome.const import ( ) from esphome.cpp_types import Component -AUTO_LOAD = ["uart", "usb_host", "bytebuffer", "socket"] +AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] CODEOWNERS = ["@clydebarrow"] usb_uart_ns = cg.esphome_ns.namespace("usb_uart") @@ -117,10 +116,6 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): - # Enable wake_loop_threadsafe for low-latency USB data processing - # The USB task queues data events that need immediate processing - socket.require_wake_loop_threadsafe() - for device in config: var = await register_usb_client(device) for index, channel in enumerate(device[CONF_CHANNELS]): diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 0b8589f6713..30ec61fdc4b 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -325,10 +325,8 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Re-enable component loop to process the queued data this->enable_loop_soon_any_context(); - // Wake main loop immediately to process USB data instead of waiting for select() timeout -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Wake main loop immediately to process USB data App.wake_loop_threadsafe(); -#endif } // On success, restart input immediately from USB task for performance diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 5cb8a5bb244..26486aa3a18 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -134,7 +134,7 @@ void Application::setup() { // when USE_LWIP_FAST_SELECT is enabled (ESP32 and LibreTiny). esphome_lwip_fast_select_init(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // Set up wake socket for waking main loop from tasks (platforms without fast select only) this->setup_wake_loop_threadsafe_(); #endif @@ -618,14 +618,9 @@ alignas(Application) char app_storage[sizeof(Application)] asm( #undef ESPHOME_STRINGIFY_ #undef ESPHOME_STRINGIFY_IMPL_ -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) - -#ifdef USE_LWIP_FAST_SELECT -void Application::wake_loop_threadsafe() { - // Direct FreeRTOS task notification — <1 us, task context only (NOT ISR-safe) - esphome_lwip_wake_main_loop(); -} -#else // !USE_LWIP_FAST_SELECT +// Host platform wake_loop_threadsafe() and setup — needs wake_socket_fd_ +// ESP32/LibreTiny/ESP8266/RP2040 implementations are in wake.cpp +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications @@ -681,21 +676,18 @@ void Application::setup_wake_loop_threadsafe_() { } } -void Application::wake_loop_threadsafe() { - // Called from FreeRTOS task context when events need immediate processing +void wake_loop_threadsafe() { // Wakes up lwip_select() in main loop by writing to connected loopback socket - if (this->wake_socket_fd_ >= 0) { + if (App.wake_socket_fd_ >= 0) { const char dummy = 1; // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway // No error checking needed: we control both ends of this loopback socket. // This is safe to call from FreeRTOS tasks - send() is thread-safe in lwip // Socket is already connected to loopback address, so send() is faster than sendto() - lwip_send(this->wake_socket_fd_, &dummy, 1, 0); + lwip_send(App.wake_socket_fd_, &dummy, 1, 0); } } -#endif // USE_LWIP_FAST_SELECT - -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#endif // host wake_loop_threadsafe void Application::get_build_time_string(std::span buffer) { ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size()); diff --git a/esphome/core/application.h b/esphome/core/application.h index 6cc61bc9547..168596fc837 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -36,20 +36,13 @@ #endif #else #include -#ifdef USE_WAKE_LOOP_THREADSAFE #include #endif -#endif #endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_RUNTIME_STATS #include "esphome/components/runtime_stats/runtime_stats.h" #endif -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) -namespace esphome::socket { -void socket_wake(); // NOLINT(readability-redundant-declaration) -void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) -} // namespace esphome::socket -#endif +#include "esphome/core/wake.h" #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -558,38 +551,18 @@ class Application { void unregister_socket_fd(int fd); #endif -#ifdef USE_WAKE_LOOP_THREADSAFE - /// Wake the main event loop from another FreeRTOS task. - /// Thread-safe, but must only be called from task context (NOT ISR-safe). - /// On ESP32: uses xTaskNotifyGive (<1 us) - /// On other platforms: uses UDP loopback socket - void wake_loop_threadsafe(); -#endif + /// Wake the main event loop from another thread or callback. + /// @see esphome::wake_loop_threadsafe() in wake.h for platform details. + void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); } #ifdef USE_LWIP_FAST_SELECT - /// Wake the main event loop from an ISR. - /// Uses vTaskNotifyGiveFromISR() — <1 us, ISR-safe. - /// Only available on platforms with fast select (ESP32, LibreTiny). - /// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. - static void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { - esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); - } - -#ifdef USE_ESP32 - /// Wake the main event loop from any context (ISR, thread, or main loop). - /// Detects the calling context and uses the appropriate FreeRTOS API. - static void IRAM_ATTR wake_loop_any_context() { esphome_lwip_wake_main_loop_any_context(); } -#endif + /// Wake from ISR (ESP32/LibreTiny only). Delegates to esphome::wake_loop_isrsafe(). + static void wake_loop_isrsafe(int *px) { esphome::wake_loop_isrsafe(px); } #endif -#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) - /// Wake the main event loop from any context (ISR, thread, or main loop). - /// Sets the socket wake flag and calls esp_schedule() to exit esp_delay() early. - static void IRAM_ATTR wake_loop_any_context() { socket::socket_wake(); } -#elif defined(USE_RP2040) && defined(USE_SOCKET_IMPL_LWIP_TCP) - /// Wake the main event loop from any context. - /// Sets the socket wake flag and calls __sev() to exit __wfe() early. - static void wake_loop_any_context() { socket::socket_wake(); } +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) + /// Wake from any context (ISR, thread, callback). Delegates to esphome::wake_loop_any_context(). + static void wake_loop_any_context() { esphome::wake_loop_any_context(); } #endif protected: @@ -602,6 +575,9 @@ class Application { #endif friend void ::setup(); friend void ::original_setup(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) + friend void wake_loop_threadsafe(); // Host platform accesses wake_socket_fd_ +#endif #if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } @@ -655,7 +631,7 @@ class Application { inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) void setup_wake_loop_threadsafe_(); // Create wake notification socket inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) #endif @@ -689,7 +665,7 @@ class Application { std::vector socket_fds_; // Vector of all monitored socket file descriptors #endif #ifdef USE_SOCKET_SELECT_SUPPORT -#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#if !defined(USE_LWIP_FAST_SELECT) int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks #endif #endif @@ -815,7 +791,7 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -837,10 +813,10 @@ inline void Application::drain_wake_notifications_() { } } } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -953,17 +929,15 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay } // Sleep with instant wake via FreeRTOS task notification. - // Woken by: callback wrapper (socket data arrives), wake_loop_threadsafe() (other tasks), or timeout. - // Without USE_WAKE_LOOP_THREADSAFE, only hooked socket callbacks wake the task — - // background tasks won't call wake, so this degrades to a pure timeout (same as old select path). + // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); -#elif (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) - // No select support but can wake on socket activity - // ESP8266: via esp_schedule() - // RP2040: via __sev()/__wfe() hardware sleep/wake - socket::socket_delay(delay_ms); +#elif defined(USE_ESP8266) || defined(USE_RP2040) + // Wakeable delay — broken early by wake_loop_any_context() / wake_loop_threadsafe() + // ESP8266: esp_delay() with wake flag callback + // RP2040: hardware timer + __wfe()/__sev() + esphome::wakeable_delay(delay_ms); #else - // No select support, use regular delay + // No wake mechanism available, use regular delay delay(delay_ms); #endif } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 0f68f0c8e02..747ade87e83 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -311,13 +311,9 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if (defined(USE_LWIP_FAST_SELECT) && defined(USE_ESP32)) || \ - ((defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP)) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) // Wake the main loop from sleep. Without this, the main loop would not // wake until the select/delay timeout expires (~16ms). - // ESP32: uses xPortInIsrContext() to choose the correct FreeRTOS notify API. - // ESP8266: sets socket wake flag and calls esp_schedule() to exit esp_delay() early. - // RP2040: sets socket wake flag and calls __sev() to exit __wfe() early. Application::wake_loop_any_context(); #endif } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index faa8c6d4b0e..0bd9418b9d0 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -253,7 +253,7 @@ #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT #define USE_LWIP_FAST_SELECT -#define USE_WAKE_LOOP_THREADSAFE + #define USE_SPEAKER #define USE_SPEAKER_MEDIA_PLAYER_ON_OFF #define USE_SPI diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp new file mode 100644 index 00000000000..6a8edd32851 --- /dev/null +++ b/esphome/core/wake.cpp @@ -0,0 +1,87 @@ +#include "esphome/core/wake.h" +#include "esphome/core/hal.h" + +#ifdef USE_ESP8266 +#include +#elif defined(USE_RP2040) +#include +#include +#endif + +namespace esphome { + +// === ESP32/LibreTiny — IRAM_ATTR entry points (inline impls in wake.h) === +#ifdef USE_LWIP_FAST_SELECT + +void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { + wake_loop_isrsafe_inline_(px_higher_priority_task_woken); +} + +#ifdef USE_ESP32 +void IRAM_ATTR wake_loop_any_context() { wake_loop_any_context_inline_(); } +#endif + +// === ESP8266 — IRAM_ATTR entry point + wakeable_delay === +#elif defined(USE_ESP8266) + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +volatile bool g_main_loop_woke = false; + +void IRAM_ATTR wake_loop_any_context() { wake_loop_impl_(); } + +void wakeable_delay(uint32_t ms) { + if (ms == 0) { + delay(0); + return; + } + g_main_loop_woke = false; + esp_delay(ms, []() { return !g_main_loop_woke; }); +} + +// === RP2040 — wakeable_delay (wake functions are inline in wake.h) === +#elif defined(USE_RP2040) + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +volatile bool g_main_loop_woke = false; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_delay_expired = false; + +static int64_t alarm_callback_(alarm_id_t id, void *user_data) { + (void) id; + (void) user_data; + s_delay_expired = true; + __sev(); // Wake from __wfe() — timeout expired. + return 0; // One-shot +} + +void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + // If a wake was already signalled, consume it and return immediately + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + s_delay_expired = false; + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + // Sleep until woken by either the timer alarm or wake_loop_any_context()/wake_loop_threadsafe() + while (!g_main_loop_woke && !s_delay_expired) { + __wfe(); + } + if (!s_delay_expired) + cancel_alarm(alarm); + g_main_loop_woke = false; +} + +#endif + +// Host platform wake_loop_threadsafe() is in application.cpp (needs App.wake_socket_fd_) + +} // namespace esphome diff --git a/esphome/core/wake.h b/esphome/core/wake.h new file mode 100644 index 00000000000..d41d173c47c --- /dev/null +++ b/esphome/core/wake.h @@ -0,0 +1,99 @@ +#pragma once + +/// @file wake.h +/// Platform-specific main loop wake primitives. +/// +/// All functions are always available on all platforms — no opt-in needed. +/// - wake_loop_any_context(): ISR + thread + callback safe +/// - wake_loop_isrsafe(): ISR only, ESP32/LibreTiny +/// - wake_loop_threadsafe(): thread/callback safe +/// - wakeable_delay(): sleeps until timeout or wake + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +#ifdef USE_LWIP_FAST_SELECT +#include "esphome/core/lwip_fast_select.h" +#endif +#ifdef USE_ESP8266 +#include +#elif defined(USE_RP2040) +#include +#endif + +namespace esphome { + +// === Wake flag for ESP8266/RP2040 === +// Checked by wakeable_delay() to exit early. Set by wake functions. +#if defined(USE_ESP8266) || defined(USE_RP2040) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern volatile bool g_main_loop_woke; +#endif + +// === ESP32/LibreTiny (FreeRTOS) === +#ifdef USE_LWIP_FAST_SELECT + +/// Inline implementation — callers in IRAM get it inlined, keeping it IRAM-safe. +/// IRAM_ATTR entry points in wake.cpp exist for callers that aren't themselves IRAM. +inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe_inline_(int *px_higher_priority_task_woken) { + esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); +} + +/// IRAM_ATTR entry point — defined in wake.cpp. +void wake_loop_isrsafe(int *px_higher_priority_task_woken); + +#ifdef USE_ESP32 +inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { esphome_lwip_wake_main_loop_any_context(); } + +/// IRAM_ATTR entry point — defined in wake.cpp. +void wake_loop_any_context(); +#else +/// LibreTiny: no working IRAM_ATTR — just use threadsafe version. +inline void wake_loop_any_context() { esphome_lwip_wake_main_loop(); } +#endif + +inline void wake_loop_threadsafe() { esphome_lwip_wake_main_loop(); } + +// === ESP8266 === +#elif defined(USE_ESP8266) + +/// Inline implementation — IRAM callers inline this directly. +inline void ESPHOME_ALWAYS_INLINE wake_loop_impl_() { + g_main_loop_woke = true; + esp_schedule(); +} + +/// IRAM_ATTR entry point for ISR callers — defined in wake.cpp. +void wake_loop_any_context(); + +/// Non-ISR: always inline. +inline void wake_loop_threadsafe() { wake_loop_impl_(); } + +/// Wakeable delay for ESP8266. Defined in wake.cpp. +void wakeable_delay(uint32_t ms); + +// === RP2040 === +#elif defined(USE_RP2040) + +inline void wake_loop_any_context() { + g_main_loop_woke = true; + __sev(); +} + +inline void wake_loop_threadsafe() { + g_main_loop_woke = true; + __sev(); +} + +/// Delay that can be woken early. Uses hardware timer + __wfe()/__sev(). Defined in wake.cpp. +void wakeable_delay(uint32_t ms); + +// === Host (UDP loopback socket) === +#else + +/// Host platform: wakes select() via UDP loopback socket. Defined in application.cpp. +void wake_loop_threadsafe(); + +#endif + +} // namespace esphome diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py deleted file mode 100644 index 0434b3e1b54..00000000000 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ /dev/null @@ -1,127 +0,0 @@ -import pytest - -from esphome.components import socket -from esphome.const import ( - KEY_CORE, - KEY_TARGET_PLATFORM, - PLATFORM_BK72XX, - PLATFORM_ESP32, - PLATFORM_ESP8266, - PLATFORM_LN882X, - PLATFORM_RTL87XX, -) -from esphome.core import CORE - - -def _setup_platform(platform=PLATFORM_ESP8266) -> None: - """Set up CORE.data with a platform for testing.""" - CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform} - - -def test_require_wake_loop_threadsafe__first_call() -> None: - """Test that first call sets up define and consumes socket.""" - _setup_platform() - CORE.config = {"wifi": True} - socket.require_wake_loop_threadsafe() - - # Verify CORE.data was updated - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__idempotent() -> None: - """Test that subsequent calls are idempotent.""" - # Set up initial state as if already called - CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - CORE.config = {"ethernet": True} - - # Call again - should not raise or fail - socket.require_wake_loop_threadsafe() - - # Verify state is still True - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Define should not be added since flag was already True - assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__multiple_calls() -> None: - """Test that multiple calls only set up once.""" - _setup_platform() - # Call three times - CORE.config = {"openthread": True} - socket.require_wake_loop_threadsafe() - socket.require_wake_loop_threadsafe() - socket.require_wake_loop_threadsafe() - - # Verify CORE.data was set - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added (only once, but we can just check it exists) - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__no_networking() -> None: - """Test that wake loop is NOT configured when no networking is configured.""" - # Set up config without any networking components - CORE.config = {"esphome": {"name": "test"}, "logger": {}} - - # Call require_wake_loop_threadsafe - socket.require_wake_loop_threadsafe() - - # Verify CORE.data flag was NOT set (since has_networking returns False) - assert socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED not in CORE.data - - # Verify the define was NOT added - assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - -def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -> None: - """Test that no socket is consumed when no networking is configured.""" - # Set up config without any networking components - CORE.config = {"logger": {}} - - # Track initial socket consumer state - initial_udp = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - - # Call require_wake_loop_threadsafe - socket.require_wake_loop_threadsafe() - - # Verify no socket was consumed - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - assert "socket.wake_loop_threadsafe" not in udp_consumers - assert udp_consumers == initial_udp - - -@pytest.mark.parametrize( - "platform", - [PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LN882X], -) -def test_require_wake_loop_threadsafe__fast_select_no_udp_socket( - platform: str, -) -> None: - """Test that fast select platforms use task notifications instead of UDP socket.""" - _setup_platform(platform) - CORE.config = {"wifi": True} - socket.require_wake_loop_threadsafe() - - # Verify the define was added - assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) - - # Verify no UDP socket was consumed (fast select platforms use FreeRTOS task notifications) - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - assert "socket.wake_loop_threadsafe" not in udp_consumers - - -def test_require_wake_loop_threadsafe__non_fast_select_consumes_udp_socket() -> None: - """Test that platforms without fast select consume a UDP socket for wake notifications.""" - _setup_platform(PLATFORM_ESP8266) - CORE.config = {"wifi": True} - socket.require_wake_loop_threadsafe() - - # Verify UDP socket was consumed - udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {}) - assert udp_consumers.get("socket.wake_loop_threadsafe") == 1 From 04b40f8f15a3f36d046ed47381911eac49cc78a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:32:49 -1000 Subject: [PATCH 02/45] Clean up comments in application.h --- esphome/core/application.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 168596fc837..e67ed00b7c2 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -556,12 +556,12 @@ class Application { void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); } #ifdef USE_LWIP_FAST_SELECT - /// Wake from ISR (ESP32/LibreTiny only). Delegates to esphome::wake_loop_isrsafe(). + /// Wake from ISR (ESP32/LibreTiny only). static void wake_loop_isrsafe(int *px) { esphome::wake_loop_isrsafe(px); } #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) - /// Wake from any context (ISR, thread, callback). Delegates to esphome::wake_loop_any_context(). + /// Wake from any context (ISR, thread, callback). static void wake_loop_any_context() { esphome::wake_loop_any_context(); } #endif From 72c7fc25b49b8eff9779b24f3a743870ce7ea638 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:34:49 -1000 Subject: [PATCH 03/45] Restrict wake_loop_isrsafe to ESP32 only --- esphome/core/application.h | 4 ++-- esphome/core/wake.cpp | 16 +++++++++------- esphome/core/wake.h | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index e67ed00b7c2..016b9f217a6 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -555,8 +555,8 @@ class Application { /// @see esphome::wake_loop_threadsafe() in wake.h for platform details. void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); } -#ifdef USE_LWIP_FAST_SELECT - /// Wake from ISR (ESP32/LibreTiny only). +#ifdef USE_ESP32 + /// Wake from ISR (ESP32 only). static void wake_loop_isrsafe(int *px) { esphome::wake_loop_isrsafe(px); } #endif diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index 6a8edd32851..c172a26ab7b 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -10,19 +10,19 @@ namespace esphome { -// === ESP32/LibreTiny — IRAM_ATTR entry points (inline impls in wake.h) === -#ifdef USE_LWIP_FAST_SELECT +// === ESP32 — IRAM_ATTR entry points (inline impls in wake.h) === +#ifdef USE_ESP32 void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { wake_loop_isrsafe_inline_(px_higher_priority_task_woken); } -#ifdef USE_ESP32 void IRAM_ATTR wake_loop_any_context() { wake_loop_any_context_inline_(); } -#endif + +#endif // USE_ESP32 // === ESP8266 — IRAM_ATTR entry point + wakeable_delay === -#elif defined(USE_ESP8266) +#ifdef USE_ESP8266 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) volatile bool g_main_loop_woke = false; @@ -38,8 +38,10 @@ void wakeable_delay(uint32_t ms) { esp_delay(ms, []() { return !g_main_loop_woke; }); } +#endif // USE_ESP8266 + // === RP2040 — wakeable_delay (wake functions are inline in wake.h) === -#elif defined(USE_RP2040) +#ifdef USE_RP2040 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) volatile bool g_main_loop_woke = false; @@ -80,7 +82,7 @@ void wakeable_delay(uint32_t ms) { g_main_loop_woke = false; } -#endif +#endif // USE_RP2040 // Host platform wake_loop_threadsafe() is in application.cpp (needs App.wake_socket_fd_) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index d41d173c47c..ce96aa0efc5 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -33,6 +33,7 @@ extern volatile bool g_main_loop_woke; // === ESP32/LibreTiny (FreeRTOS) === #ifdef USE_LWIP_FAST_SELECT +#ifdef USE_ESP32 /// Inline implementation — callers in IRAM get it inlined, keeping it IRAM-safe. /// IRAM_ATTR entry points in wake.cpp exist for callers that aren't themselves IRAM. inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe_inline_(int *px_higher_priority_task_woken) { @@ -42,7 +43,6 @@ inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe_inline_(int *px_higher_prior /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_isrsafe(int *px_higher_priority_task_woken); -#ifdef USE_ESP32 inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { esphome_lwip_wake_main_loop_any_context(); } /// IRAM_ATTR entry point — defined in wake.cpp. From 91bfa8d4faa6b4d1e4a73c263827e4514d4e658c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:37:35 -1000 Subject: [PATCH 04/45] Make wake_loop_any_context available on all platforms including host --- esphome/core/application.h | 2 -- esphome/core/component.cpp | 2 -- esphome/core/wake.h | 3 +++ 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 016b9f217a6..900c11966dd 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -560,10 +560,8 @@ class Application { static void wake_loop_isrsafe(int *px) { esphome::wake_loop_isrsafe(px); } #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) /// Wake from any context (ISR, thread, callback). static void wake_loop_any_context() { esphome::wake_loop_any_context(); } -#endif protected: friend Component; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 747ade87e83..814295fd208 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -311,11 +311,9 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { // 8. Race condition with main loop is handled by clearing flag before processing this->pending_enable_loop_ = true; App.has_pending_enable_loop_requests_ = true; -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) // Wake the main loop from sleep. Without this, the main loop would not // wake until the select/delay timeout expires (~16ms). Application::wake_loop_any_context(); -#endif } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { diff --git a/esphome/core/wake.h b/esphome/core/wake.h index ce96aa0efc5..185b73b870d 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -94,6 +94,9 @@ void wakeable_delay(uint32_t ms); /// Host platform: wakes select() via UDP loopback socket. Defined in application.cpp. void wake_loop_threadsafe(); +/// Host: no ISR, just use threadsafe version. +inline void wake_loop_any_context() { wake_loop_threadsafe(); } + #endif } // namespace esphome From bfad79f34c37ba38b80e131e64e6f05e6eee10b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:38:27 -1000 Subject: [PATCH 05/45] Replace unreachable delay fallback with #error --- esphome/core/application.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 900c11966dd..66bf189fa9f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -935,8 +935,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay // RP2040: hardware timer + __wfe()/__sev() esphome::wakeable_delay(delay_ms); #else - // No wake mechanism available, use regular delay - delay(delay_ms); +#error "No wakeable delay implementation for this platform" #endif } #endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) From 88d7198fa438b6e6972fe94eac0859585acfb2d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:40:14 -1000 Subject: [PATCH 06/45] Add wakeable_delay for ESP32/LibreTiny using ulTaskNotifyTake --- esphome/core/application.h | 8 ++------ esphome/core/wake.h | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 66bf189fa9f..cc1aba77caa 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -928,14 +928,10 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay // Sleep with instant wake via FreeRTOS task notification. // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delay_ms)); -#elif defined(USE_ESP8266) || defined(USE_RP2040) - // Wakeable delay — broken early by wake_loop_any_context() / wake_loop_threadsafe() - // ESP8266: esp_delay() with wake flag callback - // RP2040: hardware timer + __wfe()/__sev() esphome::wakeable_delay(delay_ms); #else -#error "No wakeable delay implementation for this platform" + // All other platforms: wakeable delay broken early by wake_loop_any_context() / wake_loop_threadsafe() + esphome::wakeable_delay(delay_ms); #endif } #endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 185b73b870d..ae9fc8d3555 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -14,6 +14,13 @@ #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" +#ifdef USE_ESP32 +#include +#include +#else +#include +#include +#endif #endif #ifdef USE_ESP8266 #include @@ -54,6 +61,14 @@ inline void wake_loop_any_context() { esphome_lwip_wake_main_loop(); } inline void wake_loop_threadsafe() { esphome_lwip_wake_main_loop(); } +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms)); +} + // === ESP8266 === #elif defined(USE_ESP8266) From ffb2fe5f10f3d4d162757cf808d98efeb4dd4ef7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:44:45 -1000 Subject: [PATCH 07/45] Stop setting USE_SOCKET_SELECT_SUPPORT on fast-select platforms USE_SOCKET_SELECT_SUPPORT now exclusively means 'host select() fallback'. ESP32/LibreTiny use USE_LWIP_FAST_SELECT instead. This eliminates all 'USE_SOCKET_SELECT_SUPPORT && !USE_LWIP_FAST_SELECT' guards. --- esphome/components/socket/__init__.py | 4 +-- esphome/components/socket/socket.cpp | 2 +- esphome/core/application.cpp | 10 +++---- esphome/core/application.h | 41 +++++++++------------------ esphome/core/defines.h | 2 -- 5 files changed, 22 insertions(+), 37 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index cbe61cb872e..ab4f3269a84 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -161,16 +161,16 @@ async def to_code(config): cg.add_define("USE_SOCKET_IMPL_LWIP_TCP") elif impl == IMPLEMENTATION_LWIP_SOCKETS: cg.add_define("USE_SOCKET_IMPL_LWIP_SOCKETS") - cg.add_define("USE_SOCKET_SELECT_SUPPORT") elif impl == IMPLEMENTATION_BSD_SOCKETS: cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") - cg.add_define("USE_SOCKET_SELECT_SUPPORT") # ESP32 and LibreTiny both have LwIP >= 2.1.3 with lwip_socket_dbg_get_socket() # and FreeRTOS task notifications — enable fast select to bypass lwip_select(). # Only when not using lwip_tcp, which does not provide select() support. if (CORE.is_esp32 or CORE.is_libretiny) and impl != IMPLEMENTATION_LWIP_TCP: cg.add_build_flag("-DUSE_LWIP_FAST_SELECT") elif impl != IMPLEMENTATION_LWIP_TCP: + # Host platform: uses select() syscall for socket monitoring + cg.add_define("USE_SOCKET_SELECT_SUPPORT") # Platforms with select() but without fast select (host) need a UDP # loopback socket for wake_loop_threadsafe(). consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index bfb6ae8e130..4b8a32ed1dc 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -8,7 +8,7 @@ namespace esphome::socket { -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT // Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets). // Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 26486aa3a18..3df82e25a5d 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -128,13 +128,13 @@ void Application::setup() { clear_setup_priority_overrides(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) +#ifdef USE_LWIP_FAST_SELECT // Initialize fast select: saves main loop task handle for xTaskNotifyGive wake. // The fast path (rcvevent reads + ulTaskNotifyTake) is used unconditionally // when USE_LWIP_FAST_SELECT is enabled (ESP32 and LibreTiny). esphome_lwip_fast_select_init(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT // Set up wake socket for waking main loop from tasks (platforms without fast select only) this->setup_wake_loop_threadsafe_(); #endif @@ -547,7 +547,7 @@ void Application::unregister_socket_fd(int fd) { #endif // Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT void Application::yield_with_select_(uint32_t delay_ms) { // Fallback select() path (host platform and any future platforms without fast select). if (!this->socket_fds_.empty()) [[likely]] { @@ -597,7 +597,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { // No sockets registered or select() failed - use regular delay delay(delay_ms); } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#endif // USE_SOCKET_SELECT_SUPPORT // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. @@ -620,7 +620,7 @@ alignas(Application) char app_storage[sizeof(Application)] asm( // Host platform wake_loop_threadsafe() and setup — needs wake_socket_fd_ // ESP32/LibreTiny/ESP8266/RP2040 implementations are in wake.cpp -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications diff --git a/esphome/core/application.h b/esphome/core/application.h index cc1aba77caa..a5bacde9976 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -25,20 +25,9 @@ #endif #ifdef USE_SOCKET_SELECT_SUPPORT -#ifdef USE_LWIP_FAST_SELECT -#include "esphome/core/lwip_fast_select.h" -#ifdef USE_ESP32 -#include -#include -#else -#include -#include -#endif -#else #include #include #endif -#endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_RUNTIME_STATS #include "esphome/components/runtime_stats/runtime_stats.h" #endif @@ -565,7 +554,7 @@ class Application { protected: friend Component; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT friend bool socket::socket_ready_fd(int fd, bool loop_monitored); #endif #ifdef USE_RUNTIME_STATS @@ -573,11 +562,11 @@ class Application { #endif friend void ::setup(); friend void ::original_setup(); -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT friend void wake_loop_threadsafe(); // Host platform accesses wake_socket_fd_ #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } #endif @@ -622,14 +611,14 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT // select() fallback path is too complex to inline (host platform) void yield_with_select_(uint32_t delay_ms); #else inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT void setup_wake_loop_threadsafe_(); // Create wake notification socket inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) #endif @@ -663,9 +652,7 @@ class Application { std::vector socket_fds_; // Vector of all monitored socket file descriptors #endif #ifdef USE_SOCKET_SELECT_SUPPORT -#if !defined(USE_LWIP_FAST_SELECT) int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks -#endif #endif // StringRef members (8 bytes each: pointer + size) @@ -676,7 +663,7 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT int max_fd_{-1}; // Highest file descriptor number for select() #endif @@ -692,11 +679,11 @@ class Application { bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT // Variable-sized members (not needed with fast select — is_socket_ready_ reads rcvevent directly) fd_set read_fds_{}; // Working fd_set: populated by select() fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes @@ -789,7 +776,7 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -811,10 +798,10 @@ inline void Application::drain_wake_notifications_() { } } } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#endif // USE_SOCKET_SELECT_SUPPORT inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_SOCKET_SELECT_SUPPORT // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -905,9 +892,9 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { } // Inline yield_with_select_ for all paths except the select() fallback -#if !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +#ifndef USE_SOCKET_SELECT_SUPPORT inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_LWIP_FAST_SELECT) +#ifdef USE_LWIP_FAST_SELECT // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. // Safe because this runs on the main loop which owns socket lifetime (create, read, close). if (delay_ms == 0) [[unlikely]] { @@ -934,6 +921,6 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay esphome::wakeable_delay(delay_ms); #endif } -#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +#endif // !USE_SOCKET_SELECT_SUPPORT } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 0bd9418b9d0..576865ced8f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -251,7 +251,6 @@ #define USE_SENDSPIN #define USE_SENDSPIN_PORT 8928 // NOLINT #define USE_SOCKET_IMPL_BSD_SOCKETS -#define USE_SOCKET_SELECT_SUPPORT #define USE_LWIP_FAST_SELECT #define USE_SPEAKER @@ -377,7 +376,6 @@ #ifdef USE_LIBRETINY #define USE_CAPTIVE_PORTAL #define USE_SOCKET_IMPL_LWIP_SOCKETS -#define USE_SOCKET_SELECT_SUPPORT #define USE_LWIP_FAST_SELECT #define USE_WEBSERVER #define USE_WEBSERVER_AUTH From eac783697a7a9055c3b1d1fff7bac49734ae6589 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:48:30 -1000 Subject: [PATCH 08/45] Fix cpptests build: handle defines.h having both USE_ESP32 and USE_HOST --- esphome/core/application.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index a5bacde9976..d67baad005f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -916,9 +916,10 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay // Sleep with instant wake via FreeRTOS task notification. // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. esphome::wakeable_delay(delay_ms); -#else - // All other platforms: wakeable delay broken early by wake_loop_any_context() / wake_loop_threadsafe() +#elif defined(USE_ESP8266) || defined(USE_RP2040) esphome::wakeable_delay(delay_ms); +#else + delay(delay_ms); #endif } #endif // !USE_SOCKET_SELECT_SUPPORT From 8cbf3e354b26175945a1dbdb77b25fc690b9b824 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:49:22 -1000 Subject: [PATCH 09/45] Define wakeable_delay on all platforms, simplify yield_with_select_ --- esphome/core/application.h | 4 +--- esphome/core/wake.h | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index d67baad005f..08562e3c070 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -916,10 +916,8 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay // Sleep with instant wake via FreeRTOS task notification. // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. esphome::wakeable_delay(delay_ms); -#elif defined(USE_ESP8266) || defined(USE_RP2040) - esphome::wakeable_delay(delay_ms); #else - delay(delay_ms); + esphome::wakeable_delay(delay_ms); #endif } #endif // !USE_SOCKET_SELECT_SUPPORT diff --git a/esphome/core/wake.h b/esphome/core/wake.h index ae9fc8d3555..1580b309d68 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -112,6 +112,9 @@ void wake_loop_threadsafe(); /// Host: no ISR, just use threadsafe version. inline void wake_loop_any_context() { wake_loop_threadsafe(); } +/// Host: select() handles the delay, this is a simple fallback. +inline void wakeable_delay(uint32_t ms) { delay(ms); } + #endif } // namespace esphome From d3586c76baf652cbc289426d947929d9ef03fa8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:52:24 -1000 Subject: [PATCH 10/45] Deduplicate wakeable_delay call in yield_with_select_ --- esphome/core/application.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 08562e3c070..7e01a533dd1 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -915,10 +915,8 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay // Sleep with instant wake via FreeRTOS task notification. // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. - esphome::wakeable_delay(delay_ms); -#else - esphome::wakeable_delay(delay_ms); #endif + esphome::wakeable_delay(delay_ms); } #endif // !USE_SOCKET_SELECT_SUPPORT From 843b63edb433d38ca4386e09c04d4b0c0d81e52a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:53:15 -1000 Subject: [PATCH 11/45] Move host wake_loop_threadsafe from application.cpp to wake.cpp --- esphome/core/application.cpp | 13 +------------ esphome/core/wake.cpp | 14 +++++++++++++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3df82e25a5d..c04482c5d20 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -676,18 +676,7 @@ void Application::setup_wake_loop_threadsafe_() { } } -void wake_loop_threadsafe() { - // Wakes up lwip_select() in main loop by writing to connected loopback socket - if (App.wake_socket_fd_ >= 0) { - const char dummy = 1; - // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway - // No error checking needed: we control both ends of this loopback socket. - // This is safe to call from FreeRTOS tasks - send() is thread-safe in lwip - // Socket is already connected to loopback address, so send() is faster than sendto() - lwip_send(App.wake_socket_fd_, &dummy, 1, 0); - } -} -#endif // host wake_loop_threadsafe +#endif // USE_SOCKET_SELECT_SUPPORT void Application::get_build_time_string(std::span buffer) { ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size()); diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index c172a26ab7b..cc2b4734f7f 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -84,6 +84,18 @@ void wakeable_delay(uint32_t ms) { #endif // USE_RP2040 -// Host platform wake_loop_threadsafe() is in application.cpp (needs App.wake_socket_fd_) +// === Host (UDP loopback socket) === +#ifdef USE_SOCKET_SELECT_SUPPORT +#include "esphome/core/application.h" +#include + +void wake_loop_threadsafe() { + // Wakes up lwip_select() in main loop by writing to connected loopback socket + if (App.wake_socket_fd_ >= 0) { + const char dummy = 1; + lwip_send(App.wake_socket_fd_, &dummy, 1, 0); + } +} +#endif // USE_SOCKET_SELECT_SUPPORT } // namespace esphome From 4be95e274dc9300c79b14aab627c6a914eb715af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:55:25 -1000 Subject: [PATCH 12/45] Fix host platform: use POSIX socket APIs instead of lwip_* --- esphome/core/application.cpp | 22 +++++++++++----------- esphome/core/application.h | 8 ++++++-- esphome/core/wake.cpp | 6 +++--- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c04482c5d20..846c9963e5f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -624,7 +624,7 @@ alignas(Application) char app_storage[sizeof(Application)] asm( void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications - this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + this->wake_socket_fd_ = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (this->wake_socket_fd_ < 0) { ESP_LOGW(TAG, "Wake socket create failed: %d", errno); return; @@ -633,12 +633,12 @@ void Application::setup_wake_loop_threadsafe_() { // Bind to loopback with auto-assigned port struct sockaddr_in addr = {}; addr.sin_family = AF_INET; - addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = 0; // Auto-assign port - if (lwip_bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + if (::bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { ESP_LOGW(TAG, "Wake socket bind failed: %d", errno); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } @@ -647,30 +647,30 @@ void Application::setup_wake_loop_threadsafe_() { // Connecting a UDP socket allows using send() instead of sendto() for better performance struct sockaddr_in wake_addr; socklen_t len = sizeof(wake_addr); - if (lwip_getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { + if (::getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) { ESP_LOGW(TAG, "Wake socket address failed: %d", errno); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } // Connect to self (loopback) - allows using send() instead of sendto() // After connect(), no need to store wake_addr - the socket remembers it - if (lwip_connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { + if (::connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) { ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } // Set non-blocking mode - int flags = lwip_fcntl(this->wake_socket_fd_, F_GETFL, 0); - lwip_fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); + int flags = ::fcntl(this->wake_socket_fd_, F_GETFL, 0); + ::fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK); // Register with application's select() loop if (!this->register_socket_fd(this->wake_socket_fd_)) { ESP_LOGW(TAG, "Wake socket register failed"); - lwip_close(this->wake_socket_fd_); + ::close(this->wake_socket_fd_); this->wake_socket_fd_ = -1; return; } diff --git a/esphome/core/application.h b/esphome/core/application.h index 7e01a533dd1..f4df256a2dc 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -26,7 +26,11 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #include -#include +#include +#include +#include +#include +#include #endif #ifdef USE_RUNTIME_STATS #include "esphome/components/runtime_stats/runtime_stats.h" @@ -793,7 +797,7 @@ inline void Application::drain_wake_notifications_() { // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK // We control both ends of this loopback socket (always write 1 byte per wake), // so no error checking needed - any errors indicate catastrophic system failure - while (lwip_recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { + while (::recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { // Just draining, no action needed - wake has already occurred } } diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index cc2b4734f7f..46fbcb3c135 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -87,13 +87,13 @@ void wakeable_delay(uint32_t ms) { // === Host (UDP loopback socket) === #ifdef USE_SOCKET_SELECT_SUPPORT #include "esphome/core/application.h" -#include +#include void wake_loop_threadsafe() { - // Wakes up lwip_select() in main loop by writing to connected loopback socket + // Wakes up select() in main loop by writing to connected loopback socket if (App.wake_socket_fd_ >= 0) { const char dummy = 1; - lwip_send(App.wake_socket_fd_, &dummy, 1, 0); + ::send(App.wake_socket_fd_, &dummy, 1, 0); } } #endif // USE_SOCKET_SELECT_SUPPORT From f3778edc69f7e63c6eba91a7c97f1a9d7c0f8a7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:56:30 -1000 Subject: [PATCH 13/45] Clean up dead lwip code paths under USE_SOCKET_SELECT_SUPPORT (now host-only) --- esphome/core/application.cpp | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 846c9963e5f..3078e7aca58 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -30,20 +30,6 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #include - -#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS -// LWIP sockets implementation -#include -#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) -// BSD sockets implementation -#ifdef USE_ESP32 -// ESP32 "BSD sockets" are actually LWIP under the hood -#include -#else -// True BSD sockets (e.g., host platform) -#include -#endif -#endif #endif namespace esphome { @@ -497,16 +483,10 @@ bool Application::register_socket_fd(int fd) { if (fd < 0) return false; -#ifndef USE_ESP32 - // Only check on non-ESP32 platforms - // On ESP32 (both Arduino and ESP-IDF), CONFIG_LWIP_MAX_SOCKETS is always <= FD_SETSIZE by design - // (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS per lwipopts.h) - // Other platforms may not have this guarantee if (fd >= FD_SETSIZE) { ESP_LOGE(TAG, "fd %d exceeds FD_SETSIZE %d", fd, FD_SETSIZE); return false; } -#endif this->socket_fds_.push_back(fd); this->socket_fds_changed_ = true; @@ -570,11 +550,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { tv.tv_usec = (delay_ms - tv.tv_sec * 1000) * 1000; // Call select with timeout -#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS - int ret = lwip_select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); -#else int ret = ::select(this->max_fd_ + 1, &this->read_fds_, nullptr, nullptr, &tv); -#endif // Process select() result: // ret > 0: socket(s) have data ready - normal and expected From edc5ec62e0654dacd27ad0a1a1f631b41e06af5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:59:58 -1000 Subject: [PATCH 14/45] Fix socket to_code: USE_SOCKET_SELECT_SUPPORT only for BSD sockets on host --- esphome/components/socket/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index ab4f3269a84..3fa57b0b478 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -168,11 +168,14 @@ async def to_code(config): # Only when not using lwip_tcp, which does not provide select() support. if (CORE.is_esp32 or CORE.is_libretiny) and impl != IMPLEMENTATION_LWIP_TCP: cg.add_build_flag("-DUSE_LWIP_FAST_SELECT") - elif impl != IMPLEMENTATION_LWIP_TCP: + if ( + impl == IMPLEMENTATION_BSD_SOCKETS + and not CORE.is_esp32 + and not CORE.is_libretiny + ): # Host platform: uses select() syscall for socket monitoring + # and a UDP loopback socket for wake_loop_threadsafe() cg.add_define("USE_SOCKET_SELECT_SUPPORT") - # Platforms with select() but without fast select (host) need a UDP - # loopback socket for wake_loop_threadsafe(). consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) From ad3c92e3f7acd2ea65bf7c8e169ce4cf49c107d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:01:11 -1000 Subject: [PATCH 15/45] Remove unused wakeable_delay on host --- esphome/core/wake.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 1580b309d68..ae9fc8d3555 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -112,9 +112,6 @@ void wake_loop_threadsafe(); /// Host: no ISR, just use threadsafe version. inline void wake_loop_any_context() { wake_loop_threadsafe(); } -/// Host: select() handles the delay, this is a simple fallback. -inline void wakeable_delay(uint32_t ms) { delay(ms); } - #endif } // namespace esphome From 95e8a222506ce1d1d66ac87e93a39d5d1f1988e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:03:07 -1000 Subject: [PATCH 16/45] Fix wake_loop_isrsafe comment: ESP32 only, not LibreTiny --- esphome/core/wake.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index ae9fc8d3555..ade64f08b60 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -5,7 +5,7 @@ /// /// All functions are always available on all platforms — no opt-in needed. /// - wake_loop_any_context(): ISR + thread + callback safe -/// - wake_loop_isrsafe(): ISR only, ESP32/LibreTiny +/// - wake_loop_isrsafe(): ISR only, ESP32 /// - wake_loop_threadsafe(): thread/callback safe /// - wakeable_delay(): sleeps until timeout or wake From aaead7b5eb5736196e704a6c0eec877e8d3ba9e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:07:01 -1000 Subject: [PATCH 17/45] Move wakeable_delay into namespace internal to signal it's not public API --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 2 +- esphome/core/application.h | 2 +- esphome/core/wake.cpp | 4 ++++ esphome/core/wake.h | 6 ++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index c320a6714ab..35224442eec 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -111,7 +111,7 @@ void socket_delay(uint32_t ms) { // callbacks via pendsv (not hard IRQ), so they execute from flash safely. void socket_wake() { s_socket_woke = true; - // Also set core wake flag so Application::wakeable_delay_() breaks out + // Also set core wake flag so wakeable_delay() breaks out esphome::wake_loop_any_context(); } #endif diff --git a/esphome/core/application.h b/esphome/core/application.h index f4df256a2dc..e1129849848 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -920,7 +920,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay // Sleep with instant wake via FreeRTOS task notification. // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. #endif - esphome::wakeable_delay(delay_ms); + esphome::internal::wakeable_delay(delay_ms); } #endif // !USE_SOCKET_SELECT_SUPPORT diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index 46fbcb3c135..c5fc782c253 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -29,6 +29,7 @@ volatile bool g_main_loop_woke = false; void IRAM_ATTR wake_loop_any_context() { wake_loop_impl_(); } +namespace internal { void wakeable_delay(uint32_t ms) { if (ms == 0) { delay(0); @@ -37,6 +38,7 @@ void wakeable_delay(uint32_t ms) { g_main_loop_woke = false; esp_delay(ms, []() { return !g_main_loop_woke; }); } +} // namespace internal #endif // USE_ESP8266 @@ -57,6 +59,7 @@ static int64_t alarm_callback_(alarm_id_t id, void *user_data) { return 0; // One-shot } +namespace internal { void wakeable_delay(uint32_t ms) { if (ms == 0) { yield(); @@ -81,6 +84,7 @@ void wakeable_delay(uint32_t ms) { cancel_alarm(alarm); g_main_loop_woke = false; } +} // namespace internal #endif // USE_RP2040 diff --git a/esphome/core/wake.h b/esphome/core/wake.h index ade64f08b60..53b84f084f0 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -61,6 +61,7 @@ inline void wake_loop_any_context() { esphome_lwip_wake_main_loop(); } inline void wake_loop_threadsafe() { esphome_lwip_wake_main_loop(); } +namespace internal { inline void wakeable_delay(uint32_t ms) { if (ms == 0) { yield(); @@ -68,6 +69,7 @@ inline void wakeable_delay(uint32_t ms) { } ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms)); } +} // namespace internal // === ESP8266 === #elif defined(USE_ESP8266) @@ -84,8 +86,10 @@ void wake_loop_any_context(); /// Non-ISR: always inline. inline void wake_loop_threadsafe() { wake_loop_impl_(); } +namespace internal { /// Wakeable delay for ESP8266. Defined in wake.cpp. void wakeable_delay(uint32_t ms); +} // namespace internal // === RP2040 === #elif defined(USE_RP2040) @@ -100,8 +104,10 @@ inline void wake_loop_threadsafe() { __sev(); } +namespace internal { /// Delay that can be woken early. Uses hardware timer + __wfe()/__sev(). Defined in wake.cpp. void wakeable_delay(uint32_t ms); +} // namespace internal // === Host (UDP loopback socket) === #else From 0bf0fc7eb8b7c2ccbb27b41488ed78bd2294ba36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:07:41 -1000 Subject: [PATCH 18/45] Inline ESP8266 wakeable_delay (no static state, 5 lines) --- esphome/core/wake.cpp | 11 ----------- esphome/core/wake.h | 10 ++++++++-- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index c5fc782c253..ef78d019504 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -29,17 +29,6 @@ volatile bool g_main_loop_woke = false; void IRAM_ATTR wake_loop_any_context() { wake_loop_impl_(); } -namespace internal { -void wakeable_delay(uint32_t ms) { - if (ms == 0) { - delay(0); - return; - } - g_main_loop_woke = false; - esp_delay(ms, []() { return !g_main_loop_woke; }); -} -} // namespace internal - #endif // USE_ESP8266 // === RP2040 — wakeable_delay (wake functions are inline in wake.h) === diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 53b84f084f0..dd2f9ab6b8b 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -87,8 +87,14 @@ void wake_loop_any_context(); inline void wake_loop_threadsafe() { wake_loop_impl_(); } namespace internal { -/// Wakeable delay for ESP8266. Defined in wake.cpp. -void wakeable_delay(uint32_t ms); +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + delay(0); + return; + } + g_main_loop_woke = false; + esp_delay(ms, []() { return !g_main_loop_woke; }); +} } // namespace internal // === RP2040 === From 7e8d3ba13e65a7e62dafbe798697d8f965168edc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:08:41 -1000 Subject: [PATCH 19/45] Inline RP2040 wakeable_delay using function-local static + lambda callback --- esphome/core/wake.cpp | 40 +--------------------------------------- esphome/core/wake.h | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 41 deletions(-) diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index ef78d019504..a42801e5beb 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -31,50 +31,12 @@ void IRAM_ATTR wake_loop_any_context() { wake_loop_impl_(); } #endif // USE_ESP8266 -// === RP2040 — wakeable_delay (wake functions are inline in wake.h) === +// === RP2040 — g_main_loop_woke definition (wake functions + wakeable_delay are inline in wake.h) === #ifdef USE_RP2040 // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) volatile bool g_main_loop_woke = false; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_delay_expired = false; - -static int64_t alarm_callback_(alarm_id_t id, void *user_data) { - (void) id; - (void) user_data; - s_delay_expired = true; - __sev(); // Wake from __wfe() — timeout expired. - return 0; // One-shot -} - -namespace internal { -void wakeable_delay(uint32_t ms) { - if (ms == 0) { - yield(); - return; - } - // If a wake was already signalled, consume it and return immediately - if (g_main_loop_woke) { - g_main_loop_woke = false; - return; - } - s_delay_expired = false; - alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true); - if (alarm <= 0) { - delay(ms); - return; - } - // Sleep until woken by either the timer alarm or wake_loop_any_context()/wake_loop_threadsafe() - while (!g_main_loop_woke && !s_delay_expired) { - __wfe(); - } - if (!s_delay_expired) - cancel_alarm(alarm); - g_main_loop_woke = false; -} -} // namespace internal - #endif // USE_RP2040 // === Host (UDP loopback socket) === diff --git a/esphome/core/wake.h b/esphome/core/wake.h index dd2f9ab6b8b..3806e2734e2 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -26,6 +26,7 @@ #include #elif defined(USE_RP2040) #include +#include #endif namespace esphome { @@ -111,8 +112,35 @@ inline void wake_loop_threadsafe() { } namespace internal { -/// Delay that can be woken early. Uses hardware timer + __wfe()/__sev(). Defined in wake.cpp. -void wakeable_delay(uint32_t ms); +inline void wakeable_delay(uint32_t ms) { + // Function-local statics — safe because this is only called from the main loop. + static volatile bool s_delay_expired = false; + if (ms == 0) { + yield(); + return; + } + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + s_delay_expired = false; + auto alarm_cb = [](alarm_id_t, void *) -> int64_t { + s_delay_expired = true; + __sev(); + return 0; + }; + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_cb, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + while (!g_main_loop_woke && !s_delay_expired) { + __wfe(); + } + if (!s_delay_expired) + cancel_alarm(alarm); + g_main_loop_woke = false; +} } // namespace internal // === Host (UDP loopback socket) === From 5dd43bca2ddb2b5ef8e600d6a98590ee76f3230c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:11:13 -1000 Subject: [PATCH 20/45] Simplify wake.h file doc --- esphome/core/wake.h | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 3806e2734e2..5283e57227e 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -2,12 +2,7 @@ /// @file wake.h /// Platform-specific main loop wake primitives. -/// -/// All functions are always available on all platforms — no opt-in needed. -/// - wake_loop_any_context(): ISR + thread + callback safe -/// - wake_loop_isrsafe(): ISR only, ESP32 -/// - wake_loop_threadsafe(): thread/callback safe -/// - wakeable_delay(): sleeps until timeout or wake +/// Always available on all platforms — no opt-in needed. #include "esphome/core/defines.h" #include "esphome/core/hal.h" From 75a2b23bdbc94b164bbfc1ba1ad67a9a7fa809d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:16:41 -1000 Subject: [PATCH 21/45] Replace USE_SOCKET_SELECT_SUPPORT with USE_HOST --- esphome/components/socket/__init__.py | 1 - esphome/components/socket/socket.cpp | 2 +- esphome/components/socket/socket.h | 2 +- esphome/core/application.cpp | 14 +++++------ esphome/core/application.h | 36 +++++++++++++-------------- esphome/core/defines.h | 1 - esphome/core/wake.cpp | 4 +-- 7 files changed, 29 insertions(+), 31 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 3fa57b0b478..6be83128470 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -175,7 +175,6 @@ async def to_code(config): ): # Host platform: uses select() syscall for socket monitoring # and a UDP loopback socket for wake_loop_threadsafe() - cg.add_define("USE_SOCKET_SELECT_SUPPORT") consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 4b8a32ed1dc..bc43b2746ee 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -8,7 +8,7 @@ namespace esphome::socket { -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST // Shared ready() implementation for fd-based socket implementations (BSD and LWIP sockets). // Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored) { return !loop_monitored || App.is_socket_ready_(fd); } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 226a669e31a..a681bc228b6 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -45,7 +45,7 @@ using ListenSocket = LWIPRawListenImpl; inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) { return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock)); } -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) /// Shared ready() helper for fd-based socket implementations. /// Checks if the Application's select() loop has marked this fd as ready. bool socket_ready_fd(int fd, bool loop_monitored); diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 3078e7aca58..c389f1c342f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -28,7 +28,7 @@ #include "esphome/components/socket/socket.h" #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST #include #endif @@ -120,7 +120,7 @@ void Application::setup() { // when USE_LWIP_FAST_SELECT is enabled (ESP32 and LibreTiny). esphome_lwip_fast_select_init(); #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST // Set up wake socket for waking main loop from tasks (platforms without fast select only) this->setup_wake_loop_threadsafe_(); #endif @@ -476,7 +476,7 @@ void Application::unregister_socket(struct lwip_sock *sock) { return; } } -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) bool Application::register_socket_fd(int fd) { // WARNING: This function is NOT thread-safe and must only be called from the main loop // It modifies socket_fds_ and related variables without locking @@ -527,7 +527,7 @@ void Application::unregister_socket_fd(int fd) { #endif // Only the select() fallback path remains in the .cpp — all other paths are inlined in application.h -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST void Application::yield_with_select_(uint32_t delay_ms) { // Fallback select() path (host platform and any future platforms without fast select). if (!this->socket_fds_.empty()) [[likely]] { @@ -573,7 +573,7 @@ void Application::yield_with_select_(uint32_t delay_ms) { // No sockets registered or select() failed - use regular delay delay(delay_ms); } -#endif // USE_SOCKET_SELECT_SUPPORT +#endif // USE_HOST // App storage — asm label shares the linker symbol with "extern Application App". // char[] is trivially destructible, so no __cxa_atexit or destructor chain is emitted. @@ -596,7 +596,7 @@ alignas(Application) char app_storage[sizeof(Application)] asm( // Host platform wake_loop_threadsafe() and setup — needs wake_socket_fd_ // ESP32/LibreTiny/ESP8266/RP2040 implementations are in wake.cpp -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST void Application::setup_wake_loop_threadsafe_() { // Create UDP socket for wake notifications @@ -652,7 +652,7 @@ void Application::setup_wake_loop_threadsafe_() { } } -#endif // USE_SOCKET_SELECT_SUPPORT +#endif // USE_HOST void Application::get_build_time_string(std::span buffer) { ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size()); diff --git a/esphome/core/application.h b/esphome/core/application.h index e1129849848..c3e2a42660d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,7 +24,7 @@ #include "esphome/core/area.h" #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST #include #include #include @@ -110,7 +110,7 @@ #endif namespace esphome::socket { -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST /// Shared ready() helper for fd-based socket implementations. bool socket_ready_fd(int fd, bool loop_monitored); // NOLINT(readability-redundant-declaration) #endif @@ -536,7 +536,7 @@ class Application { /// @return true if registration was successful, false if sock is null bool register_socket(struct lwip_sock *sock); void unregister_socket(struct lwip_sock *sock); -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) /// Fallback select() path: monitors file descriptors. /// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error. /// @return true if registration was successful, false if fd exceeds limits @@ -558,7 +558,7 @@ class Application { protected: friend Component; -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST friend bool socket::socket_ready_fd(int fd, bool loop_monitored); #endif #ifdef USE_RUNTIME_STATS @@ -566,11 +566,11 @@ class Application { #endif friend void ::setup(); friend void ::original_setup(); -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST friend void wake_loop_threadsafe(); // Host platform accesses wake_socket_fd_ #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } #endif @@ -615,14 +615,14 @@ class Application { void feed_wdt_arch_(); /// Perform a delay while also monitoring socket file descriptors for readiness -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST // select() fallback path is too complex to inline (host platform) void yield_with_select_(uint32_t delay_ms); #else inline void ESPHOME_ALWAYS_INLINE yield_with_select_(uint32_t delay_ms); #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST void setup_wake_loop_threadsafe_(); // Create wake notification socket inline void drain_wake_notifications_(); // Read pending wake notifications in main loop (hot path - inlined) #endif @@ -652,10 +652,10 @@ class Application { FixedVector looping_components_{}; #ifdef USE_LWIP_FAST_SELECT std::vector monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read -#elif defined(USE_SOCKET_SELECT_SUPPORT) +#elif defined(USE_HOST) std::vector socket_fds_; // Vector of all monitored socket file descriptors #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks #endif @@ -667,7 +667,7 @@ class Application { uint32_t last_loop_{0}; uint32_t loop_component_start_time_{0}; -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST int max_fd_{-1}; // Highest file descriptor number for select() #endif @@ -683,11 +683,11 @@ class Application { bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST // Variable-sized members (not needed with fast select — is_socket_ready_ reads rcvevent directly) fd_set read_fds_{}; // Working fd_set: populated by select() fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes @@ -780,7 +780,7 @@ class Application { /// Global storage of Application pointer - only one Application can exist. extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -802,10 +802,10 @@ inline void Application::drain_wake_notifications_() { } } } -#endif // USE_SOCKET_SELECT_SUPPORT +#endif // USE_HOST inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) { -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -896,7 +896,7 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { } // Inline yield_with_select_ for all paths except the select() fallback -#ifndef USE_SOCKET_SELECT_SUPPORT +#ifndef USE_HOST inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) { #ifdef USE_LWIP_FAST_SELECT // Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers. @@ -922,6 +922,6 @@ inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay #endif esphome::internal::wakeable_delay(delay_ms); } -#endif // !USE_SOCKET_SELECT_SUPPORT +#endif // !USE_HOST } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 576865ced8f..5b0536937e4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -387,7 +387,6 @@ #ifdef USE_HOST #define USE_HTTP_REQUEST_RESPONSE #define USE_SOCKET_IMPL_BSD_SOCKETS -#define USE_SOCKET_SELECT_SUPPORT #define USE_ESPHOME_TASK_LOG_BUFFER #define ESPHOME_TASK_LOG_BUFFER_SIZE 64 #endif diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index a42801e5beb..076083ded4f 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -40,7 +40,7 @@ volatile bool g_main_loop_woke = false; #endif // USE_RP2040 // === Host (UDP loopback socket) === -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST #include "esphome/core/application.h" #include @@ -51,6 +51,6 @@ void wake_loop_threadsafe() { ::send(App.wake_socket_fd_, &dummy, 1, 0); } } -#endif // USE_SOCKET_SELECT_SUPPORT +#endif // USE_HOST } // namespace esphome From e2cb5df6c803c74b575181b2d460ecab3ebac522 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:17:12 -1000 Subject: [PATCH 22/45] Simplify host check in socket to_code: use CORE.is_host --- esphome/components/socket/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 6be83128470..7d5b9d95a2e 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -168,11 +168,7 @@ async def to_code(config): # Only when not using lwip_tcp, which does not provide select() support. if (CORE.is_esp32 or CORE.is_libretiny) and impl != IMPLEMENTATION_LWIP_TCP: cg.add_build_flag("-DUSE_LWIP_FAST_SELECT") - if ( - impl == IMPLEMENTATION_BSD_SOCKETS - and not CORE.is_esp32 - and not CORE.is_libretiny - ): + if CORE.is_host: # Host platform: uses select() syscall for socket monitoring # and a UDP loopback socket for wake_loop_threadsafe() consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) From cb3791ae8a11d11ae493587692c60071129342b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:18:54 -1000 Subject: [PATCH 23/45] Deduplicate g_main_loop_woke definition in wake.cpp --- esphome/core/wake.cpp | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index 076083ded4f..df7955aeb79 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -21,23 +21,14 @@ void IRAM_ATTR wake_loop_any_context() { wake_loop_any_context_inline_(); } #endif // USE_ESP32 -// === ESP8266 — IRAM_ATTR entry point + wakeable_delay === +#if defined(USE_ESP8266) || defined(USE_RP2040) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +volatile bool g_main_loop_woke = false; +#endif + #ifdef USE_ESP8266 - -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -volatile bool g_main_loop_woke = false; - void IRAM_ATTR wake_loop_any_context() { wake_loop_impl_(); } - -#endif // USE_ESP8266 - -// === RP2040 — g_main_loop_woke definition (wake functions + wakeable_delay are inline in wake.h) === -#ifdef USE_RP2040 - -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -volatile bool g_main_loop_woke = false; - -#endif // USE_RP2040 +#endif // === Host (UDP loopback socket) === #ifdef USE_HOST From 80ff9f365b1cf5393c59777d7d447c5e2e19bff3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:21:52 -1000 Subject: [PATCH 24/45] Fix wake.cpp: move includes outside namespace to fix cpptests build --- esphome/core/wake.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index df7955aeb79..870eb16b22a 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -3,9 +3,11 @@ #ifdef USE_ESP8266 #include -#elif defined(USE_RP2040) -#include -#include +#endif + +#ifdef USE_HOST +#include "esphome/core/application.h" +#include #endif namespace esphome { @@ -32,9 +34,6 @@ void IRAM_ATTR wake_loop_any_context() { wake_loop_impl_(); } // === Host (UDP loopback socket) === #ifdef USE_HOST -#include "esphome/core/application.h" -#include - void wake_loop_threadsafe() { // Wakes up select() in main loop by writing to connected loopback socket if (App.wake_socket_fd_ >= 0) { From aa0e56bfde269053a20b27e73950b716a62a7bef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:28:21 -1000 Subject: [PATCH 25/45] Fix ESP32/LibreTiny without fast select: use FreeRTOS task notifications directly --- esphome/core/wake.cpp | 17 +++++----- esphome/core/wake.h | 73 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index 870eb16b22a..b95af3734e8 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -12,17 +12,21 @@ namespace esphome { -// === ESP32 — IRAM_ATTR entry points (inline impls in wake.h) === -#ifdef USE_ESP32 - +// === ESP32 — IRAM_ATTR entry points (fast-select only) === +#if defined(USE_ESP32) && defined(USE_LWIP_FAST_SELECT) void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { wake_loop_isrsafe_inline_(px_higher_priority_task_woken); } - void IRAM_ATTR wake_loop_any_context() { wake_loop_any_context_inline_(); } +#endif -#endif // USE_ESP32 +// === FreeRTOS task handle for non-fast-select ESP32/LibreTiny === +#if (defined(USE_ESP32) || defined(USE_LIBRETINY)) && !defined(USE_LWIP_FAST_SELECT) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +TaskHandle_t g_main_task_handle = nullptr; +#endif +// === ESP8266 / RP2040 === #if defined(USE_ESP8266) || defined(USE_RP2040) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) volatile bool g_main_loop_woke = false; @@ -35,12 +39,11 @@ void IRAM_ATTR wake_loop_any_context() { wake_loop_impl_(); } // === Host (UDP loopback socket) === #ifdef USE_HOST void wake_loop_threadsafe() { - // Wakes up select() in main loop by writing to connected loopback socket if (App.wake_socket_fd_ >= 0) { const char dummy = 1; ::send(App.wake_socket_fd_, &dummy, 1, 0); } } -#endif // USE_HOST +#endif } // namespace esphome diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 5283e57227e..352c50e45a2 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -9,6 +9,8 @@ #ifdef USE_LWIP_FAST_SELECT #include "esphome/core/lwip_fast_select.h" +#endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) #ifdef USE_ESP32 #include #include @@ -27,38 +29,81 @@ namespace esphome { // === Wake flag for ESP8266/RP2040 === -// Checked by wakeable_delay() to exit early. Set by wake functions. #if defined(USE_ESP8266) || defined(USE_RP2040) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern volatile bool g_main_loop_woke; #endif -// === ESP32/LibreTiny (FreeRTOS) === -#ifdef USE_LWIP_FAST_SELECT +// === ESP32/LibreTiny — FreeRTOS task handle for non-fast-select path === +#if (defined(USE_ESP32) || defined(USE_LIBRETINY)) && !defined(USE_LWIP_FAST_SELECT) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern TaskHandle_t g_main_task_handle; +#endif -#ifdef USE_ESP32 -/// Inline implementation — callers in IRAM get it inlined, keeping it IRAM-safe. -/// IRAM_ATTR entry points in wake.cpp exist for callers that aren't themselves IRAM. +// === ESP32 === +#if defined(USE_ESP32) + +#ifdef USE_LWIP_FAST_SELECT inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe_inline_(int *px_higher_priority_task_woken) { esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); } - /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_isrsafe(int *px_higher_priority_task_woken); inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { esphome_lwip_wake_main_loop_any_context(); } - /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_any_context(); -#else -/// LibreTiny: no working IRAM_ATTR — just use threadsafe version. -inline void wake_loop_any_context() { esphome_lwip_wake_main_loop(); } -#endif inline void wake_loop_threadsafe() { esphome_lwip_wake_main_loop(); } +#else +inline void wake_loop_any_context() { + if (g_main_task_handle != nullptr) + xTaskNotifyGive(g_main_task_handle); +} +inline void wake_loop_threadsafe() { + if (g_main_task_handle != nullptr) + xTaskNotifyGive(g_main_task_handle); +} +#endif namespace internal { inline void wakeable_delay(uint32_t ms) { +#ifndef USE_LWIP_FAST_SELECT + // Cache main task handle on first call + if (g_main_task_handle == nullptr) + g_main_task_handle = xTaskGetCurrentTaskHandle(); +#endif + if (ms == 0) { + yield(); + return; + } + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms)); +} +} // namespace internal + +// === LibreTiny === +#elif defined(USE_LIBRETINY) + +#ifdef USE_LWIP_FAST_SELECT +inline void wake_loop_any_context() { esphome_lwip_wake_main_loop(); } +inline void wake_loop_threadsafe() { esphome_lwip_wake_main_loop(); } +#else +inline void wake_loop_any_context() { + if (g_main_task_handle != nullptr) + xTaskNotifyGive(g_main_task_handle); +} +inline void wake_loop_threadsafe() { + if (g_main_task_handle != nullptr) + xTaskNotifyGive(g_main_task_handle); +} +#endif + +namespace internal { +inline void wakeable_delay(uint32_t ms) { +#ifndef USE_LWIP_FAST_SELECT + if (g_main_task_handle == nullptr) + g_main_task_handle = xTaskGetCurrentTaskHandle(); +#endif if (ms == 0) { yield(); return; @@ -108,7 +153,6 @@ inline void wake_loop_threadsafe() { namespace internal { inline void wakeable_delay(uint32_t ms) { - // Function-local statics — safe because this is only called from the main loop. static volatile bool s_delay_expired = false; if (ms == 0) { yield(); @@ -141,10 +185,9 @@ inline void wakeable_delay(uint32_t ms) { // === Host (UDP loopback socket) === #else -/// Host platform: wakes select() via UDP loopback socket. Defined in application.cpp. +/// Defined in wake.cpp. void wake_loop_threadsafe(); -/// Host: no ISR, just use threadsafe version. inline void wake_loop_any_context() { wake_loop_threadsafe(); } #endif From 63b8275e236039e1d53ccf308bedcfd5d99267f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:30:45 -1000 Subject: [PATCH 26/45] Fix ISR safety: ESP32 without fast select needs xPortInIsrContext detection --- esphome/core/wake.cpp | 6 ++++-- esphome/core/wake.h | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index b95af3734e8..0a8e1f870f2 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -12,11 +12,13 @@ namespace esphome { -// === ESP32 — IRAM_ATTR entry points (fast-select only) === -#if defined(USE_ESP32) && defined(USE_LWIP_FAST_SELECT) +// === ESP32 — IRAM_ATTR entry points === +#ifdef USE_ESP32 +#ifdef USE_LWIP_FAST_SELECT void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { wake_loop_isrsafe_inline_(px_higher_priority_task_woken); } +#endif void IRAM_ATTR wake_loop_any_context() { wake_loop_any_context_inline_(); } #endif diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 352c50e45a2..7fa5ecb6a64 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -56,10 +56,21 @@ void wake_loop_any_context(); inline void wake_loop_threadsafe() { esphome_lwip_wake_main_loop(); } #else -inline void wake_loop_any_context() { - if (g_main_task_handle != nullptr) +/// Inline impl — ISR callers inline this into IRAM. Uses xPortInIsrContext() to pick safe API. +inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { + if (g_main_task_handle == nullptr) + return; + if (xPortInIsrContext()) { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + vTaskNotifyGiveFromISR(g_main_task_handle, &xHigherPriorityTaskWoken); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); + } else { xTaskNotifyGive(g_main_task_handle); + } } +/// IRAM_ATTR entry point — defined in wake.cpp. +void wake_loop_any_context(); + inline void wake_loop_threadsafe() { if (g_main_task_handle != nullptr) xTaskNotifyGive(g_main_task_handle); @@ -69,7 +80,6 @@ inline void wake_loop_threadsafe() { namespace internal { inline void wakeable_delay(uint32_t ms) { #ifndef USE_LWIP_FAST_SELECT - // Cache main task handle on first call if (g_main_task_handle == nullptr) g_main_task_handle = xTaskGetCurrentTaskHandle(); #endif From 36d2b78c18119a95d582eb8b3a20cf61b218ec3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:34:10 -1000 Subject: [PATCH 27/45] Unify ESP32/LibreTiny wake: always use g_main_task_handle, remove lwip_fast_select dependency --- esphome/core/application.cpp | 8 +++++--- esphome/core/wake.cpp | 14 ++++++-------- esphome/core/wake.h | 33 ++++++--------------------------- 3 files changed, 17 insertions(+), 38 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index c389f1c342f..1d01d6ff0b1 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -114,10 +114,12 @@ void Application::setup() { clear_setup_priority_overrides(); #endif +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + // Save main loop task handle for wake_loop_*() FreeRTOS notifications. + g_main_task_handle = xTaskGetCurrentTaskHandle(); +#endif #ifdef USE_LWIP_FAST_SELECT - // Initialize fast select: saves main loop task handle for xTaskNotifyGive wake. - // The fast path (rcvevent reads + ulTaskNotifyTake) is used unconditionally - // when USE_LWIP_FAST_SELECT is enabled (ESP32 and LibreTiny). + // Initialize fast select: hooks socket monitoring for direct rcvevent reads. esphome_lwip_fast_select_init(); #endif #ifdef USE_HOST diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index 0a8e1f870f2..18fbbfd4f45 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -12,22 +12,20 @@ namespace esphome { +// === ESP32/LibreTiny — FreeRTOS task handle === +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +TaskHandle_t g_main_task_handle = nullptr; +#endif + // === ESP32 — IRAM_ATTR entry points === #ifdef USE_ESP32 -#ifdef USE_LWIP_FAST_SELECT void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { wake_loop_isrsafe_inline_(px_higher_priority_task_woken); } -#endif void IRAM_ATTR wake_loop_any_context() { wake_loop_any_context_inline_(); } #endif -// === FreeRTOS task handle for non-fast-select ESP32/LibreTiny === -#if (defined(USE_ESP32) || defined(USE_LIBRETINY)) && !defined(USE_LWIP_FAST_SELECT) -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -TaskHandle_t g_main_task_handle = nullptr; -#endif - // === ESP8266 / RP2040 === #if defined(USE_ESP8266) || defined(USE_RP2040) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 7fa5ecb6a64..6c3343e1353 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -7,9 +7,6 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#ifdef USE_LWIP_FAST_SELECT -#include "esphome/core/lwip_fast_select.h" -#endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) #ifdef USE_ESP32 #include @@ -34,8 +31,8 @@ namespace esphome { extern volatile bool g_main_loop_woke; #endif -// === ESP32/LibreTiny — FreeRTOS task handle for non-fast-select path === -#if (defined(USE_ESP32) || defined(USE_LIBRETINY)) && !defined(USE_LWIP_FAST_SELECT) +// === ESP32/LibreTiny — FreeRTOS task handle for wake notifications === +#if defined(USE_ESP32) || defined(USE_LIBRETINY) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern TaskHandle_t g_main_task_handle; #endif @@ -43,19 +40,14 @@ extern TaskHandle_t g_main_task_handle; // === ESP32 === #if defined(USE_ESP32) -#ifdef USE_LWIP_FAST_SELECT +/// Inline impl — ISR callers inline this into IRAM. inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe_inline_(int *px_higher_priority_task_woken) { - esphome_lwip_wake_main_loop_from_isr(px_higher_priority_task_woken); + if (g_main_task_handle != nullptr) + vTaskNotifyGiveFromISR(g_main_task_handle, (BaseType_t *) px_higher_priority_task_woken); } /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_isrsafe(int *px_higher_priority_task_woken); -inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { esphome_lwip_wake_main_loop_any_context(); } -/// IRAM_ATTR entry point — defined in wake.cpp. -void wake_loop_any_context(); - -inline void wake_loop_threadsafe() { esphome_lwip_wake_main_loop(); } -#else /// Inline impl — ISR callers inline this into IRAM. Uses xPortInIsrContext() to pick safe API. inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { if (g_main_task_handle == nullptr) @@ -75,14 +67,9 @@ inline void wake_loop_threadsafe() { if (g_main_task_handle != nullptr) xTaskNotifyGive(g_main_task_handle); } -#endif namespace internal { inline void wakeable_delay(uint32_t ms) { -#ifndef USE_LWIP_FAST_SELECT - if (g_main_task_handle == nullptr) - g_main_task_handle = xTaskGetCurrentTaskHandle(); -#endif if (ms == 0) { yield(); return; @@ -94,26 +81,18 @@ inline void wakeable_delay(uint32_t ms) { // === LibreTiny === #elif defined(USE_LIBRETINY) -#ifdef USE_LWIP_FAST_SELECT -inline void wake_loop_any_context() { esphome_lwip_wake_main_loop(); } -inline void wake_loop_threadsafe() { esphome_lwip_wake_main_loop(); } -#else inline void wake_loop_any_context() { if (g_main_task_handle != nullptr) xTaskNotifyGive(g_main_task_handle); } + inline void wake_loop_threadsafe() { if (g_main_task_handle != nullptr) xTaskNotifyGive(g_main_task_handle); } -#endif namespace internal { inline void wakeable_delay(uint32_t ms) { -#ifndef USE_LWIP_FAST_SELECT - if (g_main_task_handle == nullptr) - g_main_task_handle = xTaskGetCurrentTaskHandle(); -#endif if (ms == 0) { yield(); return; From a1e6a89fc8abcfbf7c2e35622cef681140db7790 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:36:49 -1000 Subject: [PATCH 28/45] Share task handle between wake.h and lwip_fast_select via main_task.h --- esphome/core/application.cpp | 8 ++------ esphome/core/lwip_fast_select.c | 16 +++++++-------- esphome/core/lwip_fast_select.h | 4 ---- esphome/core/main_task.c | 5 +++++ esphome/core/main_task.h | 22 ++++++++++++++++++++ esphome/core/wake.cpp | 6 ------ esphome/core/wake.h | 36 +++++++++++---------------------- 7 files changed, 48 insertions(+), 49 deletions(-) create mode 100644 esphome/core/main_task.c create mode 100644 esphome/core/main_task.h diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 1d01d6ff0b1..cd758598801 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -115,12 +115,8 @@ void Application::setup() { #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) - // Save main loop task handle for wake_loop_*() FreeRTOS notifications. - g_main_task_handle = xTaskGetCurrentTaskHandle(); -#endif -#ifdef USE_LWIP_FAST_SELECT - // Initialize fast select: hooks socket monitoring for direct rcvevent reads. - esphome_lwip_fast_select_init(); + // Save main loop task handle for wake_loop_*() / fast select FreeRTOS notifications. + esphome_main_task_handle = xTaskGetCurrentTaskHandle(); #endif #ifdef USE_HOST // Set up wake socket for waking main loop from tasks (platforms without fast select only) diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index a695fa396bc..c850e88e205 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -63,11 +63,11 @@ // // Shared state and safety rationale: // -// s_main_loop_task (TaskHandle_t, 4 bytes): +// esphome_main_task_handle (TaskHandle_t, 4 bytes): // Written once by main loop in init(). Read by TCP/IP thread (in callback) // and background tasks (in wake). // Safe: write-once-then-read pattern. Socket hooks may run before init(), -// but the NULL check on s_main_loop_task in the callback provides correct +// but the NULL check on esphome_main_task_handle in the callback provides correct // degraded behavior — notifications are simply skipped until init() completes. // // s_original_callback (netconn_callback, 4-byte function pointer): @@ -123,6 +123,7 @@ #endif #include "esphome/core/lwip_fast_select.h" +#include "esphome/core/main_task.h" #include @@ -157,8 +158,7 @@ _Static_assert(offsetof(struct lwip_sock, rcvevent) % sizeof(((struct lwip_sock _Static_assert(offsetof(struct lwip_sock, rcvevent) == ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET, "lwip_sock.rcvevent offset changed — update ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET in lwip_fast_select.h"); -// Task handle for the main loop — written once in init(), read from TCP/IP and background tasks. -static TaskHandle_t s_main_loop_task = NULL; +// Task handle is in main_task.c (esphome_main_task_handle) — shared with wake.h. // Saved original event_callback pointer — written once in first hook_socket(), read from TCP/IP task. static netconn_callback s_original_callback = NULL; @@ -177,15 +177,13 @@ 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) { - TaskHandle_t task = s_main_loop_task; + TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { xTaskNotifyGive(task); } } } -void esphome_lwip_fast_select_init(void) { s_main_loop_task = xTaskGetCurrentTaskHandle(); } - // lwip_socket_dbg_get_socket() is a thin wrapper around the static // tryget_socket_unconn_nouse() — a direct array lookup without the refcount // that get_socket()/done_socket() uses. This is safe because: @@ -234,7 +232,7 @@ bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable) { // Wake the main loop from another FreeRTOS task. NOT ISR-safe. void esphome_lwip_wake_main_loop(void) { - TaskHandle_t task = s_main_loop_task; + TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { xTaskNotifyGive(task); } @@ -242,7 +240,7 @@ void esphome_lwip_wake_main_loop(void) { // Wake the main loop from an ISR. ISR-safe variant. void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken) { - TaskHandle_t task = s_main_loop_task; + TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); } diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 50706ba9f69..96337a1dcd1 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -20,10 +20,6 @@ enum { ESPHOME_LWIP_SOCK_RCVEVENT_OFFSET = 8 }; extern "C" { #endif -/// Initialize fast select — must be called from the main loop task during setup(). -/// Saves the current task handle for xTaskNotifyGive() wake notifications. -void esphome_lwip_fast_select_init(void); - /// Look up a LwIP socket struct from a file descriptor. /// Returns NULL if fd is invalid or the socket/netconn is not initialized. /// Use this at registration time to cache the pointer for esphome_lwip_socket_has_data(). diff --git a/esphome/core/main_task.c b/esphome/core/main_task.c new file mode 100644 index 00000000000..52d9c2951a6 --- /dev/null +++ b/esphome/core/main_task.c @@ -0,0 +1,5 @@ +#include "esphome/core/main_task.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +TaskHandle_t esphome_main_task_handle = NULL; +#endif diff --git a/esphome/core/main_task.h b/esphome/core/main_task.h new file mode 100644 index 00000000000..d46a54cb191 --- /dev/null +++ b/esphome/core/main_task.h @@ -0,0 +1,22 @@ +#pragma once + +/// Main loop task handle — shared between wake.h (C++) and lwip_fast_select.c (C). +/// Set once during Application::setup() via xTaskGetCurrentTaskHandle(). + +#ifdef USE_ESP32 +#include +#include +#elif defined(USE_LIBRETINY) +#include +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +extern TaskHandle_t esphome_main_task_handle; + +#ifdef __cplusplus +} +#endif diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index 18fbbfd4f45..723988509b8 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -12,12 +12,6 @@ namespace esphome { -// === ESP32/LibreTiny — FreeRTOS task handle === -#if defined(USE_ESP32) || defined(USE_LIBRETINY) -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -TaskHandle_t g_main_task_handle = nullptr; -#endif - // === ESP32 — IRAM_ATTR entry points === #ifdef USE_ESP32 void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 6c3343e1353..79037c82649 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -8,13 +8,7 @@ #include "esphome/core/hal.h" #if defined(USE_ESP32) || defined(USE_LIBRETINY) -#ifdef USE_ESP32 -#include -#include -#else -#include -#include -#endif +#include "esphome/core/main_task.h" #endif #ifdef USE_ESP8266 #include @@ -31,41 +25,35 @@ namespace esphome { extern volatile bool g_main_loop_woke; #endif -// === ESP32/LibreTiny — FreeRTOS task handle for wake notifications === -#if defined(USE_ESP32) || defined(USE_LIBRETINY) -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -extern TaskHandle_t g_main_task_handle; -#endif - // === ESP32 === #if defined(USE_ESP32) /// Inline impl — ISR callers inline this into IRAM. inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe_inline_(int *px_higher_priority_task_woken) { - if (g_main_task_handle != nullptr) - vTaskNotifyGiveFromISR(g_main_task_handle, (BaseType_t *) px_higher_priority_task_woken); + if (esphome_main_task_handle != nullptr) + vTaskNotifyGiveFromISR(esphome_main_task_handle, (BaseType_t *) px_higher_priority_task_woken); } /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_isrsafe(int *px_higher_priority_task_woken); /// Inline impl — ISR callers inline this into IRAM. Uses xPortInIsrContext() to pick safe API. inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { - if (g_main_task_handle == nullptr) + if (esphome_main_task_handle == nullptr) return; if (xPortInIsrContext()) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(g_main_task_handle, &xHigherPriorityTaskWoken); + vTaskNotifyGiveFromISR(esphome_main_task_handle, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } else { - xTaskNotifyGive(g_main_task_handle); + xTaskNotifyGive(esphome_main_task_handle); } } /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_any_context(); inline void wake_loop_threadsafe() { - if (g_main_task_handle != nullptr) - xTaskNotifyGive(g_main_task_handle); + if (esphome_main_task_handle != nullptr) + xTaskNotifyGive(esphome_main_task_handle); } namespace internal { @@ -82,13 +70,13 @@ inline void wakeable_delay(uint32_t ms) { #elif defined(USE_LIBRETINY) inline void wake_loop_any_context() { - if (g_main_task_handle != nullptr) - xTaskNotifyGive(g_main_task_handle); + if (esphome_main_task_handle != nullptr) + xTaskNotifyGive(esphome_main_task_handle); } inline void wake_loop_threadsafe() { - if (g_main_task_handle != nullptr) - xTaskNotifyGive(g_main_task_handle); + if (esphome_main_task_handle != nullptr) + xTaskNotifyGive(esphome_main_task_handle); } namespace internal { From 6dcdefe1799e3596675ea0dabfb2d13aba0e14b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:39:10 -1000 Subject: [PATCH 29/45] Move task notification helpers to main_task.c/h, deduplicate ESP32/LibreTiny in wake.h --- esphome/core/lwip_fast_select.c | 37 ---------------------- esphome/core/lwip_fast_select.h | 16 ---------- esphome/core/main_task.c | 36 +++++++++++++++++++++- esphome/core/main_task.h | 21 +++++++++++-- esphome/core/wake.h | 54 +++++++-------------------------- 5 files changed, 64 insertions(+), 100 deletions(-) diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index c850e88e205..82b68aa738b 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -127,12 +127,6 @@ #include -// IRAM_ATTR is defined by esp_attr.h (included via FreeRTOS headers) on ESP32. -// On LibreTiny it's not defined — provide a no-op fallback. -#ifndef IRAM_ATTR -#define IRAM_ATTR -#endif - // Compile-time verification of thread safety assumptions. // On ESP32 (Xtensa/RISC-V) and LibreTiny (ARM Cortex-M), naturally-aligned // reads/writes up to 32 bits are atomic. @@ -230,35 +224,4 @@ bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable) { return true; } -// Wake the main loop from another FreeRTOS task. NOT ISR-safe. -void esphome_lwip_wake_main_loop(void) { - TaskHandle_t task = esphome_main_task_handle; - if (task != NULL) { - xTaskNotifyGive(task); - } -} - -// Wake the main loop from an ISR. ISR-safe variant. -void IRAM_ATTR esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken) { - TaskHandle_t task = esphome_main_task_handle; - if (task != NULL) { - vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); - } -} - -// Wake the main loop from any context (ISR, thread, or main loop). -// ESP32-only: uses xPortInIsrContext() to detect ISR context. -// LibreTiny is excluded because it lacks IRAM_ATTR support needed for ISR-safe paths. -#ifdef USE_ESP32 -void IRAM_ATTR esphome_lwip_wake_main_loop_any_context(void) { - if (xPortInIsrContext()) { - int px_higher_priority_task_woken = 0; - esphome_lwip_wake_main_loop_from_isr(&px_higher_priority_task_woken); - portYIELD_FROM_ISR(px_higher_priority_task_woken); - } else { - esphome_lwip_wake_main_loop(); - } -} -#endif - #endif // USE_LWIP_FAST_SELECT diff --git a/esphome/core/lwip_fast_select.h b/esphome/core/lwip_fast_select.h index 96337a1dcd1..20ac191673f 100644 --- a/esphome/core/lwip_fast_select.h +++ b/esphome/core/lwip_fast_select.h @@ -53,15 +53,6 @@ static inline bool esphome_lwip_socket_has_data(struct lwip_sock *sock) { /// The sock pointer must have been obtained from esphome_lwip_get_sock(). void esphome_lwip_hook_socket(struct lwip_sock *sock); -/// Wake the main loop task from another FreeRTOS task — costs <1 us. -/// NOT ISR-safe — must only be called from task context. -void esphome_lwip_wake_main_loop(void); - -/// Wake the main loop task from an ISR — costs <1 us. -/// ISR-safe variant using vTaskNotifyGiveFromISR(). -/// @param px_higher_priority_task_woken Set to pdTRUE if a context switch is needed. -void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); - /// Set or clear TCP_NODELAY on a socket's tcp_pcb directly. /// Must be called with the TCPIP core lock held (LwIPLock in C++). /// This bypasses lwip_setsockopt() overhead (socket lookups, switch cascade, @@ -69,13 +60,6 @@ void esphome_lwip_wake_main_loop_from_isr(int *px_higher_priority_task_woken); /// Returns true if successful, false if sock/conn/pcb is NULL or the socket is not TCP. bool esphome_lwip_set_nodelay(struct lwip_sock *sock, bool enable); -/// Wake the main loop task from any context (ISR, thread, or main loop). -/// ESP32-only: uses xPortInIsrContext() to detect ISR context. -/// LibreTiny lacks IRAM_ATTR support needed for ISR-safe paths. -#ifdef USE_ESP32 -void esphome_lwip_wake_main_loop_any_context(void); -#endif - #ifdef __cplusplus } #endif diff --git a/esphome/core/main_task.c b/esphome/core/main_task.c index 52d9c2951a6..0addb6ed867 100644 --- a/esphome/core/main_task.c +++ b/esphome/core/main_task.c @@ -1,5 +1,39 @@ #include "esphome/core/main_task.h" #if defined(USE_ESP32) || defined(USE_LIBRETINY) -TaskHandle_t esphome_main_task_handle = NULL; + +// IRAM_ATTR is defined by esp_attr.h (included via FreeRTOS headers) on ESP32. +// On LibreTiny it's not reliably available — provide a no-op fallback. +#ifndef IRAM_ATTR +#define IRAM_ATTR #endif + +TaskHandle_t esphome_main_task_handle = NULL; + +void esphome_main_task_notify(void) { + TaskHandle_t task = esphome_main_task_handle; + if (task != NULL) { + xTaskNotifyGive(task); + } +} + +void IRAM_ATTR esphome_main_task_notify_from_isr(int *px_higher_priority_task_woken) { + TaskHandle_t task = esphome_main_task_handle; + if (task != NULL) { + vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); + } +} + +#ifdef USE_ESP32 +void IRAM_ATTR esphome_main_task_notify_any_context(void) { + if (xPortInIsrContext()) { + int px_higher_priority_task_woken = 0; + esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); + portYIELD_FROM_ISR(px_higher_priority_task_woken); + } else { + esphome_main_task_notify(); + } +} +#endif + +#endif // USE_ESP32 || USE_LIBRETINY diff --git a/esphome/core/main_task.h b/esphome/core/main_task.h index d46a54cb191..4223dec3ec9 100644 --- a/esphome/core/main_task.h +++ b/esphome/core/main_task.h @@ -1,12 +1,14 @@ #pragma once -/// Main loop task handle — shared between wake.h (C++) and lwip_fast_select.c (C). -/// Set once during Application::setup() via xTaskGetCurrentTaskHandle(). +/// Main loop task handle and wake helpers — shared between wake.h (C++) and lwip_fast_select.c (C). +/// esphome_main_task_handle is set once during Application::setup() via xTaskGetCurrentTaskHandle(). + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) #ifdef USE_ESP32 #include #include -#elif defined(USE_LIBRETINY) +#else #include #include #endif @@ -17,6 +19,19 @@ extern "C" { extern TaskHandle_t esphome_main_task_handle; +/// Wake the main loop task from another FreeRTOS task. NOT ISR-safe. +void esphome_main_task_notify(void); + +/// Wake the main loop task from an ISR. ISR-safe. +void esphome_main_task_notify_from_isr(int *px_higher_priority_task_woken); + +#ifdef USE_ESP32 +/// Wake the main loop from any context (ISR or task). ESP32-only (needs xPortInIsrContext). +void esphome_main_task_notify_any_context(void); +#endif + #ifdef __cplusplus } #endif + +#endif // USE_ESP32 || USE_LIBRETINY diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 79037c82649..1b2905934da 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -25,59 +25,27 @@ namespace esphome { extern volatile bool g_main_loop_woke; #endif -// === ESP32 === -#if defined(USE_ESP32) +// === ESP32 / LibreTiny (FreeRTOS) === +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#ifdef USE_ESP32 /// Inline impl — ISR callers inline this into IRAM. inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe_inline_(int *px_higher_priority_task_woken) { - if (esphome_main_task_handle != nullptr) - vTaskNotifyGiveFromISR(esphome_main_task_handle, (BaseType_t *) px_higher_priority_task_woken); + esphome_main_task_notify_from_isr(px_higher_priority_task_woken); } /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_isrsafe(int *px_higher_priority_task_woken); -/// Inline impl — ISR callers inline this into IRAM. Uses xPortInIsrContext() to pick safe API. -inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { - if (esphome_main_task_handle == nullptr) - return; - if (xPortInIsrContext()) { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(esphome_main_task_handle, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } else { - xTaskNotifyGive(esphome_main_task_handle); - } -} +/// Inline impl — ISR callers inline this into IRAM. +inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { esphome_main_task_notify_any_context(); } /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_any_context(); +#else +/// LibreTiny: no working IRAM_ATTR, no ISR callers. +inline void wake_loop_any_context() { esphome_main_task_notify(); } +#endif -inline void wake_loop_threadsafe() { - if (esphome_main_task_handle != nullptr) - xTaskNotifyGive(esphome_main_task_handle); -} - -namespace internal { -inline void wakeable_delay(uint32_t ms) { - if (ms == 0) { - yield(); - return; - } - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms)); -} -} // namespace internal - -// === LibreTiny === -#elif defined(USE_LIBRETINY) - -inline void wake_loop_any_context() { - if (esphome_main_task_handle != nullptr) - xTaskNotifyGive(esphome_main_task_handle); -} - -inline void wake_loop_threadsafe() { - if (esphome_main_task_handle != nullptr) - xTaskNotifyGive(esphome_main_task_handle); -} +inline void wake_loop_threadsafe() { esphome_main_task_notify(); } namespace internal { inline void wakeable_delay(uint32_t ms) { From 1dc0559e20c3fe7961d0e24852860b73fbed4ab3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:40:55 -1000 Subject: [PATCH 30/45] Inline main_task notify helpers in header, simplify wake.h --- esphome/core/main_task.c | 34 ---------------------------------- esphome/core/main_task.h | 24 +++++++++++++++++++++--- esphome/core/wake.cpp | 4 ++-- esphome/core/wake.h | 7 ------- 4 files changed, 23 insertions(+), 46 deletions(-) diff --git a/esphome/core/main_task.c b/esphome/core/main_task.c index 0addb6ed867..52d9c2951a6 100644 --- a/esphome/core/main_task.c +++ b/esphome/core/main_task.c @@ -1,39 +1,5 @@ #include "esphome/core/main_task.h" #if defined(USE_ESP32) || defined(USE_LIBRETINY) - -// IRAM_ATTR is defined by esp_attr.h (included via FreeRTOS headers) on ESP32. -// On LibreTiny it's not reliably available — provide a no-op fallback. -#ifndef IRAM_ATTR -#define IRAM_ATTR -#endif - TaskHandle_t esphome_main_task_handle = NULL; - -void esphome_main_task_notify(void) { - TaskHandle_t task = esphome_main_task_handle; - if (task != NULL) { - xTaskNotifyGive(task); - } -} - -void IRAM_ATTR esphome_main_task_notify_from_isr(int *px_higher_priority_task_woken) { - TaskHandle_t task = esphome_main_task_handle; - if (task != NULL) { - vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); - } -} - -#ifdef USE_ESP32 -void IRAM_ATTR esphome_main_task_notify_any_context(void) { - if (xPortInIsrContext()) { - int px_higher_priority_task_woken = 0; - esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); - portYIELD_FROM_ISR(px_higher_priority_task_woken); - } else { - esphome_main_task_notify(); - } -} #endif - -#endif // USE_ESP32 || USE_LIBRETINY diff --git a/esphome/core/main_task.h b/esphome/core/main_task.h index 4223dec3ec9..8ea46afe35b 100644 --- a/esphome/core/main_task.h +++ b/esphome/core/main_task.h @@ -20,14 +20,32 @@ extern "C" { extern TaskHandle_t esphome_main_task_handle; /// Wake the main loop task from another FreeRTOS task. NOT ISR-safe. -void esphome_main_task_notify(void); +static inline void esphome_main_task_notify(void) { + TaskHandle_t task = esphome_main_task_handle; + if (task != NULL) { + xTaskNotifyGive(task); + } +} /// Wake the main loop task from an ISR. ISR-safe. -void esphome_main_task_notify_from_isr(int *px_higher_priority_task_woken); +static inline void esphome_main_task_notify_from_isr(int *px_higher_priority_task_woken) { + TaskHandle_t task = esphome_main_task_handle; + if (task != NULL) { + vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); + } +} #ifdef USE_ESP32 /// Wake the main loop from any context (ISR or task). ESP32-only (needs xPortInIsrContext). -void esphome_main_task_notify_any_context(void); +static inline void esphome_main_task_notify_any_context(void) { + if (xPortInIsrContext()) { + int px_higher_priority_task_woken = 0; + esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); + portYIELD_FROM_ISR(px_higher_priority_task_woken); + } else { + esphome_main_task_notify(); + } +} #endif #ifdef __cplusplus diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index 723988509b8..ddc377d191b 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -15,9 +15,9 @@ namespace esphome { // === ESP32 — IRAM_ATTR entry points === #ifdef USE_ESP32 void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { - wake_loop_isrsafe_inline_(px_higher_priority_task_woken); + esphome_main_task_notify_from_isr(px_higher_priority_task_woken); } -void IRAM_ATTR wake_loop_any_context() { wake_loop_any_context_inline_(); } +void IRAM_ATTR wake_loop_any_context() { esphome_main_task_notify_any_context(); } #endif // === ESP8266 / RP2040 === diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 1b2905934da..82375f4c0c2 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -29,15 +29,8 @@ extern volatile bool g_main_loop_woke; #if defined(USE_ESP32) || defined(USE_LIBRETINY) #ifdef USE_ESP32 -/// Inline impl — ISR callers inline this into IRAM. -inline void ESPHOME_ALWAYS_INLINE wake_loop_isrsafe_inline_(int *px_higher_priority_task_woken) { - esphome_main_task_notify_from_isr(px_higher_priority_task_woken); -} /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_isrsafe(int *px_higher_priority_task_woken); - -/// Inline impl — ISR callers inline this into IRAM. -inline void ESPHOME_ALWAYS_INLINE wake_loop_any_context_inline_() { esphome_main_task_notify_any_context(); } /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_any_context(); #else From 277c4f63434570b7890420745770f1f76a3240b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:45:44 -1000 Subject: [PATCH 31/45] Filter main_task.c and lwip_fast_select.c from platforms that don't need them --- esphome/core/config.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/esphome/core/config.py b/esphome/core/config.py index c47693c783e..62c41b254c4 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -753,6 +753,20 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, + "main_task.c": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, + "lwip_fast_select.c": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + PlatformFramework.BK72XX_ARDUINO, + PlatformFramework.RTL87XX_ARDUINO, + PlatformFramework.LN882X_ARDUINO, + }, "time_64.cpp": { PlatformFramework.ESP8266_ARDUINO, PlatformFramework.BK72XX_ARDUINO, From 113edd2076a97d13a89c83e9a33f8d99957a1bd2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:46:21 -1000 Subject: [PATCH 32/45] Fix CI: remove redundant (void) in main_task.h, restore lwip_fast_select.h include for socket monitoring --- esphome/core/application.h | 3 +++ esphome/core/main_task.h | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index c3e2a42660d..474639b556d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,6 +24,9 @@ #include "esphome/core/area.h" #endif +#ifdef USE_LWIP_FAST_SELECT +#include "esphome/core/lwip_fast_select.h" +#endif #ifdef USE_HOST #include #include diff --git a/esphome/core/main_task.h b/esphome/core/main_task.h index 8ea46afe35b..183ce650274 100644 --- a/esphome/core/main_task.h +++ b/esphome/core/main_task.h @@ -20,7 +20,7 @@ extern "C" { extern TaskHandle_t esphome_main_task_handle; /// Wake the main loop task from another FreeRTOS task. NOT ISR-safe. -static inline void esphome_main_task_notify(void) { +static inline void esphome_main_task_notify() { TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { xTaskNotifyGive(task); @@ -37,7 +37,7 @@ static inline void esphome_main_task_notify_from_isr(int *px_higher_priority_tas #ifdef USE_ESP32 /// Wake the main loop from any context (ISR or task). ESP32-only (needs xPortInIsrContext). -static inline void esphome_main_task_notify_any_context(void) { +static inline void esphome_main_task_notify_any_context() { if (xPortInIsrContext()) { int px_higher_priority_task_woken = 0; esphome_main_task_notify_from_isr(&px_higher_priority_task_woken); From ac95bde627c820282ea5c8e40c3b2f94caee8f0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:49:22 -1000 Subject: [PATCH 33/45] Move RP2040 wakeable_delay to wake.cpp (file-scope state for alarm callback) --- esphome/core/wake.cpp | 39 +++++++++++++++++++++++++++++++++++++++ esphome/core/wake.h | 30 ++---------------------------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index ddc377d191b..d09918af4ca 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -30,6 +30,45 @@ volatile bool g_main_loop_woke = false; void IRAM_ATTR wake_loop_any_context() { wake_loop_impl_(); } #endif +// === RP2040 — wakeable_delay (needs file-scope state for alarm callback) === +#ifdef USE_RP2040 +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_delay_expired = false; + +static int64_t alarm_callback_(alarm_id_t id, void *user_data) { + (void) id; + (void) user_data; + s_delay_expired = true; + __sev(); + return 0; +} + +namespace internal { +void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + s_delay_expired = false; + alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback_, nullptr, true); + if (alarm <= 0) { + delay(ms); + return; + } + while (!g_main_loop_woke && !s_delay_expired) { + __wfe(); + } + if (!s_delay_expired) + cancel_alarm(alarm); + g_main_loop_woke = false; +} +} // namespace internal +#endif // USE_RP2040 + // === Host (UDP loopback socket) === #ifdef USE_HOST void wake_loop_threadsafe() { diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 82375f4c0c2..961edf5adbd 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -89,35 +89,9 @@ inline void wake_loop_threadsafe() { __sev(); } +/// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake.cpp. namespace internal { -inline void wakeable_delay(uint32_t ms) { - static volatile bool s_delay_expired = false; - if (ms == 0) { - yield(); - return; - } - if (g_main_loop_woke) { - g_main_loop_woke = false; - return; - } - s_delay_expired = false; - auto alarm_cb = [](alarm_id_t, void *) -> int64_t { - s_delay_expired = true; - __sev(); - return 0; - }; - alarm_id_t alarm = add_alarm_in_ms(ms, alarm_cb, nullptr, true); - if (alarm <= 0) { - delay(ms); - return; - } - while (!g_main_loop_woke && !s_delay_expired) { - __wfe(); - } - if (!s_delay_expired) - cancel_alarm(alarm); - g_main_loop_woke = false; -} +void wakeable_delay(uint32_t ms); } // namespace internal // === Host (UDP loopback socket) === From 36b3d35d1591728ce9c147bcdecadb54bf1acc09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:58:33 -1000 Subject: [PATCH 34/45] Fix Zephyr build: add fallback wakeable_delay and wake_loop_threadsafe --- esphome/core/wake.h | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 961edf5adbd..fe49038d65d 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -94,14 +94,29 @@ namespace internal { void wakeable_delay(uint32_t ms); } // namespace internal -// === Host (UDP loopback socket) === +// === Host / Zephyr / other === #else -/// Defined in wake.cpp. +#ifdef USE_HOST +/// Host: wakes select() via UDP loopback socket. Defined in wake.cpp. void wake_loop_threadsafe(); +#else +/// Fallback for platforms without a specific wake mechanism. +inline void wake_loop_threadsafe() {} +#endif inline void wake_loop_any_context() { wake_loop_threadsafe(); } +namespace internal { +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + delay(ms); +} +} // namespace internal + #endif } // namespace esphome From 58ab4866a6b26f218a99b507f8f75f0fc2d20dd6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 11:59:04 -1000 Subject: [PATCH 35/45] Document Zephyr as the only platform without wake --- esphome/core/wake.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index fe49038d65d..13052160d22 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -101,7 +101,9 @@ void wakeable_delay(uint32_t ms); /// Host: wakes select() via UDP loopback socket. Defined in wake.cpp. void wake_loop_threadsafe(); #else -/// Fallback for platforms without a specific wake mechanism. +/// Zephyr is currently the only platform without a wake mechanism. +/// wake_loop_threadsafe() is a no-op and wakeable_delay() falls back to delay(). +/// TODO: implement proper Zephyr wake using k_poll / k_sem or similar. inline void wake_loop_threadsafe() {} #endif From c74889de67e2cd18317b3dcb86a95fdefb32dd77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 12:10:42 -1000 Subject: [PATCH 36/45] Fix clang-tidy: rename wake_loop_impl_ to wake_loop_impl (no trailing underscore on free functions) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 2 +- esphome/core/wake.cpp | 2 +- esphome/core/wake.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 35224442eec..0ec16b02519 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -44,7 +44,7 @@ void socket_delay(uint32_t ms) { void IRAM_ATTR socket_wake() { s_socket_woke = true; // Inline impl — this is IRAM_ATTR so the inlined code stays in IRAM - esphome::wake_loop_impl_(); + esphome::wake_loop_impl(); } #elif defined(USE_RP2040) // RP2040 (non-FreeRTOS) socket wake using hardware WFE/SEV instructions. diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index d09918af4ca..ecc7adcb4dc 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -27,7 +27,7 @@ volatile bool g_main_loop_woke = false; #endif #ifdef USE_ESP8266 -void IRAM_ATTR wake_loop_any_context() { wake_loop_impl_(); } +void IRAM_ATTR wake_loop_any_context() { wake_loop_impl(); } #endif // === RP2040 — wakeable_delay (needs file-scope state for alarm callback) === diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 13052160d22..7f4fcd60da7 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -54,7 +54,7 @@ inline void wakeable_delay(uint32_t ms) { #elif defined(USE_ESP8266) /// Inline implementation — IRAM callers inline this directly. -inline void ESPHOME_ALWAYS_INLINE wake_loop_impl_() { +inline void ESPHOME_ALWAYS_INLINE wake_loop_impl() { g_main_loop_woke = true; esp_schedule(); } @@ -63,7 +63,7 @@ inline void ESPHOME_ALWAYS_INLINE wake_loop_impl_() { void wake_loop_any_context(); /// Non-ISR: always inline. -inline void wake_loop_threadsafe() { wake_loop_impl_(); } +inline void wake_loop_threadsafe() { wake_loop_impl(); } namespace internal { inline void wakeable_delay(uint32_t ms) { From 59969d4743d73cc9abf800b0d445af69e8123754 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 12:22:44 -1000 Subject: [PATCH 37/45] Simplify loop(): single yield_with_select_ call --- esphome/core/application.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 474639b556d..7826a49b5f4 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -876,21 +876,17 @@ inline void ESPHOME_ALWAYS_INLINE Application::loop() { #endif // Use the last component's end time instead of calling millis() again + uint32_t delay_time = 0; auto elapsed = last_op_end_time - this->last_loop_; - if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { - // Even if we overran the loop interval, we still need to select() - // to know if any sockets have data ready - this->yield_with_select_(0); - } else { - uint32_t delay_time = this->loop_interval_ - elapsed; + if (elapsed < this->loop_interval_ && !HighFrequencyLoopRequester::is_high_frequency()) { + delay_time = this->loop_interval_ - elapsed; uint32_t next_schedule = this->scheduler.next_schedule_in(last_op_end_time).value_or(delay_time); // next_schedule is max 0.5*delay_time // otherwise interval=0 schedules result in constant looping with almost no sleep next_schedule = std::max(next_schedule, delay_time / 2); delay_time = std::min(next_schedule, delay_time); - - this->yield_with_select_(delay_time); } + this->yield_with_select_(delay_time); this->last_loop_ = last_op_end_time; if (this->dump_config_at_ < this->components_.size()) { From 18497d5a7f9636230d57117a7691b7c7f2b8aa06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 12:41:30 -1000 Subject: [PATCH 38/45] Add backward-compat defines in require_wake_loop_threadsafe, remove pointless host socket consumption --- esphome/components/socket/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 7d5b9d95a2e..abbbb0f056f 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -122,15 +122,20 @@ def get_socket_counts() -> SocketCounts: def require_wake_loop_threadsafe() -> None: """Deprecated: wake loop support is now always available on all platforms. - This function is a no-op kept for backward compatibility with external components. - Remove before 2026.12.0. + This function adds backward-compatible defines so external components + that check #ifdef USE_WAKE_LOOP_THREADSAFE / USE_SOCKET_SELECT_SUPPORT + continue to compile. Remove before 2026.12.0. """ # Remove before 2026.12.0 _LOGGER.warning( "require_wake_loop_threadsafe() is deprecated and no longer needed. " - "Wake loop support is now always available. Remove this call. " + "Wake loop support is now always available. Remove this call and any " + "#ifdef USE_SOCKET_SELECT_SUPPORT / USE_WAKE_LOOP_THREADSAFE guards. " "This will be removed in 2026.12.0." ) + # Add deprecated defines for backward compat with external component C++ code + cg.add_define("USE_WAKE_LOOP_THREADSAFE") + cg.add_define("USE_SOCKET_SELECT_SUPPORT") CONFIG_SCHEMA = cv.Schema( @@ -168,10 +173,6 @@ async def to_code(config): # Only when not using lwip_tcp, which does not provide select() support. if (CORE.is_esp32 or CORE.is_libretiny) and impl != IMPLEMENTATION_LWIP_TCP: cg.add_build_flag("-DUSE_LWIP_FAST_SELECT") - if CORE.is_host: - # Host platform: uses select() syscall for socket monitoring - # and a UDP loopback socket for wake_loop_threadsafe() - consume_sockets(1, "socket.wake_loop_threadsafe", SocketType.UDP)({}) def FILTER_SOURCE_FILES() -> list[str]: From e8a8add6c37e0fc397e2837512cc9b2268f0f850 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 12:48:15 -1000 Subject: [PATCH 39/45] Remove duplicate wake code: socket_delay/socket_wake delegate to core wake mechanism --- .../components/socket/lwip_raw_tcp_impl.cpp | 104 ++---------------- 1 file changed, 11 insertions(+), 93 deletions(-) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 0ec16b02519..9576c75bf7a 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -20,100 +20,18 @@ namespace esphome::socket { +#if defined(USE_ESP8266) || defined(USE_RP2040) +// socket_delay() and socket_wake() delegate to the core wake mechanism. +// socket_wake() calls wake_loop_any_context() which sets g_main_loop_woke, +// and socket_delay() calls wakeable_delay() which checks that flag. + +void socket_delay(uint32_t ms) { esphome::internal::wakeable_delay(ms); } + #ifdef USE_ESP8266 -// Flag to signal socket activity - checked by socket_delay() to exit early -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_socket_woke = false; - -void socket_delay(uint32_t ms) { - // Use esp_delay with a callback that checks if socket data arrived. - // This allows the delay to exit early when socket_wake() is called by - // lwip recv_fn/accept_fn callbacks, reducing socket latency. - // - // When ms is 0, we must use delay(0) because esp_delay(0, callback) - // exits immediately without yielding, which can cause watchdog timeouts - // when the main loop runs in high-frequency mode (e.g., during light effects). - if (ms == 0) { - delay(0); - return; - } - s_socket_woke = false; - esp_delay(ms, []() { return !s_socket_woke; }); -} - -void IRAM_ATTR socket_wake() { - s_socket_woke = true; - // Inline impl — this is IRAM_ATTR so the inlined code stays in IRAM - esphome::wake_loop_impl(); -} -#elif defined(USE_RP2040) -// RP2040 (non-FreeRTOS) socket wake using hardware WFE/SEV instructions. -// -// Same pattern as ESP8266's esp_delay()/esp_schedule(): set a one-shot timer, -// then sleep with __wfe(). Wake on either: -// - Timer alarm fires → callback calls __sev() → __wfe() returns → timeout -// - Socket data arrives → LWIP callback calls socket_wake() → __sev() → __wfe() returns → early wake -// -// CYW43 WiFi chip communicates via SPI interrupts on core 0. When data arrives, -// the GPIO interrupt fires → async_context pendsv processes CYW43/LWIP → recv/accept -// callbacks call socket_wake() → __sev() wakes the main loop from __wfe() sleep. -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_socket_woke = false; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -static volatile bool s_delay_expired = false; - -static int64_t alarm_callback(alarm_id_t id, void *user_data) { - (void) id; - (void) user_data; - s_delay_expired = true; - // Wake the main loop from __wfe() sleep — timeout expired. - __sev(); - // Return 0 = don't reschedule (one-shot) - return 0; -} - -void socket_delay(uint32_t ms) { - if (ms == 0) { - yield(); - return; - } - // If a wake was already signalled, consume it and return immediately - // instead of going to sleep. This avoids losing a wake that arrived - // between loop iterations. - if (s_socket_woke) { - s_socket_woke = false; - return; - } - // Don't clear s_socket_woke here — if an IRQ fires between the check above - // and the while loop below, the while condition sees it immediately. Clearing - // here would lose that wake and sleep until the timer fires. - s_delay_expired = false; - // Set a one-shot timer to wake us after the timeout. - // add_alarm_in_ms returns >0 on success, 0 if time already passed, <0 on error. - alarm_id_t alarm = add_alarm_in_ms(ms, alarm_callback, nullptr, true); - if (alarm <= 0) { - delay(ms); - return; - } - // Sleep until woken by either the timer alarm or socket_wake(). - // __wfe() may return spuriously (stale event register, other interrupts), - // so we loop checking both flags. - while (!s_socket_woke && !s_delay_expired) { - __wfe(); - } - // Cancel timer if we woke early (socket data arrived before timeout) - if (!s_delay_expired) - cancel_alarm(alarm); - s_socket_woke = false; // consume the wake for next call -} - -// No IRAM_ATTR equivalent needed: on RP2040, CYW43 async_context runs LWIP -// callbacks via pendsv (not hard IRQ), so they execute from flash safely. -void socket_wake() { - s_socket_woke = true; - // Also set core wake flag so wakeable_delay() breaks out - esphome::wake_loop_any_context(); -} +void IRAM_ATTR socket_wake() { esphome::wake_loop_impl(); } +#else +void socket_wake() { esphome::wake_loop_any_context(); } +#endif #endif // ---- LWIP thread safety ---- From 619370170c90e304cb2537c91a5d542de7cf9f86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 12:56:13 -1000 Subject: [PATCH 40/45] Remove socket_wake/socket_delay: callers use wake_loop_any_context/wakeable_delay directly --- .../components/esphome/ota/ota_esphome.cpp | 2 +- .../components/socket/lwip_raw_tcp_impl.cpp | 28 ++++--------------- esphome/components/socket/lwip_raw_tcp_impl.h | 2 +- esphome/components/socket/socket.h | 14 ---------- 4 files changed, 7 insertions(+), 39 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 972d2b2b8d5..af9b8ee19a1 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -262,7 +262,7 @@ void ESPHomeOTAComponent::handle_data_() { /// BSD sockets (ESP32): setblocking(true) makes read/write block /// lwip sockets (LT): setblocking(true) makes read/write block /// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses - /// socket_delay()/socket_wake() in read(); + /// wakeable_delay() in read(); /// write() always returns immediately ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 9576c75bf7a..86131d3ddb9 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -20,20 +20,6 @@ namespace esphome::socket { -#if defined(USE_ESP8266) || defined(USE_RP2040) -// socket_delay() and socket_wake() delegate to the core wake mechanism. -// socket_wake() calls wake_loop_any_context() which sets g_main_loop_woke, -// and socket_delay() calls wakeable_delay() which checks that flag. - -void socket_delay(uint32_t ms) { esphome::internal::wakeable_delay(ms); } - -#ifdef USE_ESP8266 -void IRAM_ATTR socket_wake() { esphome::wake_loop_impl(); } -#else -void socket_wake() { esphome::wake_loop_any_context(); } -#endif -#endif - // ---- LWIP thread safety ---- // // On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1. @@ -462,10 +448,8 @@ err_t LWIPRawImpl::recv_fn(struct pbuf *pb, err_t err) { } else { pbuf_cat(this->rx_buf_, pb); } -#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can process the received data. - socket_wake(); -#endif + esphome::wake_loop_any_context(); return ERR_OK; } @@ -474,15 +458,15 @@ void LWIPRawImpl::wait_for_data_() { // (needs async_context lock). // // Loop until data arrives, connection closes, or the full timeout elapses. - // socket_delay() may return early due to other sockets waking the global - // socket_wake() flag, so we re-enter for the remaining time. + // wakeable_delay() may return early due to any wake source, + // so we re-enter for the remaining time. uint32_t timeout_ms = this->recv_timeout_cs_ * 10; uint32_t start = millis(); while (this->waiting_for_data_()) { uint32_t elapsed = millis() - start; if (elapsed >= timeout_ms) break; - socket_delay(timeout_ms - elapsed); + esphome::internal::wakeable_delay(timeout_ms - elapsed); } } @@ -870,10 +854,8 @@ 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_); -#if (defined(USE_ESP8266) || defined(USE_RP2040)) // Wake the main loop immediately so it can accept the new connection. - socket_wake(); -#endif + esphome::wake_loop_any_context(); return ERR_OK; } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.h b/esphome/components/socket/lwip_raw_tcp_impl.h index 3c27d71062f..e2dcb80d32b 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.h +++ b/esphome/components/socket/lwip_raw_tcp_impl.h @@ -109,7 +109,7 @@ class LWIPRawImpl : public LWIPRawCommon { return -1; } // Raw TCP doesn't use a blocking flag directly. Blocking behavior - // is provided by SO_RCVTIMEO which makes read() wait via socket_delay(). + // is provided by SO_RCVTIMEO which makes read() wait via wakeable_delay(). return 0; } int loop() { return 0; } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index a681bc228b6..9ea71321e0b 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -120,19 +120,5 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po /// Format sockaddr into caller-provided buffer, returns length written (excluding null) size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span buf); -#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_SOCKET_IMPL_LWIP_TCP) -/// Delay that can be woken early by socket activity. -/// On ESP8266, uses esp_delay() with a callback that checks socket activity. -/// On RP2040, uses __wfe() (Wait For Event) to truly sleep until an interrupt -/// (for example, CYW43 GPIO or a timer alarm) fires and wakes the CPU. -void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) - -/// Signal socket/IO activity and wake the main loop early. -/// On ESP8266: sets flag + esp_schedule(). -/// On RP2040: sets flag + __sev() (Send Event) to wake from __wfe(). -/// ISR-safe on both platforms. -void socket_wake(); // NOLINT(readability-redundant-declaration) -#endif - } // namespace esphome::socket #endif From 670a23b4c1c7823dd80436cfeec612f43eacb7c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 12:59:34 -1000 Subject: [PATCH 41/45] Fix LibreTiny ISR safety: use vTaskNotifyGiveFromISR in wake_loop_any_context, update stale comment --- esphome/core/lwip_fast_select.c | 10 +++++----- esphome/core/wake.h | 11 +++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/esphome/core/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index 82b68aa738b..bb3acbafcb1 100644 --- a/esphome/core/lwip_fast_select.c +++ b/esphome/core/lwip_fast_select.c @@ -63,12 +63,12 @@ // // Shared state and safety rationale: // -// esphome_main_task_handle (TaskHandle_t, 4 bytes): -// Written once by main loop in init(). Read by TCP/IP thread (in callback) -// and background tasks (in wake). -// Safe: write-once-then-read pattern. Socket hooks may run before init(), +// esphome_main_task_handle (TaskHandle_t, 4 bytes, defined in main_task.c): +// Written once by main loop in Application::setup(). Read by TCP/IP thread +// (in callback) and background tasks (in wake). +// Safe: write-once-then-read pattern. Socket hooks may run before setup(), // but the NULL check on esphome_main_task_handle in the callback provides correct -// degraded behavior — notifications are simply skipped until init() completes. +// degraded behavior — notifications are simply skipped until setup() completes. // // s_original_callback (netconn_callback, 4-byte function pointer): // Written by main loop in hook_socket() (only when NULL — set once). diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 7f4fcd60da7..bed1d64561d 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -34,8 +34,15 @@ void wake_loop_isrsafe(int *px_higher_priority_task_woken); /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_any_context(); #else -/// LibreTiny: no working IRAM_ATTR, no ISR callers. -inline void wake_loop_any_context() { esphome_main_task_notify(); } +/// LibreTiny: GPIO ISRs are real hardware interrupts, so use ISR-safe API. +/// vTaskNotifyGiveFromISR is safe from both ISR and task context on ARM Cortex-M. +inline void wake_loop_any_context() { + if (esphome_main_task_handle != NULL) { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + vTaskNotifyGiveFromISR(esphome_main_task_handle, &xHigherPriorityTaskWoken); + portYIELD_FROM_ISR(xHigherPriorityTaskWoken); + } +} #endif inline void wake_loop_threadsafe() { esphome_main_task_notify(); } From e2c71419b643a4f5760358180bfab696d650ed7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 13:00:57 -1000 Subject: [PATCH 42/45] Revert LibreTiny ISR change: FreeRTOS port lacks vTaskNotifyGiveFromISR/portYIELD_FROM_ISR --- esphome/core/wake.h | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index bed1d64561d..ba21ebf0ee4 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -34,15 +34,10 @@ void wake_loop_isrsafe(int *px_higher_priority_task_woken); /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_any_context(); #else -/// LibreTiny: GPIO ISRs are real hardware interrupts, so use ISR-safe API. -/// vTaskNotifyGiveFromISR is safe from both ISR and task context on ARM Cortex-M. -inline void wake_loop_any_context() { - if (esphome_main_task_handle != NULL) { - BaseType_t xHigherPriorityTaskWoken = pdFALSE; - vTaskNotifyGiveFromISR(esphome_main_task_handle, &xHigherPriorityTaskWoken); - portYIELD_FROM_ISR(xHigherPriorityTaskWoken); - } -} +/// LibreTiny: GPIO ISRs are real hardware interrupts but the FreeRTOS port +/// may not provide vTaskNotifyGiveFromISR/portYIELD_FROM_ISR. +/// xTaskNotifyGive is used as the best available option. +inline void wake_loop_any_context() { esphome_main_task_notify(); } #endif inline void wake_loop_threadsafe() { esphome_main_task_notify(); } From 0b90dbf328a7bb60fb98ce95ab5e823203887e5b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 13:03:11 -1000 Subject: [PATCH 43/45] Document LibreTiny IRAM_ATTR and ISR-safe wake limitations --- esphome/core/wake.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index ba21ebf0ee4..6df3df891cd 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -34,9 +34,9 @@ void wake_loop_isrsafe(int *px_higher_priority_task_woken); /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_any_context(); #else -/// LibreTiny: GPIO ISRs are real hardware interrupts but the FreeRTOS port -/// may not provide vTaskNotifyGiveFromISR/portYIELD_FROM_ISR. -/// xTaskNotifyGive is used as the best available option. +/// LibreTiny: IRAM_ATTR is not functional and the FreeRTOS port does not +/// provide vTaskNotifyGiveFromISR/portYIELD_FROM_ISR, so ISR-safe wake +/// is not possible. xTaskNotifyGive is used as the best available option. inline void wake_loop_any_context() { esphome_main_task_notify(); } #endif From 2ead905e4445d7ebe57940103cdb1ccaa1f3dbdc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 13:13:41 -1000 Subject: [PATCH 44/45] Fix ESP8266 wakeable_delay wake loss, use BaseType_t* for ISR-safe API --- esphome/core/application.h | 2 +- esphome/core/main_task.h | 4 ++-- esphome/core/wake.cpp | 2 +- esphome/core/wake.h | 7 +++++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 7826a49b5f4..aa6ec3ebd3f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -553,7 +553,7 @@ class Application { #ifdef USE_ESP32 /// Wake from ISR (ESP32 only). - static void wake_loop_isrsafe(int *px) { esphome::wake_loop_isrsafe(px); } + static void wake_loop_isrsafe(BaseType_t *px) { esphome::wake_loop_isrsafe(px); } #endif /// Wake from any context (ISR, thread, callback). diff --git a/esphome/core/main_task.h b/esphome/core/main_task.h index 183ce650274..ed2885d2e25 100644 --- a/esphome/core/main_task.h +++ b/esphome/core/main_task.h @@ -28,10 +28,10 @@ static inline void esphome_main_task_notify() { } /// Wake the main loop task from an ISR. ISR-safe. -static inline void esphome_main_task_notify_from_isr(int *px_higher_priority_task_woken) { +static inline void esphome_main_task_notify_from_isr(BaseType_t *px_higher_priority_task_woken) { TaskHandle_t task = esphome_main_task_handle; if (task != NULL) { - vTaskNotifyGiveFromISR(task, (BaseType_t *) px_higher_priority_task_woken); + vTaskNotifyGiveFromISR(task, px_higher_priority_task_woken); } } diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp index ecc7adcb4dc..b6b59b59909 100644 --- a/esphome/core/wake.cpp +++ b/esphome/core/wake.cpp @@ -14,7 +14,7 @@ namespace esphome { // === ESP32 — IRAM_ATTR entry points === #ifdef USE_ESP32 -void IRAM_ATTR wake_loop_isrsafe(int *px_higher_priority_task_woken) { +void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken) { esphome_main_task_notify_from_isr(px_higher_priority_task_woken); } void IRAM_ATTR wake_loop_any_context() { esphome_main_task_notify_any_context(); } diff --git a/esphome/core/wake.h b/esphome/core/wake.h index 6df3df891cd..fdbe2088c4e 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -30,7 +30,7 @@ extern volatile bool g_main_loop_woke; #ifdef USE_ESP32 /// IRAM_ATTR entry point — defined in wake.cpp. -void wake_loop_isrsafe(int *px_higher_priority_task_woken); +void wake_loop_isrsafe(BaseType_t *px_higher_priority_task_woken); /// IRAM_ATTR entry point — defined in wake.cpp. void wake_loop_any_context(); #else @@ -73,7 +73,10 @@ inline void wakeable_delay(uint32_t ms) { delay(0); return; } - g_main_loop_woke = false; + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } esp_delay(ms, []() { return !g_main_loop_woke; }); } } // namespace internal From c17df9aaa3a1042c884a19d2a1a7afafabeb8c52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 13:19:03 -1000 Subject: [PATCH 45/45] Deduplicate RP2040 wake: wake_loop_threadsafe delegates to wake_loop_any_context --- esphome/core/wake.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/esphome/core/wake.h b/esphome/core/wake.h index fdbe2088c4e..a8c9b7ad08b 100644 --- a/esphome/core/wake.h +++ b/esphome/core/wake.h @@ -89,10 +89,7 @@ inline void wake_loop_any_context() { __sev(); } -inline void wake_loop_threadsafe() { - g_main_loop_woke = true; - __sev(); -} +inline void wake_loop_threadsafe() { wake_loop_any_context(); } /// RP2040 wakeable delay uses file-scope state (alarm callback + flag) — defined in wake.cpp. namespace internal {