From 3ae32423629beeb4d56fd297e71c32907682fdb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Feb 2026 15:42:39 -0600 Subject: [PATCH] [esp32_ble_client] Defer IDLE transition from DISCONNECT_EVT to CLOSE_EVT ESP_GATTC_DISCONNECT_EVT only indicates that the link-layer disconnection has started, not that the controller has fully freed resources (L2CAP channels, ATT resources, HCI connection handle). Transitioning to IDLE immediately allowed reconnection attempts before cleanup was complete, causing the controller to reject new connections with status=133 (ESP_GATT_CONN_FAIL_ESTABLISH) or crash with ASSERT_PARAM in lld_evt.c. This applies the same fix that was made for bluetooth_proxy in #10303 to the base BLEClientBase class, which is used by the ble_client component. Changes: - DISCONNECT_EVT now transitions to DISCONNECTING instead of IDLE - CLOSE_EVT (already existing) handles the final IDLE transition - connect() now blocks while in DISCONNECTING state - OPEN_EVT ignores events received during DISCONNECTING state --- .../esp32_ble_client/ble_client_base.cpp | 18 ++++++++++++------ 1 file changed, 12 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 3f0eeeab4a..9370343c9c 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -101,9 +101,9 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { #endif void BLEClientBase::connect() { - // Prevent duplicate connection attempts + // Prevent duplicate connection attempts or connecting while still disconnecting if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED || - this->state() == espbt::ClientState::ESTABLISHED) { + this->state() == espbt::ClientState::ESTABLISHED || this->state() == espbt::ClientState::DISCONNECTING) { ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, espbt::client_state_to_string(this->state())); return; @@ -295,9 +295,10 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an // error, if the error occurred at the BTA/GATT layer. This can result in the event // arriving after we've already transitioned to IDLE state. - if (this->state() == espbt::ClientState::IDLE) { - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, - this->address_str_, param->open.status); + // It may also arrive during DISCONNECTING if the controller is still cleaning up. + if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) { + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d), ignoring", this->connection_index_, + this->address_str_, espbt::client_state_to_string(this->state()), param->open.status); break; } @@ -362,8 +363,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, param->disconnect.reason); } + // Don't transition to IDLE yet - wait for CLOSE_EVT to ensure the controller has + // fully freed resources (L2CAP channels, ATT resources, HCI connection handle). + // Transitioning to IDLE here would allow reconnection before cleanup is complete, + // causing the controller to reject the new connection (status=133) or crash + // with ASSERT_PARAM in lld_evt.c. this->release_services(); - this->set_state(espbt::ClientState::IDLE); + this->set_state(espbt::ClientState::DISCONNECTING); break; }