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