diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index 15f854239d..27d6899971 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -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. diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h index 0d0b4fed5b..55fc06b17a 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h @@ -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); diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index e6cdde9cda..1c49187b23 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -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" diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index e4b9cd5100..dd0958d2b8 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -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(); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 5339565a32..b3c905c8d8 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -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) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 618444e626..1a424a4a8e 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -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; }