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 68e5fffe2bc..0280439731c 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -599,9 +599,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 @@ -622,9 +620,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 @@ -633,9 +629,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/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/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..abbbb0f056f 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,22 @@ 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 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. """ - - # 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 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( @@ -184,10 +166,8 @@ 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. diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 3bcbd880850..86131d3ddb9 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 @@ -19,102 +20,6 @@ namespace esphome::socket { -#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; - esp_schedule(); -} -#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; - // Wake the main loop from __wfe() sleep. __sev() is a global event that - // wakes any core sleeping in __wfe(). This is ISR-safe. - __sev(); -} -#endif - // ---- LWIP thread safety ---- // // On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1. @@ -543,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; } @@ -555,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); } } @@ -951,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.cpp b/esphome/components/socket/socket.cpp index bfb6ae8e130..bc43b2746ee 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_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..9ea71321e0b 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); @@ -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 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..cd758598801 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -28,22 +28,8 @@ #include "esphome/components/socket/socket.h" #endif -#ifdef USE_SOCKET_SELECT_SUPPORT +#ifdef USE_HOST #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 { @@ -128,13 +114,11 @@ void Application::setup() { clear_setup_priority_overrides(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(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(); +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + // Save main loop task handle for wake_loop_*() / fast select FreeRTOS notifications. + esphome_main_task_handle = xTaskGetCurrentTaskHandle(); #endif -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST // Set up wake socket for waking main loop from tasks (platforms without fast select only) this->setup_wake_loop_threadsafe_(); #endif @@ -490,23 +474,17 @@ 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 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; @@ -547,7 +525,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_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]] { @@ -570,11 +548,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 @@ -597,7 +571,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_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. @@ -618,18 +592,13 @@ 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 +#ifdef USE_HOST 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; @@ -638,12 +607,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; } @@ -652,50 +621,36 @@ 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; } } -void Application::wake_loop_threadsafe() { - // Called from FreeRTOS task context when events need immediate processing - // Wakes up lwip_select() in main loop by writing to connected loopback socket - if (this->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); - } -} -#endif // USE_LWIP_FAST_SELECT - -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +#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 6cc61bc9547..6b2969b4907 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -24,32 +24,21 @@ #include "esphome/core/area.h" #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 +#ifdef USE_HOST #include -#ifdef USE_WAKE_LOOP_THREADSAFE -#include +#include +#include +#include +#include +#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 @@ -124,7 +113,7 @@ void socket_delay(uint32_t ms); // NOLINT(readability-redundant-declaration) #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 @@ -550,7 +539,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,43 +547,21 @@ 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 - -#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); - } + /// 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_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 only). + static void IRAM_ATTR wake_loop_isrsafe(BaseType_t *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(); } -#endif + /// Wake from any context (ISR, thread, callback). + static void IRAM_ATTR wake_loop_any_context() { esphome::wake_loop_any_context(); } protected: friend Component; -#if defined(USE_SOCKET_SELECT_SUPPORT) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST friend bool socket::socket_ready_fd(int fd, bool loop_monitored); #endif #ifdef USE_RUNTIME_STATS @@ -602,8 +569,11 @@ class Application { #endif friend void ::setup(); friend void ::original_setup(); +#ifdef USE_HOST + 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_HOST bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); } #endif @@ -648,14 +618,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_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 -#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#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 @@ -685,13 +655,11 @@ 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 -#if defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#ifdef USE_HOST int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks -#endif #endif // StringRef members (8 bytes each: pointer + size) @@ -702,7 +670,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_HOST int max_fd_{-1}; // Highest file descriptor number for select() #endif @@ -718,11 +686,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_HOST 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_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 @@ -815,7 +783,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) +#ifdef USE_HOST // Inline implementations for hot-path functions // drain_wake_notifications_() is called on every loop iteration @@ -832,15 +800,15 @@ 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 } } } -#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT) +#endif // USE_HOST 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) +#ifdef USE_HOST // Drain wake notifications first to clear socket for next wake this->drain_wake_notifications_(); #endif @@ -908,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()) { @@ -931,9 +895,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_HOST 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]] { @@ -953,20 +917,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 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). - 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); -#else - // No select support, use regular delay - delay(delay_ms); + // Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout. #endif + esphome::internal::wakeable_delay(delay_ms); } -#endif // !defined(USE_SOCKET_SELECT_SUPPORT) || defined(USE_LWIP_FAST_SELECT) +#endif // !USE_HOST } // namespace esphome diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 0f68f0c8e02..deda42b0a7d 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -299,7 +299,7 @@ void Component::enable_loop_slow_path_() { this->set_component_state_(COMPONENT_STATE_LOOP); App.enable_component_loop_(this); } -void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { +void IRAM_ATTR Component::enable_loop_soon_any_context() { // This method is thread and ISR-safe because: // 1. Only performs simple assignments to volatile variables (atomic on all platforms) // 2. No read-modify-write operations that could be interrupted @@ -311,15 +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)) // 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 + wake_loop_any_context(); } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { 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, diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d92fb6e98a9..9c90790f3a8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -252,9 +252,8 @@ #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_WAKE_LOOP_THREADSAFE + #define USE_SPEAKER #define USE_SPEAKER_MEDIA_PLAYER_ON_OFF #define USE_SPI @@ -379,7 +378,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 @@ -391,7 +389,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/lwip_fast_select.c b/esphome/core/lwip_fast_select.c index a695fa396bc..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: // -// s_main_loop_task (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 -// degraded behavior — notifications are simply skipped until init() completes. +// 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 setup() completes. // // s_original_callback (netconn_callback, 4-byte function pointer): // Written by main loop in hook_socket() (only when NULL — set once). @@ -123,15 +123,10 @@ #endif #include "esphome/core/lwip_fast_select.h" +#include "esphome/core/main_task.h" #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. @@ -157,8 +152,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 +171,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: @@ -232,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 = s_main_loop_task; - 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 = s_main_loop_task; - 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 50706ba9f69..20ac191673f 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(). @@ -57,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, @@ -73,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 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..ed2885d2e25 --- /dev/null +++ b/esphome/core/main_task.h @@ -0,0 +1,55 @@ +#pragma once + +/// 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 +#else +#include +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +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() { + TaskHandle_t task = esphome_main_task_handle; + if (task != NULL) { + xTaskNotifyGive(task); + } +} + +/// Wake the main loop task from an ISR. ISR-safe. +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, px_higher_priority_task_woken); + } +} + +#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() { + 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 +} +#endif + +#endif // USE_ESP32 || USE_LIBRETINY diff --git a/esphome/core/wake.cpp b/esphome/core/wake.cpp new file mode 100644 index 00000000000..b6b59b59909 --- /dev/null +++ b/esphome/core/wake.cpp @@ -0,0 +1,82 @@ +#include "esphome/core/wake.h" +#include "esphome/core/hal.h" + +#ifdef USE_ESP8266 +#include +#endif + +#ifdef USE_HOST +#include "esphome/core/application.h" +#include +#endif + +namespace esphome { + +// === ESP32 — IRAM_ATTR entry points === +#ifdef USE_ESP32 +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(); } +#endif + +// === ESP8266 / RP2040 === +#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 +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() { + if (App.wake_socket_fd_ >= 0) { + const char dummy = 1; + ::send(App.wake_socket_fd_, &dummy, 1, 0); + } +} +#endif + +} // namespace esphome diff --git a/esphome/core/wake.h b/esphome/core/wake.h new file mode 100644 index 00000000000..a8c9b7ad08b --- /dev/null +++ b/esphome/core/wake.h @@ -0,0 +1,126 @@ +#pragma once + +/// @file wake.h +/// Platform-specific main loop wake primitives. +/// Always available on all platforms — no opt-in needed. + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#include "esphome/core/main_task.h" +#endif +#ifdef USE_ESP8266 +#include +#elif defined(USE_RP2040) +#include +#include +#endif + +namespace esphome { + +// === Wake flag for ESP8266/RP2040 === +#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) === +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +#ifdef USE_ESP32 +/// IRAM_ATTR entry point — defined in wake.cpp. +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 +/// 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 + +inline void wake_loop_threadsafe() { esphome_main_task_notify(); } + +namespace internal { +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + yield(); + return; + } + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ms)); +} +} // namespace internal + +// === 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(); } + +namespace internal { +inline void wakeable_delay(uint32_t ms) { + if (ms == 0) { + delay(0); + return; + } + if (g_main_loop_woke) { + g_main_loop_woke = false; + return; + } + esp_delay(ms, []() { return !g_main_loop_woke; }); +} +} // namespace internal + +// === RP2040 === +#elif defined(USE_RP2040) + +inline void wake_loop_any_context() { + 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 { +void wakeable_delay(uint32_t ms); +} // namespace internal + +// === Host / Zephyr / other === +#else + +#ifdef USE_HOST +/// Host: wakes select() via UDP loopback socket. Defined in wake.cpp. +void wake_loop_threadsafe(); +#else +/// 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 + +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 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