[esp32_ble_tracker] Reset GATT clients before the BLE stack is dismantled

An idle GATT client slot has its loop disabled, so the stack-down reset in
loop() never runs across a ble.disable/ble.enable cycle. The slot keeps the
gattc interface of the torn-down stack and never registers again; the next
esp_ble_gattc_open on that interface is dropped by Bluedroid without any
event and the slot stays in CONNECTING forever, which also blocks scanning.

Give ESPBTClient a before-disabled hook, fan it out from the tracker's
existing handler, and have both client implementations settle any link,
go back to INIT and enable their loop so they register on the new stack.
The Bluedroid backend also refuses to open on an unregistered interface
instead of waiting for an event that cannot come.
This commit is contained in:
J. Nick Koston
2026-09-10 07:39:22 -05:00
parent 54706e869c
commit a84269d6b0
6 changed files with 63 additions and 11 deletions
@@ -45,15 +45,9 @@ void BluedroidGattClient::setup() {
void BluedroidGattClient::loop() {
if (!esp32_ble::global_ble->is_active()) {
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
// frees its slot, then re-register the app on the next enable.
auto down_st = this->state();
if (down_st != ClientState::IDLE && down_st != ClientState::INIT) {
this->release_services();
this->set_idle_();
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
}
this->set_state(ClientState::INIT);
// Backstop for the window between disable() and the teardown; the
// tracker's before-disabled hook is the primary path.
this->reset_for_stack_down_();
return;
}
auto st = this->state();
@@ -87,6 +81,30 @@ void BluedroidGattClient::loop() {
}
}
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
// frees its slot, then re-register the app on the next enable.
void BluedroidGattClient::reset_for_stack_down_() {
auto st = this->state();
if (st == ClientState::INIT) {
return;
}
if (st != ClientState::IDLE) {
this->release_services();
this->set_idle_();
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
}
// The interface belongs to the torn-down stack; Bluedroid drops an open
// issued with it without any event.
this->gattc_if_ = ESP_GATT_IF_NONE;
this->set_state(ClientState::INIT);
}
void BluedroidGattClient::ble_before_disabled_event_handler() {
this->reset_for_stack_down_();
// An idle slot runs no loop; the INIT branch must run to register again.
this->enable_loop();
}
void BluedroidGattClient::dump_config() {
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
if (this->is_failed()) {
@@ -97,9 +115,15 @@ void BluedroidGattClient::dump_config() {
// ---- contract ops ----
int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) {
auto st = this->state();
if (st == ClientState::INIT) {
// Nothing can be opened until the app is registered on a running stack.
ESP_LOGW(TAG, "[%d] Connect rejected, BLE stack not ready", this->connection_index_);
return ble_device_base::GATT_ERR_NOT_CONNECTED;
}
// Only from idle: clobbering DISCONNECTING would open a new link the
// stale CLOSE_EVT then tears down.
if (this->state() != ClientState::IDLE) {
if (st != ClientState::IDLE) {
ESP_LOGW(TAG, "[%d] Connect rejected, slot busy", this->connection_index_);
return ESP_GATT_BUSY;
}
@@ -121,6 +145,14 @@ void BluedroidGattClient::tracker_connect_() {
ESP_LOGW(TAG, "[%d] Cannot connect, still waiting for CLOSE_EVT", this->connection_index_);
return;
}
if (this->gattc_if_ == ESP_GATT_IF_NONE) {
// REG_EVT has not landed on this stack; Bluedroid drops an open on an
// unknown interface without any event, which would wedge the slot.
ESP_LOGW(TAG, "[%d] Connect failed, GATT app not registered", this->connection_index_);
this->set_idle_();
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
return;
}
ESP_LOGI(TAG, "[%d] 0x%02x Connecting", this->connection_index_, this->remote_addr_type_);
// Per-attempt latches; the search machine is reset by set_idle_(), the
// one door back to IDLE.
@@ -56,6 +56,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
void connect() override;
void disconnect() override;
void ble_before_disabled_event_handler() override;
bool wants_parsed_advertisements() override { return false; }
void on_scan_end() override {}
bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; }
@@ -98,6 +99,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
void unconditional_disconnect_();
void set_idle_();
void set_disconnecting_();
void reset_for_stack_down_();
esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
int check_and_log_error_(const char *operation, esp_err_t err);
@@ -72,6 +72,13 @@ void BLEClientBase::loop() {
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void BLEClientBase::ble_before_disabled_event_handler() {
// Same reset as the stack-down branch of loop(); an idle client has its
// loop disabled, so enable it for the INIT registration on the next enable.
this->set_state(espbt::ClientState::INIT);
this->enable_loop();
}
void BLEClientBase::dump_config() {
ESP_LOGCONFIG(TAG,
" Address: %s\n"
@@ -41,6 +41,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void connect() override;
esp_err_t pair();
void disconnect() override;
void ble_before_disabled_event_handler() override;
void unconditional_disconnect();
void release_services();
@@ -218,7 +218,14 @@ void ESP32BLETracker::stop_scan() {
this->stop_scan_();
}
void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); }
void ESP32BLETracker::ble_before_disabled_event_handler() {
this->stop_scan_();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->ble_before_disabled_event_handler();
}
#endif
}
bool ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
@@ -113,6 +113,9 @@ class ESPBTClient : public ESPBTDeviceListener {
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
virtual void connect() = 0;
virtual void disconnect() = 0;
/// Called right before the BLE stack is dismantled. Nothing in flight will
/// complete, and the GATT app must register again once the stack is back.
virtual void ble_before_disabled_event_handler() {}
bool disconnect_pending() const { return this->want_disconnect_; }
void cancel_pending_disconnect() { this->want_disconnect_ = false; }