From 4dd3c906635ae3964f6b12eab46bcbc7d44990f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 20:55:17 -0600 Subject: [PATCH 01/11] [esp32_ble] Wake main loop for GAP security events (#11677) --- esphome/components/esp32_ble/ble.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index eef0db5347..d6f7e1ce43 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -581,9 +581,17 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa GAP_ADV_COMPLETE_EVENTS: // Connection events - used by ble_client case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + enqueue_ble_event(event, param); + return; + // Security events - used by ble_client and bluetooth_proxy + // These are rare but interactive (pairing/bonding), so notify immediately GAP_SECURITY_EVENTS: enqueue_ble_event(event, param); + // Wake up main loop to process security event immediately +#ifdef USE_SOCKET_SELECT_SUPPORT + global_ble->notify_main_loop_(); +#endif return; // Ignore these GAP events as they are not relevant for our use case From 12077d016d19acf99b1ab59defddb95689bacc2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:48:17 -0600 Subject: [PATCH 02/11] [core][esp32_ble] Add wake_loop_threadsafe() helper for background thread wakeups --- esphome/codegen.py | 1 + esphome/components/esp32_ble/__init__.py | 10 +- esphome/components/esp32_ble/ble.cpp | 113 ++--------------------- esphome/components/esp32_ble/ble.h | 40 +------- esphome/core/application.cpp | 79 ++++++++++++++++ esphome/core/application.h | 52 +++++++++++ esphome/cpp_helpers.py | 33 ++++++- tests/unit_tests/test_cpp_helpers.py | 40 ++++++++ 8 files changed, 220 insertions(+), 148 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 6d55c6023d..f0deb6e8d3 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -51,6 +51,7 @@ from esphome.cpp_helpers import ( # noqa: F401 past_safe_mode, register_component, register_parented, + require_wake_loop_threadsafe, ) from esphome.cpp_types import ( # noqa: F401 NAN, diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 1ae8df6f5e..d3db1db70c 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,7 +7,6 @@ from typing import Any from esphome import automation import esphome.codegen as cg -from esphome.components import socket from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv from esphome.const import ( @@ -482,13 +481,10 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - # BLE uses 1 UDP socket for event notification to wake up main loop from select() + # BLE uses the core 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 socket is created in ble_setup_() and used to - # wake lwip_select() when BLE events arrive from the BLE thread. - # Note: Called during config generation, socket is created at runtime. In practice, - # always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT. - socket.consume_sockets(1, "esp32_ble")(config) + # select() timeout (0-16ms). The wake socket is shared across all components. + cg.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) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d6f7e1ce43..ecdd63f5b1 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -297,20 +297,14 @@ bool ESP32BLE::ble_setup_() { // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT - // Set up notification socket to wake main loop for BLE events - // This enables low-latency (~12μs) event processing instead of waiting for select() timeout -#ifdef USE_SOCKET_SELECT_SUPPORT - this->setup_event_notification_(); -#endif + // Wake mechanism is set up by core Application class (wake_loop_threadsafe) + // BLE tasks will call App.wake_loop_threadsafe() to wake main loop when events arrive return true; } bool ESP32BLE::ble_dismantle_() { - // Clean up notification socket first before dismantling BLE stack -#ifdef USE_SOCKET_SELECT_SUPPORT - this->cleanup_event_notification_(); -#endif + // No socket cleanup needed - wake socket is managed by core Application esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { @@ -409,12 +403,6 @@ void ESP32BLE::loop() { break; } -#ifdef USE_SOCKET_SELECT_SUPPORT - // Drain any notification socket events first - // This clears the socket so it doesn't stay "ready" in subsequent select() calls - this->drain_event_notifications_(); -#endif - BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { @@ -589,8 +577,8 @@ 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 -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#ifdef USE_WAKE_LOOP_THREADSAFE + App.wake_loop_threadsafe(); #endif return; @@ -612,8 +600,8 @@ 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 -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#ifdef USE_WAKE_LOOP_THREADSAFE + App.wake_loop_threadsafe(); #endif } #endif @@ -623,8 +611,8 @@ 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 -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#ifdef USE_WAKE_LOOP_THREADSAFE + App.wake_loop_threadsafe(); #endif } #endif @@ -665,89 +653,6 @@ void ESP32BLE::dump_config() { } } -#ifdef USE_SOCKET_SELECT_SUPPORT -void ESP32BLE::setup_event_notification_() { - // Create UDP socket for event notifications - this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (this->notify_fd_ < 0) { - ESP_LOGW(TAG, "Event socket create failed: %d", errno); - return; - } - - // 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_port = 0; // Auto-assign port - - if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - ESP_LOGW(TAG, "Event socket bind failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Get the assigned address and connect to it - // Connecting a UDP socket allows using send() instead of sendto() for better performance - struct sockaddr_in notify_addr; - socklen_t len = sizeof(notify_addr); - if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) { - ESP_LOGW(TAG, "Event socket address failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Connect to self (loopback) - allows using send() instead of sendto() - // After connect(), no need to store notify_addr - the socket remembers it - if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) { - ESP_LOGW(TAG, "Event socket connect failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Set non-blocking mode - int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0); - lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK); - - // Register with application's select() loop - if (!App.register_socket_fd(this->notify_fd_)) { - ESP_LOGW(TAG, "Event socket register failed"); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - ESP_LOGD(TAG, "Event socket ready"); -} - -void ESP32BLE::cleanup_event_notification_() { - if (this->notify_fd_ >= 0) { - App.unregister_socket_fd(this->notify_fd_); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - ESP_LOGD(TAG, "Event socket closed"); - } -} - -void ESP32BLE::drain_event_notifications_() { - // Called from main loop to drain any pending notifications - // Must check is_socket_ready() to avoid blocking on empty socket - if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { - char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; - // Drain all pending notifications with non-blocking reads - // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK - // We control both ends of this loopback socket (always write 1 byte per event), - // so no error checking needed - any errors indicate catastrophic system failure - while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { - // Just draining, no action needed - actual BLE events are already queued - } - } -} - -#endif // USE_SOCKET_SELECT_SUPPORT - uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { uint64_t u = 0; u |= uint64_t(address[0] & 0xFF) << 40; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 7c3195db6d..3be6a7048d 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -166,12 +166,10 @@ class ESP32BLE : public Component { void advertising_init_(); #endif -#ifdef USE_SOCKET_SELECT_SUPPORT - void setup_event_notification_(); // Create notification socket - void cleanup_event_notification_(); // Close and unregister socket - inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined) - void drain_event_notifications_(); // Read pending notifications in main loop -#endif + // BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop + // from BLE tasks. This enables low-latency (~12μs) event processing instead of + // waiting for select() timeout (0-16ms). The wake socket is shared with other + // components that need this functionality. private: template friend void enqueue_ble_event(Args... args); @@ -207,13 +205,6 @@ class ESP32BLE : public Component { esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes -#ifdef USE_SOCKET_SELECT_SUPPORT - // Event notification socket for waking up main loop from BLE thread - // Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout - // Socket is connected during setup, allowing use of send() instead of sendto() for efficiency - int notify_fd_{-1}; // 4 bytes (file descriptor) -#endif - // 2-byte aligned members uint16_t appearance_{0}; // 2 bytes @@ -225,29 +216,6 @@ class ESP32BLE : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32BLE *global_ble; -#ifdef USE_SOCKET_SELECT_SUPPORT -// Inline implementations for hot-path functions -// These are called from BLE thread (notify) and main loop (drain) on every event - -// Small buffer for draining notification bytes (1 byte sent per BLE event) -// Size allows draining multiple notifications per recvfrom() without wasting stack -static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16; - -inline void ESP32BLE::notify_main_loop_() { - // Called from BLE thread context when events are queued - // Wakes up lwip_select() in main loop by writing to connected loopback socket - if (this->notify_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, and the - // BLE event is already queued. Notification is best-effort to reduce latency. - // This is safe to call from BLE thread - send() is thread-safe in lwip - // Socket is already connected to loopback address, so send() is faster than sendto() - lwip_send(this->notify_fd_, &dummy, 1, 0); - } -} -#endif // USE_SOCKET_SELECT_SUPPORT - template class BLEEnabledCondition : public Condition { public: bool check(Ts... x) override { return global_ble->is_active(); } diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 61cfcc7585..75814ae253 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -122,6 +122,11 @@ void Application::setup() { // Clear setup priority overrides to free memory clear_setup_priority_overrides(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Set up wake socket for waking main loop from tasks + this->setup_wake_loop_threadsafe_(); +#endif + this->schedule_dump_config(); } void Application::loop() { @@ -472,6 +477,11 @@ void Application::enable_pending_loops_() { } void Application::before_loop_tasks_(uint32_t loop_start_time) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Drain wake notifications first to clear socket for next wake + this->drain_wake_notifications_(); +#endif + // Process scheduled tasks this->scheduler.call(loop_start_time); @@ -625,4 +635,73 @@ void Application::yield_with_select_(uint32_t delay_ms) { Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +void Application::setup_wake_loop_threadsafe_() { + // Create UDP socket for wake notifications + this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (this->wake_socket_fd_ < 0) { + ESP_LOGW(TAG, "Wake socket create failed: %d", errno); + return; + } + + // 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_port = 0; // Auto-assign port + + if (lwip_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_); + this->wake_socket_fd_ = -1; + return; + } + + // Get the assigned address and connect to it + // 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) { + ESP_LOGW(TAG, "Wake socket address failed: %d", errno); + lwip_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) { + ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); + lwip_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); + + // 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_); + 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 // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 29a734f000..fdc7f02796 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -21,7 +21,20 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #include + +#ifdef USE_WAKE_LOOP_THREADSAFE +// Inline function drain_wake_notifications_() needs lwip socket functions +#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS +#include +#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) +#ifdef USE_ESP32 +#include +#else +// True BSD sockets already included via sys/select.h #endif +#endif +#endif // USE_WAKE_LOOP_THREADSAFE +#endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" @@ -429,6 +442,13 @@ class Application { /// Check if there's data available on a socket without blocking /// This function is thread-safe for reading, but should be called after select() has run bool is_socket_ready(int fd) const; + +#ifdef USE_WAKE_LOOP_THREADSAFE + /// Wake the main event loop from a FreeRTOS task + /// Thread-safe, can be called from task context to immediately wake select() + /// IMPORTANT: NOT safe to call from ISR context (socket operations not ISR-safe) + void wake_loop_threadsafe(); +#endif #endif protected: @@ -454,6 +474,11 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + 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 + // === Member variables ordered by size to minimize padding === // Pointer-sized members first @@ -481,6 +506,9 @@ class Application { FixedVector looping_components_{}; #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors +#ifdef USE_WAKE_LOOP_THREADSAFE + int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks +#endif #endif // std::string members (typically 24-32 bytes each) @@ -597,4 +625,28 @@ 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) +// Inline implementations for hot-path functions +// drain_wake_notifications_() is called on every loop iteration + +// Small buffer for draining wake notification bytes (1 byte sent per wake) +// Size allows draining multiple notifications per recvfrom() without wasting stack +static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; + +inline void Application::drain_wake_notifications_() { + // Called from main loop to drain any pending wake notifications + // Must check is_socket_ready() to avoid blocking on empty socket + if (this->wake_socket_fd_ >= 0 && this->is_socket_ready(this->wake_socket_fd_)) { + char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; + // Drain all pending notifications with non-blocking reads + // 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) { + // Just draining, no action needed - wake has already occurred + } + } +} +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 2698b9b3d5..8b1fd1db27 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, get_variable +from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -124,3 +124,34 @@ async def past_safe_mode(): yield return await FakeAwaitable(_safe_mode_generator()) + + +# Wake loop threadsafe support tracking +# Components that need to wake the main event loop from FreeRTOS tasks can call require_wake_loop_threadsafe() +KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" + + +def require_wake_loop_threadsafe() -> None: + """Mark that wake_loop_threadsafe support is required by a component. + + Call this from components that need to wake the main event loop from FreeRTOS tasks. + This enables the shared UDP loopback socket mechanism (~208 bytes RAM). + The socket is shared across all components that use this feature. + + IMPORTANT: This is for FreeRTOS task context only, NOT ISR context. + Socket operations are not safe to call from ISR handlers. + + Example: + import esphome.codegen as cg + + async def to_code(config): + cg.require_wake_loop_threadsafe() + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): + from esphome.components import socket + + CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + add_define("USE_WAKE_LOOP_THREADSAFE") + # Consume 1 socket for the shared wake notification socket + socket.consume_sockets(1, "core.wake_loop_threadsafe")({}) diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 2618803fec..89a474f44d 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -70,3 +70,43 @@ async def test_register_component__with_setup_priority(monkeypatch): assert add_mock.call_count == 4 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == [] + + +def test_require_wake_loop_threadsafe__first_call() -> None: + """Test that first call sets up define and consumes socket.""" + ch.require_wake_loop_threadsafe() + + # Verify CORE.data was updated + assert ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True + + # Verify the define was added + assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in ch.CORE.defines) + + +def test_require_wake_loop_threadsafe__idempotent() -> None: + """Test that subsequent calls are idempotent.""" + # Set up initial state as if already called + ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + + # Call again - should not raise or fail + ch.require_wake_loop_threadsafe() + + # Verify state is still True + assert ch.CORE.data[ch.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 ch.CORE.defines) + + +def test_require_wake_loop_threadsafe__multiple_calls() -> None: + """Test that multiple calls only set up once.""" + # Call three times + ch.require_wake_loop_threadsafe() + ch.require_wake_loop_threadsafe() + ch.require_wake_loop_threadsafe() + + # Verify CORE.data was set + assert ch.CORE.data[ch.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 ch.CORE.defines) From f11103c895448778a743b35162b972f74df1cc10 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:50:56 -0600 Subject: [PATCH 03/11] [core][esp32_ble] Add wake_loop_threadsafe() helper for background thread wakeups --- esphome/components/esp32_ble/ble.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index ecdd63f5b1..4221b6331d 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -304,8 +304,6 @@ bool ESP32BLE::ble_setup_() { } bool ESP32BLE::ble_dismantle_() { - // No socket cleanup needed - wake socket is managed by core Application - esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); @@ -577,7 +575,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 -#ifdef USE_WAKE_LOOP_THREADSAFE +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif return; @@ -600,7 +598,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 -#ifdef USE_WAKE_LOOP_THREADSAFE +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif } @@ -611,7 +609,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 -#ifdef USE_WAKE_LOOP_THREADSAFE +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) App.wake_loop_threadsafe(); #endif } From 2ac95abea7c31135901c2a3fe4c30760a6b9e4c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:51:39 -0600 Subject: [PATCH 04/11] [core][esp32_ble] Add wake_loop_threadsafe() helper for background thread wakeups --- esphome/core/application.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index fdc7f02796..6909eec64a 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -26,12 +26,8 @@ // Inline function drain_wake_notifications_() needs lwip socket functions #ifdef USE_SOCKET_IMPL_LWIP_SOCKETS #include -#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) -#ifdef USE_ESP32 +#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) && defined(USE_ESP32) #include -#else -// True BSD sockets already included via sys/select.h -#endif #endif #endif // USE_WAKE_LOOP_THREADSAFE #endif // USE_SOCKET_SELECT_SUPPORT From acd26600ddcd8682970927d2b55254fe69cd33cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:57:57 -0600 Subject: [PATCH 05/11] move to socket --- esphome/codegen.py | 1 - esphome/components/esp32_ble/__init__.py | 6 ++-- esphome/components/socket/__init__.py | 28 ++++++++++++++++ esphome/cpp_helpers.py | 33 +------------------ tests/components/socket/conftest.py | 12 +++++++ tests/components/socket/test_init.py | 42 ++++++++++++++++++++++++ tests/unit_tests/test_cpp_helpers.py | 40 ---------------------- 7 files changed, 87 insertions(+), 75 deletions(-) create mode 100644 tests/components/socket/conftest.py create mode 100644 tests/components/socket/test_init.py diff --git a/esphome/codegen.py b/esphome/codegen.py index f0deb6e8d3..6d55c6023d 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -51,7 +51,6 @@ from esphome.cpp_helpers import ( # noqa: F401 past_safe_mode, register_component, register_parented, - require_wake_loop_threadsafe, ) from esphome.cpp_types import ( # noqa: F401 NAN, diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index d3db1db70c..ced7e3fec9 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -7,6 +7,7 @@ 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 import esphome.config_validation as cv from esphome.const import ( @@ -21,6 +22,7 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv DEPENDENCIES = ["esp32"] +AUTO_LOAD = ["socket"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] DOMAIN = "esp32_ble" @@ -481,10 +483,10 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - # BLE uses the core wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks + # 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. - cg.require_wake_loop_threadsafe() + 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) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index e6a4cfc07f..4c2ea7f088 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -3,6 +3,7 @@ from collections.abc import Callable, MutableMapping import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE +from esphome.cpp_generator import add_define CODEOWNERS = ["@esphome/core"] @@ -15,6 +16,9 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" # Components register their socket needs and platforms read this to configure appropriately KEY_SOCKET_CONSUMERS = "socket_consumers" +# Wake loop threadsafe support tracking +KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" + def consume_sockets( value: int, consumer: str @@ -37,6 +41,30 @@ def consume_sockets( return _consume_sockets +def require_wake_loop_threadsafe() -> None: + """Mark that wake_loop_threadsafe support is required by a component. + + 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. + + IMPORTANT: This is for background thread context only, NOT ISR context. + Socket operations are not safe to call from ISR handlers. + + Example: + from esphome.components import socket + + async def to_code(config): + socket.require_wake_loop_threadsafe() + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): + CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + add_define("USE_WAKE_LOOP_THREADSAFE") + # Consume 1 socket for the shared wake notification socket + consume_sockets(1, "socket.wake_loop_threadsafe")({}) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 8b1fd1db27..2698b9b3d5 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import LogStringLiteral, add, add_define, get_variable +from esphome.cpp_generator import LogStringLiteral, add, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -124,34 +124,3 @@ async def past_safe_mode(): yield return await FakeAwaitable(_safe_mode_generator()) - - -# Wake loop threadsafe support tracking -# Components that need to wake the main event loop from FreeRTOS tasks can call require_wake_loop_threadsafe() -KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" - - -def require_wake_loop_threadsafe() -> None: - """Mark that wake_loop_threadsafe support is required by a component. - - Call this from components that need to wake the main event loop from FreeRTOS tasks. - This enables the shared UDP loopback socket mechanism (~208 bytes RAM). - The socket is shared across all components that use this feature. - - IMPORTANT: This is for FreeRTOS task context only, NOT ISR context. - Socket operations are not safe to call from ISR handlers. - - Example: - import esphome.codegen as cg - - async def to_code(config): - cg.require_wake_loop_threadsafe() - """ - # Only set up once (idempotent - multiple components can call this) - if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): - from esphome.components import socket - - CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - add_define("USE_WAKE_LOOP_THREADSAFE") - # Consume 1 socket for the shared wake notification socket - socket.consume_sockets(1, "core.wake_loop_threadsafe")({}) diff --git a/tests/components/socket/conftest.py b/tests/components/socket/conftest.py new file mode 100644 index 0000000000..5d93cac232 --- /dev/null +++ b/tests/components/socket/conftest.py @@ -0,0 +1,12 @@ +"""Configuration file for socket component tests.""" + +import pytest + +from esphome.core import CORE + + +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE after each test.""" + yield + CORE.reset() diff --git a/tests/components/socket/test_init.py b/tests/components/socket/test_init.py new file mode 100644 index 0000000000..45e5ea2211 --- /dev/null +++ b/tests/components/socket/test_init.py @@ -0,0 +1,42 @@ +from esphome.components import socket +from esphome.core import CORE + + +def test_require_wake_loop_threadsafe__first_call() -> None: + """Test that first call sets up define and consumes socket.""" + 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 + + # 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.""" + # Call three times + 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) diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 89a474f44d..2618803fec 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -70,43 +70,3 @@ async def test_register_component__with_setup_priority(monkeypatch): assert add_mock.call_count == 4 app_mock.register_component.assert_called_with(var) assert core_mock.component_ids == [] - - -def test_require_wake_loop_threadsafe__first_call() -> None: - """Test that first call sets up define and consumes socket.""" - ch.require_wake_loop_threadsafe() - - # Verify CORE.data was updated - assert ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True - - # Verify the define was added - assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in ch.CORE.defines) - - -def test_require_wake_loop_threadsafe__idempotent() -> None: - """Test that subsequent calls are idempotent.""" - # Set up initial state as if already called - ch.CORE.data[ch.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - - # Call again - should not raise or fail - ch.require_wake_loop_threadsafe() - - # Verify state is still True - assert ch.CORE.data[ch.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 ch.CORE.defines) - - -def test_require_wake_loop_threadsafe__multiple_calls() -> None: - """Test that multiple calls only set up once.""" - # Call three times - ch.require_wake_loop_threadsafe() - ch.require_wake_loop_threadsafe() - ch.require_wake_loop_threadsafe() - - # Verify CORE.data was set - assert ch.CORE.data[ch.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 ch.CORE.defines) From 6a48c0f5cf1931fbb817d1ee6dce89182249e413 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 21:59:22 -0600 Subject: [PATCH 06/11] move to socket --- esphome/components/socket/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 4c2ea7f088..49e074a6ee 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -3,7 +3,6 @@ from collections.abc import Callable, MutableMapping import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE -from esphome.cpp_generator import add_define CODEOWNERS = ["@esphome/core"] @@ -60,7 +59,7 @@ def require_wake_loop_threadsafe() -> None: # Only set up once (idempotent - multiple components can call this) if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True - add_define("USE_WAKE_LOOP_THREADSAFE") + cg.add_define("USE_WAKE_LOOP_THREADSAFE") # Consume 1 socket for the shared wake notification socket consume_sockets(1, "socket.wake_loop_threadsafe")({}) From 4640198827163c857d53c7bc4db863f6a2a0f746 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:01:00 -0600 Subject: [PATCH 07/11] move to socket --- esphome/core/application.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index 6909eec64a..dae44d8902 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -21,15 +21,9 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #include - #ifdef USE_WAKE_LOOP_THREADSAFE -// Inline function drain_wake_notifications_() needs lwip socket functions -#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS -#include -#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS) && defined(USE_ESP32) #include #endif -#endif // USE_WAKE_LOOP_THREADSAFE #endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_BINARY_SENSOR From edd01d5c9cf6dc36a9ebaaa86f0e472e27022e6d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:04:14 -0600 Subject: [PATCH 08/11] move to socket --- tests/components/socket/test_init.py | 42 ---------------------------- 1 file changed, 42 deletions(-) delete mode 100644 tests/components/socket/test_init.py diff --git a/tests/components/socket/test_init.py b/tests/components/socket/test_init.py deleted file mode 100644 index 45e5ea2211..0000000000 --- a/tests/components/socket/test_init.py +++ /dev/null @@ -1,42 +0,0 @@ -from esphome.components import socket -from esphome.core import CORE - - -def test_require_wake_loop_threadsafe__first_call() -> None: - """Test that first call sets up define and consumes socket.""" - 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 - - # 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.""" - # Call three times - 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) From 8b7ef6cae835a83aeecdb619a04ca4fac5fff418 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:04:20 -0600 Subject: [PATCH 09/11] move to socket --- .../socket/test_wake_loop_threadsafe.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/components/socket/test_wake_loop_threadsafe.py diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py new file mode 100644 index 0000000000..45e5ea2211 --- /dev/null +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -0,0 +1,42 @@ +from esphome.components import socket +from esphome.core import CORE + + +def test_require_wake_loop_threadsafe__first_call() -> None: + """Test that first call sets up define and consumes socket.""" + 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 + + # 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.""" + # Call three times + 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) From ee2b10a992ec260ecc9b85d3a9e2bf4161ab5046 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:05:15 -0600 Subject: [PATCH 10/11] move to socket --- esphome/components/esp32_ble/ble.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 4221b6331d..fc26a7fc21 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -297,9 +297,6 @@ bool ESP32BLE::ble_setup_() { // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT - // Wake mechanism is set up by core Application class (wake_loop_threadsafe) - // BLE tasks will call App.wake_loop_threadsafe() to wake main loop when events arrive - return true; } From 8e0721318caae6044b3a9bab422dfd74a36a009a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 2 Nov 2025 22:06:15 -0600 Subject: [PATCH 11/11] analysis --- esphome/core/defines.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 868df6e254..65069f5b2f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -195,6 +195,7 @@ #define USE_PSRAM #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER #define USE_SPI #define USE_VOICE_ASSISTANT