From 989058e6a94b97dd76472ac40b2d6f7cccd4f9b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 19:12:06 -1000 Subject: [PATCH 1/4] [esp32_ble_client] Use FAST connection parameters for all v3 connections (#10052) --- .../esp32_ble_client/ble_client_base.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 94f2a6073c..031cb41e6d 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -16,8 +16,8 @@ static const char *const TAG = "esp32_ble_client"; // Intermediate connection parameters for standard operation // ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies, // causing disconnections. These medium parameters balance responsiveness with bandwidth usage. -static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x08; // 8 * 1.25ms = 10ms -static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x0A; // 10 * 1.25ms = 12.5ms +static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms +static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms // The timeout value was increased from 6s to 8s to address stability issues observed // in certain BLE devices when operating through WiFi-based BLE proxies. The longer // timeout reduces the likelihood of disconnections during periods of high latency. @@ -157,12 +157,13 @@ void BLEClientBase::connect() { this->set_state(espbt::ClientState::CONNECTING); // Always set connection parameters to ensure stable operation - // Use FAST for V3_WITHOUT_CACHE (devices that need lowest latency) - // Use MEDIUM for all other connections (balanced performance) + // Use FAST for all V3 connections (better latency and reliability) + // Use MEDIUM for V1/legacy connections (balanced performance) uint16_t min_interval, max_interval, timeout; const char *param_type; - if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE || + this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { min_interval = FAST_MIN_CONN_INTERVAL; max_interval = FAST_MAX_CONN_INTERVAL; timeout = FAST_CONN_TIMEOUT; @@ -411,9 +412,10 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str()); - // For non-cached connections, restore to medium connection parameters after service discovery + // For V3 connections, restore to medium connection parameters after service discovery // This balances performance with bandwidth usage after the critical discovery phase - if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE || + this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { esp_ble_conn_update_params_t conn_params = {{0}}; memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL; From 6be22a5ea9c9b03ee125e4d77aceb90a77154a1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 19:15:28 -1000 Subject: [PATCH 2/4] [esp32_ble_client] Connect immediately on READY_TO_CONNECT to reduce latency (#10051) --- esphome/components/esp32_ble_client/ble_client_base.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 031cb41e6d..2c13995f76 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -45,8 +45,10 @@ void BLEClientBase::set_state(espbt::ClientState st) { ESPBTClient::set_state(st); if (st == espbt::ClientState::READY_TO_CONNECT) { - // Enable loop when we need to connect + // Enable loop for state processing this->enable_loop(); + // Connect immediately instead of waiting for next loop + this->connect(); } } @@ -63,11 +65,6 @@ void BLEClientBase::loop() { } this->set_state(espbt::ClientState::IDLE); } - // READY_TO_CONNECT means we have discovered the device - // and the scanner has been stopped by the tracker. - else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { - this->connect(); - } // If its idle, we can disable the loop as set_state // will enable it again when we need to connect. else if (this->state_ == espbt::ClientState::IDLE) { From 36c44303172b60acdd2339b224ebfda6957f4b0b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 19:41:41 -1000 Subject: [PATCH 3/4] [esp32_ble] Fix BLE connection slot waste by aligning ESP-IDF timeout with client timeout (#10013) --- esphome/components/esp32_ble/__init__.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 93bb643596..1c7c075cfa 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -6,7 +6,7 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME -from esphome.core import CORE +from esphome.core import CORE, TimePeriod from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX import esphome.final_validate as fv @@ -117,6 +117,7 @@ CONF_BLE_ID = "ble_id" CONF_IO_CAPABILITY = "io_capability" CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" CONF_DISABLE_BT_LOGS = "disable_bt_logs" +CONF_CONNECTION_TIMEOUT = "connection_timeout" NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] @@ -167,6 +168,11 @@ CONFIG_SCHEMA = cv.Schema( cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), + cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All( + cv.only_with_esp_idf, + cv.positive_time_period_seconds, + cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)), + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -255,6 +261,17 @@ async def to_code(config): if logger not in _required_loggers: add_idf_sdkconfig_option(f"{logger.value}_NONE", True) + # Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector + # Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to + # cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds, + # the connection slot remains occupied for the remaining time, preventing new connection + # attempts and wasting valuable connection slots. + if CONF_CONNECTION_TIMEOUT in config: + timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds) + add_idf_sdkconfig_option( + "CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds + ) + cg.add_define("USE_ESP32_BLE") From 8d4f1802fb8de02f61ab03450f25caed613ad2c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 20:57:02 -1000 Subject: [PATCH 4/4] [esp32_ble_tracker] Optimize connection by promoting client immediately after scan stop trigger --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 254eddd1d9..ef4e6802cc 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -238,19 +238,19 @@ void ESP32BLETracker::loop() { if (this->scanner_state_ == ScannerState::RUNNING) { ESP_LOGD(TAG, "Stopping scan to make connection"); this->stop_scan_(); - } else if (this->scanner_state_ == ScannerState::IDLE) { - ESP_LOGD(TAG, "Promoting client to connect"); - // We only want to promote one client at a time. - // once the scanner is fully stopped. -#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE - ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection."); - if (!this->coex_prefer_ble_) { - this->coex_prefer_ble_ = true; - esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth - } -#endif - client->set_state(ClientState::READY_TO_CONNECT); + // Don't wait for scan stop complete - promote immediately + // The BLE stack processes commands in order through its queue } + + ESP_LOGD(TAG, "Promoting client to connect"); +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection."); + if (!this->coex_prefer_ble_) { + this->coex_prefer_ble_ = true; + esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth + } +#endif + client->set_state(ClientState::READY_TO_CONNECT); break; } }