Report teardown once at CLOSE_EVT and refuse connects on a busy slot

This commit is contained in:
J. Nick Koston
2026-08-08 23:27:04 -05:00
parent 296dd19361
commit d135ac97e5
5 changed files with 26 additions and 12 deletions
@@ -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<int>;
{ conn.get_service_table() } -> std::same_as<GattServiceTable>;
{ conn.release_services() } -> std::same_as<void>;
// Deferred-disconnect visibility and the connection-type hint; backends
// without the underlying state carry inline no-ops.
{ conn.disconnect_pending() } -> std::same_as<bool>;
{ conn.cancel_pending_disconnect() } -> std::same_as<void>;
{ conn.set_connection_type(ConnectionType{}) } -> std::same_as<void>;
};
// The event sink the backend calls directly (the hub BluetoothConnection
@@ -12,9 +12,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_gatt_common_api.h>
#include <cstring>
@@ -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;
}
@@ -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
@@ -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,
@@ -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};