diff --git a/esphome/components/ble_device_base/ble_gatt_client.h b/esphome/components/ble_device_base/ble_gatt_client.h index 81d879a704..9b55a9fcfd 100644 --- a/esphome/components/ble_device_base/ble_gatt_client.h +++ b/esphome/components/ble_device_base/ble_gatt_client.h @@ -111,6 +111,11 @@ concept BLEGattConnectionContract = requires(T conn, Sink *sink, const uint8_t * { conn.update_connection_params(uint16_t{}, uint16_t{}, uint16_t{}, uint16_t{}) } -> std::same_as; { conn.get_service_table() } -> std::same_as; { conn.release_services() } -> std::same_as; + // Deferred-disconnect visibility and the connection-type hint; backends + // without the underlying state carry inline no-ops. + { conn.disconnect_pending() } -> std::same_as; + { conn.cancel_pending_disconnect() } -> std::same_as; + { conn.set_connection_type(ConnectionType{}) } -> std::same_as; }; // The event sink the backend calls directly (the hub BluetoothConnection diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index 71e5356244..191d1ccba0 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -12,9 +12,6 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID -#include -#endif #include #include @@ -92,6 +89,13 @@ void BluedroidGattClient::dump_config() { // ---- contract ops ---- int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) { + // Refuse anything but a fully idle slot. Clobbering DISCONNECTING with + // DISCOVERED would let the tracker open a new link while the old one is + // still closing - the stale CLOSE_EVT then tears the new attempt down. + if (this->state_() != ClientState::IDLE) { + ESP_LOGW(TAG, "[%d] Connect rejected, slot busy", this->connection_index_); + return ESP_GATT_BUSY; + } ble_device_base::uint64_to_mac_msb_first(address, this->remote_bda_); this->remote_addr_type_ = addr_type; // Hand the request to the tracker's promote loop: it stops the scan, raises @@ -488,11 +492,12 @@ void BluedroidGattClient::handle_disconnect_evt_(esp_ble_gattc_cb_param_t *param // Active close delivers CLOSE_EVT first; never walk back to DISCONNECTING. return; } - // Passive disconnect: report now, but wait for CLOSE_EVT before going IDLE - - // reconnecting earlier makes the controller reject with 133 or assert. + // Passive disconnect: wait for CLOSE_EVT before going IDLE (reconnecting + // earlier makes the controller reject with 133 or assert) and before + // reporting - the wrapper frees the slot on the report, and a freed slot + // invites a reconnect into the still-closing link. this->release_services(); this->set_disconnecting_(); - this->report_connection_state_(false, param->disconnect.reason); } bool BluedroidGattClient::handle_gattc_event_(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, @@ -556,9 +561,8 @@ bool BluedroidGattClient::handle_gattc_event_(esp_gattc_cb_event_t event, esp_ga return false; this->release_services(); this->set_idle_(); - // The wrapper frees the slot on this final report; after a passive - // disconnect this is the second connected=false, matching the previous - // esp32 behavior (report at DISCONNECT, slot free at CLOSE). + // The one connected=false report: the wrapper frees the slot on it, + // so it must not fire before the controller finished closing. this->report_connection_state_(false, param->close.reason); break; } diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp index 142b1b65d9..1c716a5605 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp @@ -21,8 +21,7 @@ conn_err_t unpair_device(uint64_t address) { conn_err_t clear_gatt_cache(uint64_t address) { esp_bd_addr_t bda; ble_device_base::uint64_to_mac_msb_first(address, bda); - esp_ble_gattc_cache_clean(bda); - return CONN_OK; + return esp_ble_gattc_cache_clean(bda); } } // namespace esphome::bluetooth_connection diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp index c2cf849348..dfe7054768 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp @@ -130,7 +130,10 @@ void BluetoothConnection::on_connection_state(bool connected, uint16_t mtu, int if (this->connection_type_ == ConnectionType::V3_WITH_CACHE) { // The API client has the services cached; never discover them. No // discovery phase needs the fast interval, so settle straight into the - // shared steady-state parameters (same lifecycle place as esp32). + // shared steady-state parameters. On esp32 the backend already set the + // same values as prefer-params before opening, so this request is + // usually redundant there - kept because rp2 has no prefer-params and + // the explicit update is its only path to the steady-state interval. this->state_ = ClientState::ESTABLISHED; int param_err = this->backend_->update_connection_params(ble_device_base::MEDIUM_MIN_CONN_INTERVAL, ble_device_base::MEDIUM_MAX_CONN_INTERVAL, 0, diff --git a/tests/components/ble_device_base/test_gatt_client_contract.cpp b/tests/components/ble_device_base/test_gatt_client_contract.cpp index 25b6cbf002..d92b5561c0 100644 --- a/tests/components/ble_device_base/test_gatt_client_contract.cpp +++ b/tests/components/ble_device_base/test_gatt_client_contract.cpp @@ -51,6 +51,9 @@ class MinimalConnection { } GattServiceTable get_service_table() { return {}; } void release_services() {} + bool disconnect_pending() const { return false; } + void cancel_pending_disconnect() {} + void set_connection_type(ConnectionType ct) {} protected: RecordingSink *listener_{nullptr};