Merge branch 'esp32-gatt-backend' into neutral-ble-client

This commit is contained in:
J. Nick Koston
2026-08-09 14:46:35 -05:00
5 changed files with 20 additions and 22 deletions
@@ -163,8 +163,9 @@ _request_gatt_connection_slot = cg.slot_counter(GATT_CLIENT_COUNT_DEFINE)
def request_gatt_client() -> None:
"""Compile in the neutral GATT client contract (ble_gatt_client.h) and
claim one connection slot. Called by bluetooth_connection.new_gatt_backend()
once per backend instance."""
claim one compiled-in client slot (sizes ESPHOME_BLE_GATT_CLIENT_COUNT;
distinct from the proxy's validated connection budget). Called by
bluetooth_connection.new_gatt_backend() once per backend instance."""
cg.add_define("USE_BLE_GATT_CLIENT")
_request_gatt_connection_slot()
@@ -5,14 +5,11 @@
// Exactly one GATT backend exists per build, so BLEGattConnection is a
// compile-time alias (bluetooth_connection_gatt_backend.h), not an abstract
// interface.
// A consumer - a streaming consumer that forwards the raw database (the hub
// BluetoothConnection wrapper) or a direct consumer owning a dedicated
// backend and resolving handles by UUID - drives it and receives
// completions through the GattClientListener interface (one build can hold
// several consumer types while the backend stays a single non-virtual
// class). All listener
// calls are delivered on the ESPHome main loop; borrowed data pointers are
// valid only for the duration of the call.
// A consumer - the hub wrapper streaming the raw database, or a direct
// consumer owning a dedicated backend and resolving handles by UUID -
// drives it and receives completions through the GattClientListener
// interface. All listener calls are delivered on the ESPHome main loop;
// borrowed data pointers are valid only for the duration of the call.
//
// Error domain (plain int, forwarded to the API without translation):
// 0 success
@@ -70,7 +70,7 @@ void BluedroidGattClient::loop() {
}
// Do not wait for REG_EVT; a dropped event must not wedge the slot.
this->set_idle_();
} else if (st == ClientState::DISCONNECTING || this->want_disconnect_) {
} else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) {
// The one teardown safety net: a lost CLOSE_EVT, or a scheduled
// teardown whose OPEN_EVT never arrives.
if (millis() - this->disconnecting_started_ > ble_device_base::GATT_DISCONNECT_TIMEOUT_MS) {
@@ -734,7 +734,8 @@ void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) {
return;
}
if (this->disconnect_pending()) {
// Earliest point conn_id_ exists; keep it set so CLOSE_EVT still matches.
// Open resolved with a teardown scheduled: close now (conn_id_ stays set
// so CLOSE_EVT still matches).
this->unconditional_disconnect_();
return;
}
@@ -742,11 +743,10 @@ void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) {
ESP_LOGI(TAG, "[%d] Connection open", this->connection_index_);
if (this->connection_type_ == ConnectionType::V3_WITH_CACHE) {
this->set_state(ClientState::ESTABLISHED);
// No discovery phase: report immediately; the MTU report below is
// suppressed by seen_mtu_ (HA tolerates a post-connect MTU of 23 here,
// matching the previous esp32 behavior).
// No discovery phase: report immediately with the default MTU. The
// cached path never waits for (or reports) the exchange - seen_mtu_
// suppresses the CFG_MTU report, matching the previous esp32 behavior.
this->seen_mtu_ = true;
// Cached path never exchanged an MTU; HA has always seen the default.
this->listener_->on_connection_state(true, ble_device_base::DEFAULT_ATT_MTU, 0);
} else {
// Discovery-bound connection: start the search now so it overlaps the
@@ -151,7 +151,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
// The MTU request was refused at CONNECT_EVT; OPEN_EVT reports instead.
bool mtu_failed_ : 1 {false};
// Search issued at OPEN_EVT overlaps the MTU exchange; discover_services()
// completes from it. Reset per attempt and on idle.
// completes from it. Reset by set_idle_().
SearchState search_state_ : 4 {SearchState::NONE};
// esp_gatt_status_t of the completed search, held until claimed.
uint8_t search_status_{0};
@@ -27,8 +27,8 @@ void BluetoothConnection::set_address(uint64_t address) {
}
void BluetoothConnection::initiate_connection(uint8_t address_type) {
// No connect timeout here: the API client's own timeout or
// the api-gone sweep drives disconnect().
// No connect timeout here: the API client's own timeout or the api-gone
// sweep drives disconnect().
this->state_ = ClientState::CONNECTING;
int err = this->backend_->connect(this->address_, address_type);
if (err != 0) {
@@ -309,9 +309,9 @@ void BluetoothConnection::send_service_for_discovery_() {
}
// The subscriber vanished mid-stream: park the cursor at done WITHOUT
// sending services-done (a resubscribing client gets silence and its
// 30 s timeout, never an authoritative partial list) and
// free the table; the api-gone sweep tears the connection down anyway.
// sending services-done (a resubscribing client gets silence and its 30 s
// timeout, never an authoritative partial list) and free the table; the
// api-gone sweep tears the connection down anyway.
auto *api_conn = this->proxy_->get_api_connection();
if (api_conn == nullptr) {
ESP_LOGW(TAG, "[%d] [%s] API connection lost while streaming services", this->connection_index_,