[core] Move wake_loop out of socket component into core (#15446)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
J. Nick Koston
2026-04-06 11:01:03 -10:00
committed by GitHub
parent 62b4b250c7
commit ab45591507
38 changed files with 397 additions and 618 deletions
-6
View File
@@ -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)
-6
View File
@@ -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
+2 -3
View File
@@ -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)
@@ -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);
@@ -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;
+2 -6
View File
@@ -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))
@@ -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; }
@@ -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))
@@ -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
+1 -4
View File
@@ -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)
@@ -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;
-6
View File
@@ -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):
@@ -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
}
}
@@ -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)
@@ -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
}
}
+14 -34
View File
@@ -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.
+6 -105
View File
@@ -8,6 +8,7 @@
#include <sys/time.h>
#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;
}
@@ -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; }
+1 -1
View File
@@ -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); }
+1 -15
View File
@@ -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<char, SOCKADDR_STR_LEN> 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
-14
View File
@@ -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")
@@ -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_() {
+1 -7
View File
@@ -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 ():
@@ -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,
+1 -6
View File
@@ -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]):
+1 -3
View File
@@ -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
+23 -68
View File
@@ -28,22 +28,8 @@
#include "esphome/components/socket/socket.h"
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
#ifdef USE_HOST
#include <cerrno>
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
// LWIP sockets implementation
#include <lwip/sockets.h>
#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS)
// BSD sockets implementation
#ifdef USE_ESP32
// ESP32 "BSD sockets" are actually LWIP under the hood
#include <lwip/sockets.h>
#else
// True BSD sockets (e.g., host platform)
#include <sys/select.h>
#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<char, BUILD_TIME_STR_SIZE> buffer) {
ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size());
+41 -87
View File
@@ -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 <freertos/FreeRTOS.h>
#include <freertos/task.h>
#else
#include <FreeRTOS.h>
#include <task.h>
#endif
#else
#ifdef USE_HOST
#include <sys/select.h>
#ifdef USE_WAKE_LOOP_THREADSAFE
#include <lwip/sockets.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#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<Component *> looping_components_{};
#ifdef USE_LWIP_FAST_SELECT
std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read
#elif defined(USE_SOCKET_SELECT_SUPPORT)
#elif defined(USE_HOST)
std::vector<int> 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
+2 -8
View File
@@ -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) {
+14
View File
@@ -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,
+1 -4
View File
@@ -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
+9 -48
View File
@@ -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 <stddef.h>
// 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
-20
View File
@@ -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
+5
View File
@@ -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
+55
View File
@@ -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 <freertos/FreeRTOS.h>
#include <freertos/task.h>
#else
#include <FreeRTOS.h>
#include <task.h>
#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
+82
View File
@@ -0,0 +1,82 @@
#include "esphome/core/wake.h"
#include "esphome/core/hal.h"
#ifdef USE_ESP8266
#include <coredecls.h>
#endif
#ifdef USE_HOST
#include "esphome/core/application.h"
#include <sys/socket.h>
#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
+126
View File
@@ -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 <coredecls.h>
#elif defined(USE_RP2040)
#include <hardware/sync.h>
#include <pico/time.h>
#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
@@ -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