[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
This commit is contained in:
J. Nick Koston
2026-02-22 15:42:39 -06:00
parent 1753074eef
commit 3ae3242362

View File

@@ -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;
}