mirror of
https://github.com/esphome/esphome.git
synced 2026-09-23 21:14:03 +00:00
[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:
@@ -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)
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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,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]):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user