fix ble latency

This commit is contained in:
J. Nick Koston
2025-11-01 14:52:45 -05:00
parent 55af818629
commit 66eb10cc55
3 changed files with 136 additions and 0 deletions

View File

@@ -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,12 @@ 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.
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)

View File

@@ -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";
@@ -273,10 +277,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);
@@ -374,6 +389,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_) {
@@ -531,6 +552,12 @@ template<typename... Args> void enqueue_ble_event(Args... args) {
// Push the event to the queue
global_ble->ble_events_.push(event);
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
// Wake up main loop to process event immediately
// This is thread-safe - notify_main_loop_() uses lwip_sendto which is thread-safe
#ifdef USE_SOCKET_SELECT_SUPPORT
global_ble->notify_main_loop_();
#endif
}
// Explicit template instantiations for the friend function
@@ -630,6 +657,94 @@ void ESP32BLE::dump_config() {
}
}
#ifdef USE_SOCKET_SELECT_SUPPORT
void ESP32BLE::setup_event_notification_() {
// Guard against multiple calls (reentrant safety for ble.enable automation)
if (this->notify_fd_ >= 0) {
return; // Already set up
}
// 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 port for sendto()
socklen_t len = sizeof(this->notify_addr_);
if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) &this->notify_addr_, &len) < 0) {
ESP_LOGW(TAG, "Event socket address 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_() {
// Guard against multiple calls (reentrant safety for ble.disable automation)
if (this->notify_fd_ < 0) {
return; // Already cleaned up
}
App.unregister_socket_fd(this->notify_fd_);
lwip_close(this->notify_fd_);
this->notify_fd_ = -1;
ESP_LOGD(TAG, "Event socket closed");
}
void ESP32BLE::notify_main_loop_() {
// Called from BLE thread context when events are queued
// Wakes up lwip_select() in main loop by writing to loopback socket
if (this->notify_fd_ >= 0) {
const char dummy = 1;
// Non-blocking sendto - if it fails (unlikely), select() will wake on timeout anyway
// This is safe to call from BLE thread - sendto() is thread-safe in lwip
lwip_sendto(this->notify_fd_, &dummy, 1, 0, (struct sockaddr *) &this->notify_addr_, sizeof(this->notify_addr_));
}
}
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[64];
// Drain all pending notifications with non-blocking reads
// Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK
while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) {
// Just draining, no action needed
}
}
}
#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;

View File

@@ -162,6 +162,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
void notify_main_loop_(); // Wake up select() from BLE thread
void drain_event_notifications_(); // Read pending notifications in main loop
#endif
private:
template<typename... Args> friend void enqueue_ble_event(Args... args);
@@ -196,6 +203,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 UDP loopback to wake lwip_select() with ~12μs latency vs 0-16ms timeout
struct sockaddr_in notify_addr_ {}; // 16 bytes (sockaddr_in structure)
int notify_fd_{-1}; // 4 bytes (file descriptor)
#endif
// 2-byte aligned members
uint16_t appearance_{0}; // 2 bytes