[core] Move wake_loop out of socket component into core

Wake primitives (esp_schedule, __sev, FreeRTOS task notifications)
are platform SDK functions that don't depend on the socket/network
stack. Move them to esphome/core/wake.h/.cpp so they work
unconditionally on all platforms.

This eliminates the require_wake_loop_threadsafe() boilerplate,
the USE_WAKE_LOOP_THREADSAFE define, and the socket AUTO_LOAD
entries that 12 components needed just to use wake.

wake_loop_threadsafe() and wake_loop_any_context() now just work
on every platform without opt-in.
This commit is contained in:
J. Nick Koston
2026-04-04 10:32:32 -10:00
parent 1a1725f958
commit e372440ed2
29 changed files with 253 additions and 334 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
@@ -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
+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);
+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
}
}
+13 -32
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,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]:
@@ -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
@@ -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
-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
+8 -16
View File
@@ -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<char, BUILD_TIME_STR_SIZE> buffer) {
ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size());
+24 -50
View File
@@ -36,20 +36,13 @@
#endif
#else
#include <sys/select.h>
#ifdef USE_WAKE_LOOP_THREADSAFE
#include <lwip/sockets.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
@@ -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<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)
#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
}
+1 -5
View File
@@ -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
}
+1 -1
View File
@@ -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
+87
View File
@@ -0,0 +1,87 @@
#include "esphome/core/wake.h"
#include "esphome/core/hal.h"
#ifdef USE_ESP8266
#include <coredecls.h>
#elif defined(USE_RP2040)
#include <hardware/sync.h>
#include <pico/time.h>
#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
+99
View File
@@ -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 <coredecls.h>
#elif defined(USE_RP2040)
#include <hardware/sync.h>
#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
@@ -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