Bluedroid backend for the GATT client contract

This commit is contained in:
J. Nick Koston
2026-08-08 21:31:09 -05:00
parent 4f086b6b25
commit 2483621500
4 changed files with 784 additions and 0 deletions
@@ -36,6 +36,9 @@ HUB_MAX_CONNECTIONS: dict[str, int] = {PLATFORM_RP2: RP2_MAX_CONNECTIONS}
# The hub-platform wrapper and the rp2 BTstack backend codegen classes.
HubBluetoothConnection = bluetooth_connection_ns.class_("BluetoothConnection")
RP2GattClient = bluetooth_connection_ns.class_("RP2GattClient", cg.Component)
BluedroidGattClient = bluetooth_connection_ns.class_(
"BluedroidGattClient", cg.Component
)
@functools.cache
@@ -51,6 +54,10 @@ def esp32_connection_class() -> cg.MockObjClass:
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"bluetooth_connection_bluedroid.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"bluetooth_connection_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
@@ -0,0 +1,637 @@
#include "bluetooth_connection_bluedroid.h"
#if defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
#include "bluetooth_connection_hub.h"
#include "esphome/components/ble_device_base/ble_client_state.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <esp_bt.h>
#include <esp_gatt_common_api.h>
#include <cstring>
namespace esphome::bluetooth_connection {
static const char *const TAG = "bluetooth_connection.bluedroid";
using ble_device_base::FAST_CONN_TIMEOUT;
using ble_device_base::FAST_MAX_CONN_INTERVAL;
using ble_device_base::FAST_MIN_CONN_INTERVAL;
using ble_device_base::MEDIUM_CONN_TIMEOUT;
using ble_device_base::MEDIUM_MAX_CONN_INTERVAL;
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;
static constexpr uint32_t DISCONNECTING_TIMEOUT = 10000;
// ---- shim forwarders ----
bool BluedroidTrackerShim::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
return this->engine_->handle_gattc_event_(event, gattc_if, param);
}
void BluedroidTrackerShim::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
this->engine_->handle_gap_event_(event, param);
}
void BluedroidTrackerShim::connect() { this->engine_->tracker_connect_(); }
void BluedroidTrackerShim::disconnect() { this->engine_->tracker_disconnect_(); }
// ---- component ----
void BluedroidGattClient::setup() {
static uint8_t connection_index = 0;
this->connection_index_ = connection_index++;
this->conn_id_ = UNSET_CONN_ID;
}
void BluedroidGattClient::loop() {
if (!esp32_ble::global_ble->is_active()) {
// Stack down: re-register the app on the next enable.
this->set_state_(ClientState::INIT);
return;
}
auto st = this->state_();
if (st == ClientState::INIT) {
auto ret = esp_ble_gattc_app_register(this->shim_.app_id);
if (ret) {
ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->shim_.app_id, ret);
this->mark_failed();
}
// Do not wait for REG_EVT; a dropped event must not wedge the slot.
this->set_state_(ClientState::IDLE);
} else if (st == ClientState::IDLE) {
// The loop only drives the bootstrap and the disconnect safety timeout.
this->disable_loop();
} else if (st == ClientState::DISCONNECTING && millis() - this->disconnecting_started_ > DISCONNECTING_TIMEOUT) {
ESP_LOGE(TAG, "[%d] Timeout waiting for CLOSE_EVT, forcing IDLE", this->connection_index_);
// Release before idling: unconditional disconnect does not release, and a
// lost CLOSE/DISCONNECT would otherwise leak the table and the cache.
this->release_services();
this->set_idle_();
this->report_connection_state_(false, ESP_GATT_CONN_TIMEOUT);
}
}
void BluedroidGattClient::dump_config() {
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
if (this->is_failed()) {
ESP_LOGE(TAG, " Registration failed; if the error was ESP_GATT_NO_RESOURCES, reduce the connection slots");
}
}
// ---- contract ops ----
int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) {
this->address_ = address;
this->remote_bda_[0] = (address >> 40) & 0xFF;
this->remote_bda_[1] = (address >> 32) & 0xFF;
this->remote_bda_[2] = (address >> 24) & 0xFF;
this->remote_bda_[3] = (address >> 16) & 0xFF;
this->remote_bda_[4] = (address >> 8) & 0xFF;
this->remote_bda_[5] = (address >> 0) & 0xFF;
this->remote_addr_type_ = static_cast<esp_ble_addr_type_t>(addr_type);
// Hand the request to the tracker's promote loop: it stops the scan, raises
// coex, and calls tracker_connect_() - the tracker owns connect timing here.
this->set_state_(ClientState::DISCOVERED);
return 0;
}
void BluedroidGattClient::tracker_connect_() {
auto st = this->state_();
if (st == ClientState::CONNECTING || st == ClientState::CONNECTED || st == ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] Connection already in progress", this->connection_index_);
return;
}
if (st == ClientState::DISCONNECTING) {
ESP_LOGW(TAG, "[%d] Cannot connect, still waiting for CLOSE_EVT", this->connection_index_);
return;
}
ESP_LOGI(TAG, "[%d] 0x%02x Connecting", this->connection_index_, this->remote_addr_type_);
this->services_released_ = false;
this->seen_mtu_ = false;
this->enable_loop();
this->set_state_(ClientState::CONNECTING);
if (this->connection_type_ == ConnectionType::V3_WITHOUT_CACHE) {
// Fast params for the discovery phase; stepped down at SEARCH_CMPL.
esp_ble_gap_set_prefer_conn_params(this->remote_bda_, FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0,
FAST_CONN_TIMEOUT);
} else {
esp_ble_gap_set_prefer_conn_params(this->remote_bda_, MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0,
MEDIUM_CONN_TIMEOUT);
}
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, 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);
this->report_connection_state_(false, ret);
}
}
int BluedroidGattClient::disconnect() {
auto st = this->state_();
if (st == ClientState::IDLE || st == ClientState::DISCONNECTING) {
return 0;
}
if (st == ClientState::DISCOVERED) {
// Never opened: nothing to close.
this->set_state_(ClientState::IDLE);
return 0;
}
if (st == ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGD(TAG, "[%d] Disconnect scheduled", this->connection_index_);
this->shim_.schedule_disconnect();
return 0;
}
this->unconditional_disconnect_();
return 0;
}
void BluedroidGattClient::unconditional_disconnect_() {
ESP_LOGI(TAG, "[%d] Disconnecting (conn_id: %d)", this->connection_index_, this->conn_id_);
if (this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGE(TAG, "[%d] conn id unset, cannot disconnect", this->connection_index_);
return;
}
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
if (err != ESP_OK) {
// The stack is now in an indeterminate state for this link.
ESP_LOGE(TAG, "[%d] esp_ble_gattc_close error: %d", this->connection_index_, err);
}
this->set_disconnecting_();
}
int BluedroidGattClient::discover_services() {
if (this->conn_id_ == UNSET_CONN_ID) {
return ble_device_base::GATT_ERR_NOT_CONNECTED;
}
this->service_count_ = 0;
auto err = esp_ble_gattc_search_service(this->gattc_if_, this->conn_id_, nullptr);
if (err != ESP_OK) {
this->log_gattc_warning_("esp_ble_gattc_search_service", err);
}
return err;
}
int BluedroidGattClient::read_characteristic(uint16_t handle) {
return this->check_and_log_error_("esp_ble_gattc_read_char", esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_,
handle, ESP_GATT_AUTH_REQ_NONE));
}
int BluedroidGattClient::write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
// The BTC layer copies the payload immediately, so the const_cast is safe.
return this->check_and_log_error_(
"esp_ble_gattc_write_char",
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP,
ESP_GATT_AUTH_REQ_NONE));
}
int BluedroidGattClient::read_descriptor(uint16_t handle) {
return this->check_and_log_error_(
"esp_ble_gattc_read_char_descr",
esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE));
}
int BluedroidGattClient::write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
return this->check_and_log_error_(
"esp_ble_gattc_write_char_descr",
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE));
}
int BluedroidGattClient::notify_characteristic(uint16_t handle, bool enable) {
// Local registration only; the CCCD write is the API client's responsibility.
if (enable) {
return this->check_and_log_error_("esp_ble_gattc_register_for_notify",
esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle));
}
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify",
esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle));
}
int BluedroidGattClient::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
int BluedroidGattClient::update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout) {
return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom");
}
ble_device_base::GattServiceTable BluedroidGattClient::get_service_table() { return this->table_; }
void BluedroidGattClient::release_services() {
if (this->arena_ != nullptr) {
RAMAllocator<uint8_t> allocator;
allocator.deallocate(this->arena_, 0);
this->arena_ = nullptr;
}
this->table_ = {};
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
// Only the cache clean makes the stack's database unsafe to walk.
this->services_released_ = true;
esp_ble_gattc_cache_clean(this->remote_bda_);
#endif
}
// ---- internals ----
bool BluedroidGattClient::check_addr_(const esp_bd_addr_t &addr) const {
return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0;
}
void BluedroidGattClient::set_idle_() {
this->set_state_(ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID;
}
void BluedroidGattClient::set_disconnecting_() {
this->disconnecting_started_ = millis();
this->set_state_(ClientState::DISCONNECTING);
// The loop may be disabled while idle; the safety timeout needs it.
this->enable_loop();
}
void BluedroidGattClient::report_connection_state_(bool connected, int error) {
if (this->listener_ != nullptr) {
this->listener_->on_connection_state(connected, this->mtu_, error);
}
}
esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
conn_params.latency = latency;
conn_params.timeout = timeout;
ESP_LOGD(TAG, "[%d] %s conn params", this->connection_index_, param_type);
auto err = esp_ble_gap_update_conn_params(&conn_params);
if (err != ESP_OK) {
this->log_gattc_warning_("esp_ble_gap_update_conn_params", err);
}
return err;
}
int BluedroidGattClient::check_and_log_error_(const char *operation, esp_err_t err) {
if (err != ESP_OK) {
this->log_gattc_warning_(operation, err);
}
return err;
}
void BluedroidGattClient::log_gattc_warning_(const char *operation, int code) {
ESP_LOGW(TAG, "[%d] %s failed, status=%d", this->connection_index_, operation, code);
}
// ---- table materialization ----
bool BluedroidGattClient::materialize_table_() {
if (this->services_released_) {
ESP_LOGW(TAG, "[%d] Services released, cannot walk the GATT cache", this->connection_index_);
return false;
}
// Pass 1: exact counts for a single allocation.
uint16_t total_chars = 0;
uint16_t total_descs = 0;
for (uint16_t s = 0; s < this->service_count_; s++) {
esp_gattc_service_elem_t service_result;
uint16_t count = 1;
if (esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &service_result, &count, s) !=
ESP_GATT_OK ||
count == 0) {
return false;
}
uint16_t char_count = 0;
if (esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
service_result.start_handle, service_result.end_handle, 0,
&char_count) != ESP_GATT_OK) {
return false;
}
total_chars += char_count;
uint16_t char_offset = 0;
while (char_offset < char_count) {
esp_gattc_char_elem_t char_result;
uint16_t cc = 1;
auto st = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
service_result.end_handle, &char_result, &cc, char_offset);
if (st == ESP_GATT_INVALID_OFFSET || st == ESP_GATT_NOT_FOUND || cc == 0) {
break;
}
if (st != ESP_GATT_OK) {
return false;
}
uint16_t desc_count = 0;
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0,
char_result.char_handle, &desc_count);
total_descs += desc_count;
char_offset++;
}
}
const size_t svc_bytes = this->service_count_ * sizeof(ble_device_base::GattService);
const size_t chr_bytes = total_chars * sizeof(ble_device_base::GattCharacteristic);
const size_t dsc_bytes = total_descs * sizeof(ble_device_base::GattDescriptor);
RAMAllocator<uint8_t> allocator;
this->arena_ = allocator.allocate(svc_bytes + chr_bytes + dsc_bytes);
if (this->arena_ == nullptr) {
ESP_LOGE(TAG, "[%d] Service table allocation failed", this->connection_index_);
return false;
}
auto *services = reinterpret_cast<ble_device_base::GattService *>(this->arena_);
auto *chars = reinterpret_cast<ble_device_base::GattCharacteristic *>(this->arena_ + svc_bytes);
auto *descs = reinterpret_cast<ble_device_base::GattDescriptor *>(this->arena_ + svc_bytes + chr_bytes);
// Pass 2: fill. Enumeration stays bounded by the pass-1 totals - a
// misbehaving peripheral can return more entries than it reported.
uint16_t chr_index = 0;
uint16_t dsc_index = 0;
for (uint16_t s = 0; s < this->service_count_; s++) {
esp_gattc_service_elem_t service_result;
uint16_t count = 1;
if (esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &service_result, &count, s) !=
ESP_GATT_OK ||
count == 0) {
break;
}
auto &service = services[s];
service.uuid = esp32_ble_tracker::ESPBTUUID::from_uuid(service_result.uuid);
service.start_handle = service_result.start_handle;
service.end_handle = service_result.end_handle;
service.first_characteristic = chr_index;
service.characteristic_count = 0;
uint16_t char_offset = 0;
esp_gattc_char_elem_t char_result;
while (chr_index < total_chars) {
uint16_t cc = 1;
auto st = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
service_result.end_handle, &char_result, &cc, char_offset);
if (st == ESP_GATT_INVALID_OFFSET || st == ESP_GATT_NOT_FOUND || st != ESP_GATT_OK || cc == 0) {
break;
}
auto &chr = chars[chr_index];
chr.uuid = esp32_ble_tracker::ESPBTUUID::from_uuid(char_result.uuid);
chr.value_handle = char_result.char_handle;
chr.end_handle = char_result.char_handle;
chr.properties = char_result.properties;
chr.first_descriptor = dsc_index;
chr.descriptor_count = 0;
uint16_t desc_offset = 0;
esp_gattc_descr_elem_t desc_result;
while (dsc_index < total_descs) {
uint16_t dc = 1;
auto dst = esp_ble_gattc_get_all_descr(this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result,
&dc, desc_offset);
if (dst == ESP_GATT_INVALID_OFFSET || dst == ESP_GATT_NOT_FOUND || dst != ESP_GATT_OK || dc == 0) {
break;
}
descs[dsc_index].uuid = esp32_ble_tracker::ESPBTUUID::from_uuid(desc_result.uuid);
descs[dsc_index].handle = desc_result.handle;
dsc_index++;
chr.descriptor_count++;
desc_offset++;
}
chr_index++;
service.characteristic_count++;
char_offset++;
}
}
this->table_.services = services;
this->table_.characteristics = chars;
this->table_.descriptors = descs;
this->table_.service_count = this->service_count_;
this->table_.characteristic_count = chr_index;
this->table_.descriptor_count = dsc_index;
return true;
}
void BluedroidGattClient::handle_search_cmpl_() {
// Step down from the fast discovery params.
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
bool ok = this->materialize_table_();
if (this->listener_ != nullptr) {
this->listener_->on_service_discovery_done(ok ? 0 : ble_device_base::GATT_ERR_NO_MEMORY);
}
}
// ---- events ----
void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) {
this->service_count_ = 0;
auto st = this->state_();
if (st == ClientState::IDLE) {
// IDF can deliver OPEN_EVT after esp_ble_gattc_open already returned an
// error and the slot went IDLE; do not resurrect it.
ESP_LOGD(TAG, "[%d] OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, param->open.status);
return;
}
if (st != ClientState::CONNECTING) {
ESP_LOGE(TAG, "[%d] OPEN_EVT in unexpected state", this->connection_index_);
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->log_gattc_warning_("Connection open", param->open.status);
// Never established, CLOSE_EVT may not follow.
this->set_idle_();
this->report_connection_state_(false, param->open.status);
return;
}
if (this->shim_.disconnect_scheduled()) {
// Earliest point conn_id_ exists; keep it set so CLOSE_EVT still matches.
this->unconditional_disconnect_();
return;
}
this->set_state_(ClientState::CONNECTED);
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).
this->seen_mtu_ = true;
this->report_connection_state_(true, 0);
}
}
void BluedroidGattClient::handle_disconnect_evt_(esp_ble_gattc_cb_param_t *param) {
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && this->state_() == ClientState::CONNECTED) {
ESP_LOGW(TAG, "[%d] Remote closed during discovery", this->connection_index_);
} else {
ESP_LOGD(TAG, "[%d] DISCONNECT_EVT reason=0x%02x", this->connection_index_, param->disconnect.reason);
}
if (this->state_() == ClientState::IDLE) {
// 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.
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,
esp_ble_gattc_cb_param_t *param) {
if (event == ESP_GATTC_REG_EVT && this->shim_.app_id != param->reg.app_id)
return false;
if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_)
return false;
switch (event) {
case ESP_GATTC_REG_EVT: {
if (param->reg.status == ESP_GATT_OK) {
this->gattc_if_ = esp_gattc_if;
} else {
ESP_LOGE(TAG, "[%d] gattc app registration failed, status=%d", this->connection_index_, param->reg.status);
this->mark_failed();
}
break;
}
case ESP_GATTC_CONNECT_EVT: {
if (!this->check_addr_(param->connect.remote_bda))
return false;
this->conn_id_ = param->connect.conn_id;
// MTU request here rather than OPEN_EVT, matching the IDF examples.
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_send_mtu_req", ret);
}
break;
}
case ESP_GATTC_OPEN_EVT: {
if (!this->check_addr_(param->open.remote_bda))
return false;
this->handle_open_evt_(param);
break;
}
case ESP_GATTC_CFG_MTU_EVT: {
if (this->conn_id_ != param->cfg_mtu.conn_id)
return false;
if (param->cfg_mtu.status != ESP_GATT_OK) {
// Warn only; a disconnect will follow if the link is dead.
this->log_gattc_warning_("MTU exchange", param->cfg_mtu.status);
} else {
this->mtu_ = param->cfg_mtu.mtu;
}
if (!this->seen_mtu_) {
this->seen_mtu_ = true;
// The connected report waited for the MTU so HA never sees 23.
this->report_connection_state_(true, 0);
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
if (!this->check_addr_(param->disconnect.remote_bda))
return false;
this->handle_disconnect_evt_(param);
break;
}
case ESP_GATTC_CLOSE_EVT: {
if (this->conn_id_ != param->close.conn_id)
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).
this->report_connection_state_(false, param->close.reason);
break;
}
case ESP_GATTC_SEARCH_RES_EVT: {
if (this->conn_id_ != param->search_res.conn_id)
return false;
this->service_count_++;
break;
}
case ESP_GATTC_SEARCH_CMPL_EVT: {
if (this->conn_id_ != param->search_cmpl.conn_id)
return false;
ESP_LOGI(TAG, "[%d] Service discovery complete", this->connection_index_);
this->set_state_(ClientState::ESTABLISHED);
this->handle_search_cmpl_();
break;
}
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT: {
if (this->conn_id_ != param->read.conn_id)
return false;
if (this->listener_ != nullptr) {
bool ok = param->read.status == ESP_GATT_OK;
this->listener_->on_read_result(param->read.handle, ok ? param->read.value : nullptr,
ok ? param->read.value_len : 0, ok ? 0 : param->read.status);
}
break;
}
case ESP_GATTC_WRITE_CHAR_EVT:
case ESP_GATTC_WRITE_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
if (this->listener_ != nullptr) {
this->listener_->on_write_result(param->write.handle,
param->write.status == ESP_GATT_OK ? 0 : param->write.status);
}
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (this->listener_ != nullptr) {
this->listener_->on_notify_state(
param->reg_for_notify.handle, true,
param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status);
}
break;
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
if (this->listener_ != nullptr) {
this->listener_->on_notify_state(
param->unreg_for_notify.handle, false,
param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status);
}
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (this->conn_id_ != param->notify.conn_id)
return false;
ESP_LOGV(TAG, "[%d] NOTIFY_EVT handle=0x%2X", this->connection_index_, param->notify.handle);
if (this->listener_ != nullptr) {
this->listener_->on_notify_data(param->notify.handle, param->notify.value, param->notify.value_len);
}
break;
}
default:
break;
}
return true;
}
void BluedroidGattClient::handle_gap_event_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
case ESP_GAP_BLE_SEC_REQ_EVT: {
if (!this->check_addr_(param->ble_security.auth_cmpl.bd_addr))
break;
// Always accept a server-initiated security request.
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break;
}
case ESP_GAP_BLE_AUTH_CMPL_EVT: {
if (!this->check_addr_(param->ble_security.auth_cmpl.bd_addr))
break;
if (this->listener_ != nullptr) {
this->listener_->on_pairing_result(
param->ble_security.auth_cmpl.success ? 0 : param->ble_security.auth_cmpl.fail_reason);
}
break;
}
default:
break;
}
}
} // namespace esphome::bluetooth_connection
#endif // USE_ESP32 && USE_BLE_GATT_CLIENT
@@ -0,0 +1,137 @@
// Bluedroid (esp32) GATT client backend: the esp32 arm of the
// ble_device_base::BLEGattConnection alias for the hub BluetoothConnection
// wrapper. Not a BLEClientBase: the tracker's promote loop owns
// scan-stop/coex/one-connect-at-a-time, so the contract's connect() only
// parks the address in DISCOVERED and the real esp_ble_gattc_open happens in
// the tracker-invoked shim connect(). The shim exists because the tracker's
// ESPBTClient::disconnect() returns void while the contract's returns int -
// one class cannot carry both.
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
#include "esphome/components/ble_device_base/ble_gatt_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/core/component.h"
#include <esp_gap_ble_api.h>
#include <esp_gattc_api.h>
namespace esphome::bluetooth_connection {
class BluetoothConnection;
class BluedroidGattClient;
// The tracker-facing half: owns the ClientState the promote loop reads and
// forwards events/commands to the engine.
class BluedroidTrackerShim final : public esp32_ble_tracker::ESPBTClient {
public:
explicit BluedroidTrackerShim(BluedroidGattClient *engine) : engine_(engine) {}
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
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;
bool wants_parsed_advertisements() override { return false; }
void on_scan_end() override {}
bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; }
void schedule_disconnect() { this->want_disconnect_ = true; }
bool disconnect_scheduled() const { return this->want_disconnect_; }
protected:
BluedroidGattClient *engine_;
};
class BluedroidGattClient final : public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_BLUETOOTH; }
void set_listener(BluetoothConnection *listener) { this->listener_ = listener; }
esp32_ble_tracker::ESPBTClient *tracker_client() { return &this->shim_; }
// ---- ble_device_base::BLEGattConnection contract ----
int connect(uint64_t address, uint8_t addr_type);
int disconnect();
int discover_services();
int read_characteristic(uint16_t handle);
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response);
int read_descriptor(uint16_t handle);
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len);
int notify_characteristic(uint16_t handle, bool enable);
int pair();
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout);
ble_device_base::GattServiceTable get_service_table();
void release_services();
void set_connection_type(esp32_ble_tracker::ConnectionType ct) { this->connection_type_ = ct; }
bool disconnect_pending() const { return this->shim_.disconnect_scheduled(); }
void cancel_pending_disconnect() { this->shim_.cancel_pending_disconnect(); }
protected:
friend class BluedroidTrackerShim;
esp32_ble_tracker::ClientState state_() const { return this->shim_.state(); }
void set_state_(esp32_ble_tracker::ClientState st) { this->shim_.set_state(st); }
bool check_addr_(const esp_bd_addr_t &addr) const;
void tracker_connect_();
void tracker_disconnect_() { this->disconnect(); }
bool handle_gattc_event_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
void handle_gap_event_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
void handle_open_evt_(esp_ble_gattc_cb_param_t *param);
void handle_disconnect_evt_(esp_ble_gattc_cb_param_t *param);
void handle_search_cmpl_();
bool materialize_table_();
void unconditional_disconnect_();
void set_idle_();
void set_disconnecting_();
void report_connection_state_(bool connected, int error);
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);
void log_gattc_warning_(const char *operation, int code);
// Group 1: pointers / composed objects
BluedroidTrackerShim shim_{this};
BluetoothConnection *listener_{nullptr};
// One exact-size allocation holding services, then characteristics, then
// descriptors; freed by release_services().
uint8_t *arena_{nullptr};
ble_device_base::GattServiceTable table_{};
// Group 2: 8-byte types
uint64_t address_{0};
// Group 3: 4-byte types
int gattc_if_{ESP_GATT_IF_NONE};
uint32_t disconnecting_started_{0};
// Group 4: arrays
esp_bd_addr_t remote_bda_{};
// Group 5: 2-byte types
uint16_t conn_id_;
uint16_t mtu_{23};
// Group 6: 1-byte types
esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
esp32_ble_tracker::ConnectionType connection_type_{esp32_ble_tracker::ConnectionType::V3_WITHOUT_CACHE};
uint8_t connection_index_;
uint8_t service_count_{0};
// Set only when release_services() cleans the stack's GATT cache, which no
// walk may then touch (Bluedroid asserts rather than erroring).
bool services_released_{false};
// The connected report waits for the MTU exchange; OPEN_EVT alone would
// hand HA the default 23.
bool seen_mtu_{false};
};
} // namespace esphome::bluetooth_connection
#endif // USE_ESP32 && USE_BLE_GATT_CLIENT
@@ -15,6 +15,9 @@
#if defined(USE_RP2040_BLE)
#include "bluetooth_connection_rp2.h"
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::RP2GattClient
#elif defined(USE_ESP32)
#include "bluetooth_connection_bluedroid.h"
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::BluedroidGattClient
#elif defined(USE_BLE_GATT_CLIENT_STUB_BACKEND)
// Emitted only by the host unit-test manifest: the tests compile the hub
// wrapper standalone, so bind a do-nothing backend. Every other backend-less