mirror of
https://github.com/esphome/esphome.git
synced 2025-11-14 22:05:54 +00:00
[esp32_ble] Reduce GATT event latency from 8ms to 12μs with notification socket (#11663)
This commit is contained in:
@@ -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 (
|
||||
@@ -481,6 +482,14 @@ 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()
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -27,6 +27,10 @@ extern "C" {
|
||||
#include <esp32-hal-bt.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
static const char *const TAG = "esp32_ble";
|
||||
@@ -293,10 +297,21 @@ 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
|
||||
|
||||
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
|
||||
|
||||
esp_err_t err = esp_bluedroid_disable();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||
@@ -394,6 +409,12 @@ 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_) {
|
||||
@@ -582,6 +603,10 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
||||
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||
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_();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -589,6 +614,10 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
|
||||
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
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_();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -628,6 +657,89 @@ 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;
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
#include <lwip/sockets.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
// Maximum size of the BLE event queue
|
||||
@@ -162,6 +166,13 @@ 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
|
||||
|
||||
private:
|
||||
template<typename... Args> friend void enqueue_ble_event(Args... args);
|
||||
|
||||
@@ -196,6 +207,13 @@ 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
|
||||
|
||||
@@ -207,6 +225,29 @@ 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<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(Ts... x) override { return global_ble->is_active(); }
|
||||
|
||||
Reference in New Issue
Block a user