From 5c86bad64bb71e3b6070e0fb4c40b3c3f9c95d14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 11:53:44 -0600 Subject: [PATCH] [esp32_ble_client] Add 10s safety timeout for DISCONNECTING state If CLOSE_EVT never arrives after DISCONNECT_EVT, the client gets stuck in DISCONNECTING forever, blocking reconnection and scanner restart. Add a 10s timeout watchdog in loop() that forces IDLE as a recovery path. Introduce set_disconnecting_() helper to ensure the timeout timestamp is always set when entering DISCONNECTING state. Co-Authored-By: Claude Opus 4.6 --- .../components/esp32_ble_client/ble_client_base.cpp | 10 ++++++++-- esphome/components/esp32_ble_client/ble_client_base.h | 10 +++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index b317aef8e3..c672e5a971 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -27,6 +27,7 @@ static constexpr uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s static constexpr uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum) static constexpr uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms static constexpr uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s +static constexpr uint32_t DISCONNECTING_TIMEOUT = 10000; // 10s static const esp_bt_uuid_t NOTIFY_DESC_UUID = { .len = ESP_UUID_LEN_16, .uuid = @@ -62,6 +63,11 @@ void BLEClientBase::loop() { // will enable it again when a connection is needed. else if (this->state() == espbt::ClientState::IDLE) { this->disable_loop(); + } else if (this->state() == espbt::ClientState::DISCONNECTING && + (millis() - this->disconnecting_started_) > DISCONNECTING_TIMEOUT) { + ESP_LOGE(TAG, "[%d] [%s] Timeout waiting for CLOSE_EVT after disconnect, forcing IDLE", this->connection_index_, + this->address_str_); + this->set_idle_(); } } @@ -178,7 +184,7 @@ void BLEClientBase::unconditional_disconnect() { this->set_address(0); this->set_state(espbt::ClientState::IDLE); } else { - this->set_state(espbt::ClientState::DISCONNECTING); + this->set_disconnecting_(); } } @@ -373,7 +379,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // 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::DISCONNECTING); + this->set_disconnecting_(); break; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 9b7fc7b5ed..b7fc772ba0 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -113,7 +113,10 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]{}; esp_bd_addr_t remote_bda_; // 6 bytes - // Group 5: 2-byte types + // Group 5: 4-byte types + uint32_t disconnecting_started_{0}; + + // Group 6: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; uint16_t mtu_{23}; @@ -142,6 +145,11 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { this->set_state(espbt::ClientState::IDLE); this->conn_id_ = UNSET_CONN_ID; } + /// Transition to DISCONNECTING and start the safety timeout. + void set_disconnecting_() { + this->disconnecting_started_ = millis(); + this->set_state(espbt::ClientState::DISCONNECTING); + } // Compact error logging helpers to reduce flash usage void log_error_(const char *message); void log_error_(const char *message, int code);