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

# Conflicts:
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp
#	esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h
This commit is contained in:
J. Nick Koston
2026-08-09 14:38:33 -05:00
6 changed files with 39 additions and 34 deletions
@@ -32,7 +32,6 @@ using ble_device_base::MEDIUM_MIN_CONN_INTERVAL;
using esp32_ble_tracker::ClientState;
using esp32_ble_tracker::ConnectionType;
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
// Bounds one characteristic's descriptor walk against a stack that never
// reports end-of-range.
static constexpr uint16_t MAX_DESCRIPTORS_PER_CHARACTERISTIC = 64;
@@ -70,7 +69,7 @@ void BluedroidGattClient::loop() {
this->mark_failed();
}
// Do not wait for REG_EVT; a dropped event must not wedge the slot.
this->set_state(ClientState::IDLE);
this->set_idle_();
} else if (st == ClientState::DISCONNECTING || this->want_disconnect_) {
// The one teardown safety net: a lost CLOSE_EVT, or a scheduled
// teardown whose OPEN_EVT never arrives.
@@ -126,13 +125,11 @@ void BluedroidGattClient::tracker_connect_() {
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.
this->services_released_ = false;
this->seen_mtu_ = false;
this->mtu_failed_ = false;
// Per-attempt reset: the stack-down path reaches IDLE without set_idle_(),
// and a stale result must not satisfy this attempt's discovery.
this->search_state_ = SearchState::NONE;
this->search_status_ = 0;
this->enable_loop();
this->set_state(ClientState::CONNECTING);
if (this->connection_type_ == ConnectionType::V3_WITHOUT_CACHE) {
@@ -147,8 +144,8 @@ void BluedroidGattClient::tracker_connect_() {
static_cast<esp_ble_addr_type_t>(this->remote_addr_type_), true);
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_open", ret);
// CONNECT_EVT never fired, so conn_id_ is legitimately unset: plain IDLE.
this->set_state(ClientState::IDLE);
// CONNECT_EVT never fired; nothing to close.
this->set_idle_();
this->listener_->on_connection_state(false, 0, ret);
}
}
@@ -165,7 +162,7 @@ int BluedroidGattClient::gatt_disconnect() {
}
if (st == ClientState::DISCOVERED) {
// Parked for the tracker promote loop, never opened.
this->set_state(ClientState::IDLE);
this->set_idle_();
return ble_device_base::GATT_ERR_NOT_CONNECTED;
}
if (st == ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
@@ -552,9 +549,9 @@ int BluedroidGattClient::handle_search_cmpl_(esp_gatt_status_t status) {
0x0001, 0xFFFF, 0, &secondary);
if (primary_status != ESP_GATT_OK || secondary_status != ESP_GATT_OK) {
// A failed count must not become an authoritative empty database.
auto status = primary_status != ESP_GATT_OK ? primary_status : secondary_status;
this->log_gattc_warning_("esp_ble_gattc_get_attr_count", status);
return status;
auto count_status = primary_status != ESP_GATT_OK ? primary_status : secondary_status;
this->log_gattc_warning_("esp_ble_gattc_get_attr_count", count_status);
return count_status;
}
this->service_total_ = primary + secondary;
return 0;
@@ -830,7 +827,9 @@ bool BluedroidGattClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
// Warn only; a disconnect will follow if the link is dead.
this->log_gattc_warning_("MTU exchange", param->cfg_mtu.status);
}
if (!this->seen_mtu_) {
if (!this->seen_mtu_ && !this->disconnect_pending() && this->state() != ClientState::DISCONNECTING) {
// Teardown owns the link: suppress the connected report here like
// OPEN_EVT and SEARCH_CMPL do; the terminal report settles it.
this->seen_mtu_ = true;
// The connected report waited for the MTU; forwarded, not stored.
this->listener_->on_connection_state(
@@ -33,6 +33,8 @@ class BluetoothConnection;
// void disconnect() cannot overload with an int-returning twin.
class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component {
public:
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
// Lifecycle of one connection attempt's service search.
enum class SearchState : uint8_t {
NONE, // no search this attempt
@@ -126,7 +128,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
esp_bd_addr_t remote_bda_{};
// Group 4: 2-byte types
uint16_t conn_id_{0xFFFF};
uint16_t conn_id_{UNSET_CONN_ID};
uint16_t service_total_{0};
#ifdef USE_BLE_GATT_SERVICE_TABLE
// Filled element counts of the materialized table (0 when none).
@@ -26,7 +26,7 @@ void BluetoothConnection::set_address(uint64_t address) {
format_mac_addr_upper(mac, this->address_str_);
}
void BluetoothConnection::start_connect_(uint8_t address_type) {
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().
this->state_ = ClientState::CONNECTING;
@@ -38,9 +38,9 @@ void BluetoothConnection::start_connect_(uint8_t address_type) {
}
void BluetoothConnection::disconnect() {
// Idempotent: the proxy's teardown loop calls this
// every 100 ms while the API subscriber is gone, and a repeat call must not
// reach the backend (whose busy error would free the slot mid-teardown).
// Idempotent: the proxy's teardown loop calls this every 100 ms while the
// API subscriber is gone, and a repeat call reaching the backend would
// re-arm its teardown timer so the safety timeout never fires.
if (this->state_ == ClientState::IDLE || this->state_ == ClientState::DISCONNECTING) {
return;
}
@@ -83,11 +83,9 @@ void BluetoothConnection::on_connection_state(bool connected, uint16_t mtu, int
if (connected && this->address_ == 0) {
// Late completion for a slot that was already freed: nothing to report,
// and the api-gone sweep or a new reservation owns the slot now.
int err = this->backend_->gatt_disconnect();
if (err != 0 && err != GATT_NOT_CONNECTED) {
// Log only: re-arming a freed slot could clobber a new reservation.
ESP_LOGW(TAG, "[%d] freed-slot disconnect refused, err=%d", this->connection_index_, err);
}
// Return ignored: nonzero just means the backend was already idle, and
// re-arming a freed slot could clobber a new reservation.
this->backend_->gatt_disconnect();
return;
}
if (connected && this->state_ == ClientState::DISCONNECTING) {
@@ -129,7 +127,7 @@ void BluetoothConnection::on_connection_state(bool connected, uint16_t mtu, int
if (err != 0) {
ESP_LOGW(TAG, "[%d] [%s] discover_services failed, err=%d", this->connection_index_, this->address_str_, err);
// Latch the real cause for the disconnect report.
this->pending_error_ = err;
this->latch_pending_error_(err);
this->disconnect();
}
return;
@@ -148,7 +146,7 @@ void BluetoothConnection::on_service_discovery_done(int error) {
ESP_LOGW(TAG, "[%d] [%s] Service discovery failed, err=%d", this->connection_index_, this->address_str_, error);
// Carry the GATT error into the disconnection report so the client sees
// the real cause instead of a generic HCI reason.
this->pending_error_ = error;
this->latch_pending_error_(error);
this->disconnect();
return;
}
@@ -41,12 +41,9 @@ class BluetoothConnection final : public ble_device_base::GattClientListener {
conn_err_t notify_characteristic(uint16_t handle, bool enable);
conn_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout);
/// Streamer abort: latch the GATT cause (first wins), park the cursor,
/// tear down.
/// Streamer abort: latch the GATT cause, park the cursor, tear down.
void abort_service_stream(conn_err_t err) {
if (this->pending_error_ == 0) {
this->pending_error_ = err;
}
this->latch_pending_error_(err);
this->send_service_ = DONE_SENDING_SERVICES;
this->disconnect();
}
@@ -54,7 +51,7 @@ class BluetoothConnection final : public ble_device_base::GattClientListener {
/// Start connecting with the API address type (BLE_ADDR_TYPE_* code
/// space). Failures report through the same reset path a failed open
/// takes.
void initiate_connection(uint8_t address_type) { this->start_connect_(address_type); }
void initiate_connection(uint8_t address_type);
void disconnect();
bool is_paired() const { return this->paired_; }
void set_unpaired() { this->paired_ = false; }
@@ -102,7 +99,12 @@ class BluetoothConnection final : public ble_device_base::GattClientListener {
// The Bluedroid backend streams services in place from its stack cache.
friend class BluedroidGattClient;
void start_connect_(uint8_t address_type);
/// First cause wins: a later, less specific error must not overwrite it.
void latch_pending_error_(conn_err_t err) {
if (this->pending_error_ == 0) {
this->pending_error_ = err;
}
}
// A backend providing its own streamer (see the contract doc) builds the
// response in place from its stack cache; the rest use the table streamer.
// Template so the discarded branch is not odr-checked against backends
@@ -475,12 +475,14 @@ void BluetoothProxy::loop() {
for (uint8_t i = 0; i < this->connection_count_; i++) {
this->connections_[i]->process_pending_services();
}
#endif
if (this->connections_free_pending_ && this->api_connection_ != nullptr) {
// Resend a dropped slot-state update once the TCP buffer drains.
// Resend a dropped slot-state update once the TCP buffer drains; the
// advertisement-only arm answers DISCONNECT requests with this message
// too, so the drain compiles on every proxy build.
this->connections_free_pending_ = false;
this->send_connections_free(this->api_connection_);
}
#endif
// Run advertisement flush / scanner-state poll every 100ms
uint32_t now = App.get_loop_component_start_time();
@@ -1,6 +1,8 @@
# Advertisement-only proxy on esp32 by explicit choice: no GATT backend is
# compiled (USE_BLE_GATT_CLIENT unset), which pins the SERVES_PROXY gating and the
# address-scoped maintenance path that a connections build never exercises.
# Under batch grouping the active default build is what runs; the standalone
# compile of this fixture is what exercises the passive gating.
packages:
common: !include common.yaml