mirror of
https://github.com/esphome/esphome.git
synced 2025-11-16 23:05:46 +00:00
[core][esp32_ble] Add wake_loop_threadsafe() helper for background thread wakeups
This commit is contained in:
@@ -51,6 +51,7 @@ from esphome.cpp_helpers import ( # noqa: F401
|
|||||||
past_safe_mode,
|
past_safe_mode,
|
||||||
register_component,
|
register_component,
|
||||||
register_parented,
|
register_parented,
|
||||||
|
require_wake_loop_threadsafe,
|
||||||
)
|
)
|
||||||
from esphome.cpp_types import ( # noqa: F401
|
from esphome.cpp_types import ( # noqa: F401
|
||||||
NAN,
|
NAN,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from typing import Any
|
|||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
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 import add_idf_sdkconfig_option, const, get_esp32_variant
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -482,13 +481,10 @@ async def to_code(config):
|
|||||||
cg.add(var.set_name(name))
|
cg.add(var.set_name(name))
|
||||||
await cg.register_component(var, config)
|
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
|
# 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
|
# select() timeout (0-16ms). The wake socket is shared across all components.
|
||||||
# wake lwip_select() when BLE events arrive from the BLE thread.
|
cg.require_wake_loop_threadsafe()
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Define max connections for use in C++ code (e.g., ble_server.h)
|
# Define max connections for use in C++ code (e.g., ble_server.h)
|
||||||
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||||
|
|||||||
@@ -297,20 +297,14 @@ bool ESP32BLE::ble_setup_() {
|
|||||||
// BLE takes some time to be fully set up, 200ms should be more than enough
|
// BLE takes some time to be fully set up, 200ms should be more than enough
|
||||||
delay(200); // NOLINT
|
delay(200); // NOLINT
|
||||||
|
|
||||||
// Set up notification socket to wake main loop for BLE events
|
// Wake mechanism is set up by core Application class (wake_loop_threadsafe)
|
||||||
// This enables low-latency (~12μs) event processing instead of waiting for select() timeout
|
// BLE tasks will call App.wake_loop_threadsafe() to wake main loop when events arrive
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
this->setup_event_notification_();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ESP32BLE::ble_dismantle_() {
|
bool ESP32BLE::ble_dismantle_() {
|
||||||
// Clean up notification socket first before dismantling BLE stack
|
// No socket cleanup needed - wake socket is managed by core Application
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
this->cleanup_event_notification_();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
esp_err_t err = esp_bluedroid_disable();
|
esp_err_t err = esp_bluedroid_disable();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -409,12 +403,6 @@ void ESP32BLE::loop() {
|
|||||||
break;
|
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();
|
BLEEvent *ble_event = this->ble_events_.pop();
|
||||||
while (ble_event != nullptr) {
|
while (ble_event != nullptr) {
|
||||||
switch (ble_event->type_) {
|
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:
|
GAP_SECURITY_EVENTS:
|
||||||
enqueue_ble_event(event, param);
|
enqueue_ble_event(event, param);
|
||||||
// Wake up main loop to process security event immediately
|
// Wake up main loop to process security event immediately
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
#ifdef USE_WAKE_LOOP_THREADSAFE
|
||||||
global_ble->notify_main_loop_();
|
App.wake_loop_threadsafe();
|
||||||
#endif
|
#endif
|
||||||
return;
|
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) {
|
esp_ble_gatts_cb_param_t *param) {
|
||||||
enqueue_ble_event(event, gatts_if, param);
|
enqueue_ble_event(event, gatts_if, param);
|
||||||
// Wake up main loop to process GATT event immediately
|
// Wake up main loop to process GATT event immediately
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
#ifdef USE_WAKE_LOOP_THREADSAFE
|
||||||
global_ble->notify_main_loop_();
|
App.wake_loop_threadsafe();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#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) {
|
esp_ble_gattc_cb_param_t *param) {
|
||||||
enqueue_ble_event(event, gattc_if, param);
|
enqueue_ble_event(event, gattc_if, param);
|
||||||
// Wake up main loop to process GATT event immediately
|
// Wake up main loop to process GATT event immediately
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
#ifdef USE_WAKE_LOOP_THREADSAFE
|
||||||
global_ble->notify_main_loop_();
|
App.wake_loop_threadsafe();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
#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 ble_addr_to_uint64(const esp_bd_addr_t address) {
|
||||||
uint64_t u = 0;
|
uint64_t u = 0;
|
||||||
u |= uint64_t(address[0] & 0xFF) << 40;
|
u |= uint64_t(address[0] & 0xFF) << 40;
|
||||||
|
|||||||
@@ -166,12 +166,10 @@ class ESP32BLE : public Component {
|
|||||||
void advertising_init_();
|
void advertising_init_();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
// BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop
|
||||||
void setup_event_notification_(); // Create notification socket
|
// from BLE tasks. This enables low-latency (~12μs) event processing instead of
|
||||||
void cleanup_event_notification_(); // Close and unregister socket
|
// waiting for select() timeout (0-16ms). The wake socket is shared with other
|
||||||
inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined)
|
// components that need this functionality.
|
||||||
void drain_event_notifications_(); // Read pending notifications in main loop
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
template<typename... Args> friend void enqueue_ble_event(Args... args);
|
template<typename... Args> 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)
|
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
|
||||||
uint32_t advertising_cycle_time_{}; // 4 bytes
|
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
|
// 2-byte aligned members
|
||||||
uint16_t appearance_{0}; // 2 bytes
|
uint16_t appearance_{0}; // 2 bytes
|
||||||
|
|
||||||
@@ -225,29 +216,6 @@ class ESP32BLE : public Component {
|
|||||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
extern ESP32BLE *global_ble;
|
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<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
||||||
public:
|
public:
|
||||||
bool check(Ts... x) override { return global_ble->is_active(); }
|
bool check(Ts... x) override { return global_ble->is_active(); }
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ void Application::setup() {
|
|||||||
// Clear setup priority overrides to free memory
|
// Clear setup priority overrides to free memory
|
||||||
clear_setup_priority_overrides();
|
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();
|
this->schedule_dump_config();
|
||||||
}
|
}
|
||||||
void Application::loop() {
|
void Application::loop() {
|
||||||
@@ -472,6 +477,11 @@ void Application::enable_pending_loops_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Application::before_loop_tasks_(uint32_t loop_start_time) {
|
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
|
// Process scheduled tasks
|
||||||
this->scheduler.call(loop_start_time);
|
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)
|
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
|
} // namespace esphome
|
||||||
|
|||||||
@@ -21,7 +21,20 @@
|
|||||||
|
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
#include <sys/select.h>
|
#include <sys/select.h>
|
||||||
|
|
||||||
|
#ifdef USE_WAKE_LOOP_THREADSAFE
|
||||||
|
// Inline function drain_wake_notifications_() needs lwip socket functions
|
||||||
|
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
|
||||||
|
#include <lwip/sockets.h>
|
||||||
|
#elif defined(USE_SOCKET_IMPL_BSD_SOCKETS)
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
#include <lwip/sockets.h>
|
||||||
|
#else
|
||||||
|
// True BSD sockets already included via sys/select.h
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
#endif // USE_WAKE_LOOP_THREADSAFE
|
||||||
|
#endif // USE_SOCKET_SELECT_SUPPORT
|
||||||
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
#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
|
/// 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
|
/// This function is thread-safe for reading, but should be called after select() has run
|
||||||
bool is_socket_ready(int fd) const;
|
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
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -454,6 +474,11 @@ class Application {
|
|||||||
/// Perform a delay while also monitoring socket file descriptors for readiness
|
/// Perform a delay while also monitoring socket file descriptors for readiness
|
||||||
void yield_with_select_(uint32_t delay_ms);
|
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 ===
|
// === Member variables ordered by size to minimize padding ===
|
||||||
|
|
||||||
// Pointer-sized members first
|
// Pointer-sized members first
|
||||||
@@ -481,6 +506,9 @@ class Application {
|
|||||||
FixedVector<Component *> looping_components_{};
|
FixedVector<Component *> looping_components_{};
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
|
std::vector<int> 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
|
#endif
|
||||||
|
|
||||||
// std::string members (typically 24-32 bytes each)
|
// std::string members (typically 24-32 bytes each)
|
||||||
@@ -597,4 +625,28 @@ class Application {
|
|||||||
/// Global storage of Application pointer - only one Application can exist.
|
/// Global storage of Application pointer - only one Application can exist.
|
||||||
extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
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
|
} // namespace esphome
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
from esphome.core import CORE, ID, coroutine
|
from esphome.core import CORE, ID, coroutine
|
||||||
from esphome.coroutine import FakeAwaitable
|
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.cpp_types import App
|
||||||
from esphome.types import ConfigFragmentType, ConfigType
|
from esphome.types import ConfigFragmentType, ConfigType
|
||||||
from esphome.util import Registry, RegistryEntry
|
from esphome.util import Registry, RegistryEntry
|
||||||
@@ -124,3 +124,34 @@ async def past_safe_mode():
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
return await FakeAwaitable(_safe_mode_generator())
|
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")({})
|
||||||
|
|||||||
@@ -70,3 +70,43 @@ async def test_register_component__with_setup_priority(monkeypatch):
|
|||||||
assert add_mock.call_count == 4
|
assert add_mock.call_count == 4
|
||||||
app_mock.register_component.assert_called_with(var)
|
app_mock.register_component.assert_called_with(var)
|
||||||
assert core_mock.component_ids == []
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user