Flip the esp32 proxy onto the hub wrapper

This commit is contained in:
J. Nick Koston
2026-08-08 22:21:38 -05:00
parent 8164cdd018
commit 3c7a1062c8
12 changed files with 111 additions and 655 deletions
@@ -5,19 +5,12 @@ user-facing configuration; the proxy's codegen declares and registers the
connection instances.
"""
import functools
import esphome.codegen as cg
from esphome.config_helpers import filter_source_files_from_platform
from esphome.const import PLATFORM_RP2, PlatformFramework
from esphome.core import CORE
def AUTO_LOAD() -> list[str]:
"""The esp32 connection header includes esp32_ble_client, so the closure
must be self-satisfying; no target platform (tooling) gets the union."""
if CORE.is_esp32 or CORE.target_platform is None:
return ["ble_device_base", "esp32_ble_client"]
return ["ble_device_base"]
@@ -41,17 +34,6 @@ BluedroidGattClient = bluetooth_connection_ns.class_(
)
@functools.cache
def esp32_connection_class() -> cg.MockObjClass:
"""Lazy: importing esp32_ble_client registers esp32-only automations as
an import side effect, which must not leak into other platforms."""
from esphome.components import esp32_ble_client
return bluetooth_connection_ns.class_(
"BluetoothConnection", esp32_ble_client.BLEClientBase
)
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"bluetooth_connection_bluedroid.cpp": {
@@ -68,6 +50,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
"bluetooth_connection_hub.cpp": {
PlatformFramework.RP2_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"bluetooth_connection_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
}
@@ -12,7 +12,9 @@
#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>
@@ -342,7 +344,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
resp.services.emplace_back();
auto &service_resp = resp.services.back();
fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid,
esp32_ble_tracker::ESPBTUUID::from_uuid(service_result.uuid), use_efficient_uuids);
ble_device_base::ESPBTUUID::from_uuid(service_result.uuid), use_efficient_uuids);
service_resp.handle = service_result.start_handle;
if (total_char_count > 0) {
@@ -368,7 +370,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
service_resp.characteristics.emplace_back();
auto &characteristic_resp = service_resp.characteristics.back();
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid,
esp32_ble_tracker::ESPBTUUID::from_uuid(char_result.uuid), use_efficient_uuids);
ble_device_base::ESPBTUUID::from_uuid(char_result.uuid), use_efficient_uuids);
characteristic_resp.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties;
@@ -396,7 +398,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
characteristic_resp.descriptors.emplace_back();
auto &descriptor_resp = characteristic_resp.descriptors.back();
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid,
esp32_ble_tracker::ESPBTUUID::from_uuid(desc_result.uuid), use_efficient_uuids);
ble_device_base::ESPBTUUID::from_uuid(desc_result.uuid), use_efficient_uuids);
descriptor_resp.handle = desc_result.handle;
desc_offset++;
}
@@ -439,7 +441,7 @@ void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) {
this->report_connection_state_(false, param->open.status);
return;
}
if (this->shim_.disconnect_scheduled()) {
if (this->shim_.disconnect_pending()) {
// Earliest point conn_id_ exists; keep it set so CLOSE_EVT still matches.
this->unconditional_disconnect_();
return;
@@ -1,482 +1,36 @@
#include "bluetooth_connection_esp32.h"
// Address-scoped Bluedroid maintenance shared by every esp32 proxy build,
// including advertisement-only ones where no GATT backend is compiled.
#include "esphome/components/api/api_pb2.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
#include "bluetooth_connection.h"
#include <esp_bt_device.h>
#include <esp_gattc_api.h>
namespace esphome::bluetooth_connection {
namespace espbt = esphome::esp32_ble_tracker;
using ble_device_base::ESPBTUUID;
static const char *const TAG = "bluetooth_connection";
namespace {
void address_to_bda(uint64_t address, esp_bd_addr_t &bda) {
for (uint8_t i = 0; i < 6; i++) {
bda[i] = (address >> ((5 - i) * 8)) & 0xFF;
}
}
} // namespace
conn_err_t unpair_device(uint64_t address) {
esp_bd_addr_t bd_addr;
ble_device_base::uint64_to_mac_msb_first(address, bd_addr);
return esp_ble_remove_bond_device(bd_addr);
esp_bd_addr_t bda;
address_to_bda(address, bda);
return esp_ble_remove_bond_device(bda);
}
conn_err_t clear_gatt_cache(uint64_t address) {
esp_bd_addr_t bd_addr;
ble_device_base::uint64_to_mac_msb_first(address, bd_addr);
return esp_ble_gattc_cache_clean(bd_addr);
}
void BluetoothConnection::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Connection:");
BLEClientBase::dump_config();
}
void BluetoothConnection::set_address(uint64_t address) {
// Keep the proxy's pre-allocated connections-free message in step
this->proxy_->update_address_slot_(this->address_, address);
// Call parent implementation to actually set the address
BLEClientBase::set_address(address);
}
void BluetoothConnection::loop() {
BLEClientBase::loop();
// Early return if no active connection
if (this->address_ == 0) {
return;
}
// Handle service discovery if in valid range
if (this->send_service_ >= 0 && this->send_service_ <= this->service_count_) {
this->send_service_for_discovery_();
}
// Check if we should disable the loop
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
// Never disable while DISCONNECTING — BLEClientBase::loop() needs to keep running so the
// 10s safety timeout can force IDLE if CLOSE_EVT is never delivered.
if (this->state() != espbt::ClientState::INIT && this->state() != espbt::ClientState::DISCONNECTING &&
(this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
this->disable_loop();
}
}
void BluetoothConnection::on_disconnect_complete(esp_err_t reason) {
// Called from both the CLOSE_EVT handler and the DISCONNECTING safety timeout in the
// base class. Free the proxy slot, notify the API client, and reset send_service_.
// address_ may already be 0 if reset_connection_ ran earlier on this teardown.
if (this->address_ == 0) {
return;
}
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_, reason);
this->reset_connection_(reason);
}
void BluetoothConnection::reset_connection_(esp_err_t reason) { this->proxy_->reset_connection_slot_(this, reason); }
void BluetoothConnection::send_service_for_discovery_() {
if (this->send_service_ >= this->service_count_) {
this->send_service_ = DONE_SENDING_SERVICES;
this->proxy_->send_gatt_services_done(this->address_);
this->release_services();
return;
}
// Early return if no API connection
auto *api_conn = this->proxy_->get_api_connection();
if (api_conn == nullptr) {
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
// Check if client supports efficient UUIDs
bool use_efficient_uuids = this->proxy_->client_supports_efficient_uuids();
// Prepare response
api::BluetoothGATTGetServicesResponse resp;
resp.address = this->address_;
// Dynamic batching based on actual size
// Keep running total of actual message size
size_t current_size = resp.calculate_size();
int16_t batch_start = this->send_service_;
while (this->send_service_ < this->service_count_) {
esp_gattc_service_elem_t service_result;
uint16_t service_count = 1;
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
&service_result, &service_count, this->send_service_);
if (service_status != ESP_GATT_OK || service_count == 0) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing",
service_status, service_count, this->send_service_);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
// Get the number of characteristics BEFORE adding to response
uint16_t total_char_count = 0;
esp_gatt_status_t char_count_status =
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, &total_char_count);
if (char_count_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
// If this service likely won't fit, send current batch (unless it's the first)
size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids);
if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) {
// This service likely won't fit, send current batch
break;
}
// Now add the service since we know it will likely fit
resp.services.emplace_back();
auto &service_resp = resp.services.back();
fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, ESPBTUUID::from_uuid(service_result.uuid),
use_efficient_uuids);
service_resp.handle = service_result.start_handle;
if (total_char_count > 0) {
// Initialize FixedVector with exact count and process characteristics
service_resp.characteristics.init(total_char_count);
uint16_t char_offset = 0;
esp_gattc_char_elem_t char_result;
// Bound by total_char_count: the vector is sized for it, and a malicious peripheral
// can make enumeration return more entries than the count query reported
while (char_offset < total_char_count) { // characteristics
uint16_t char_count = 1;
esp_gatt_status_t char_status =
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
service_result.end_handle, &char_result, &char_count, char_offset);
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
break;
}
if (char_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_all_char", char_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
if (char_count == 0) {
break;
}
service_resp.characteristics.emplace_back();
auto &characteristic_resp = service_resp.characteristics.back();
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, ESPBTUUID::from_uuid(char_result.uuid),
use_efficient_uuids);
characteristic_resp.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties;
char_offset++;
// Get the number of descriptors directly with one call
uint16_t total_desc_count = 0;
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
if (desc_count_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
if (total_desc_count == 0) {
continue;
}
// Initialize FixedVector with exact count and process descriptors
characteristic_resp.descriptors.init(total_desc_count);
uint16_t desc_offset = 0;
esp_gattc_descr_elem_t desc_result;
while (desc_offset < total_desc_count) { // descriptors
uint16_t desc_count = 1;
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
break;
}
if (desc_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
if (desc_count == 0) {
break; // No more descriptors
}
characteristic_resp.descriptors.emplace_back();
auto &descriptor_resp = characteristic_resp.descriptors.back();
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, ESPBTUUID::from_uuid(desc_result.uuid),
use_efficient_uuids);
descriptor_resp.handle = desc_result.handle;
desc_offset++;
}
}
} // end if (total_char_count > 0)
if (close_service_batch(resp, current_size, this->send_service_, this->connection_index_, this->address_str()) !=
BatchClose::CONTINUE) {
break;
}
}
// Send the message with dynamically batched services; on a failed send,
// rewind the cursor so the batch is retried instead of silently skipped.
if (!api_conn->send_message(resp)) {
ESP_LOGW(TAG, "[%d] [%s] Failed to send service batch, retrying", this->connection_index_, this->address_str_);
this->send_service_ = batch_start;
}
}
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status);
}
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err);
}
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action,
type);
}
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(),
operation, handle, status);
}
esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) {
if (err != ESP_OK) {
this->log_connection_warning_(operation, err);
return err;
}
return ESP_OK;
}
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
return false;
switch (event) {
case ESP_GATTC_DISCONNECT_EVT: {
// Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
// This prevents race condition where we mark slot as free before controller cleanup is complete
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_,
param->disconnect.reason);
// Send disconnection notification but don't free the slot yet
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
break;
}
case ESP_GATTC_OPEN_EVT: {
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->reset_connection_(param->open.status);
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
this->proxy_->send_connections_free();
}
this->seen_mtu_or_services_ = false;
break;
}
case ESP_GATTC_CFG_MTU_EVT:
case ESP_GATTC_SEARCH_CMPL_EVT: {
if (!this->seen_mtu_or_services_) {
// We don't know if we will get the MTU or the services first, so
// only send the device connection true if we have already received
// the services.
this->seen_mtu_or_services_ = true;
break;
}
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
this->proxy_->send_connections_free();
break;
}
case ESP_GATTC_READ_DESCR_EVT:
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.status != ESP_GATT_OK) {
this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status);
this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status);
break;
}
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTReadResponse resp;
resp.address = this->address_;
resp.handle = param->read.handle;
resp.set_data(param->read.value, param->read.value_len);
api_connection->send_message(resp);
break;
}
case ESP_GATTC_WRITE_CHAR_EVT:
case ESP_GATTC_WRITE_DESCR_EVT: {
if (param->write.status != ESP_GATT_OK) {
this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status);
this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status);
break;
}
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTWriteResponse resp;
resp.address = this->address_;
resp.handle = param->write.handle;
api_connection->send_message(resp);
break;
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
if (param->unreg_for_notify.status != ESP_GATT_OK) {
this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle,
param->unreg_for_notify.status);
this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status);
break;
}
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->unreg_for_notify.handle;
api_connection->send_message(resp);
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (param->reg_for_notify.status != ESP_GATT_OK) {
this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle,
param->reg_for_notify.status);
this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status);
break;
}
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->reg_for_notify.handle;
api_connection->send_message(resp);
break;
}
case ESP_GATTC_NOTIFY_EVT: {
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_,
param->notify.handle);
auto *api_connection = this->proxy_->get_api_connection();
if (api_connection == nullptr)
break;
api::BluetoothGATTNotifyDataResponse resp;
resp.address = this->address_;
resp.handle = param->notify.handle;
resp.set_data(param->notify.value, param->notify.value_len);
api_connection->send_message(resp);
break;
}
default:
break;
}
return true;
}
void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
BLEClientBase::gap_event_handler(event, param);
switch (event) {
case ESP_GAP_BLE_AUTH_CMPL_EVT:
if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0)
break;
if (param->ble_security.auth_cmpl.success) {
this->proxy_->send_device_pairing(this->address_, true);
} else {
this->proxy_->send_device_pairing(this->address_, false, param->ble_security.auth_cmpl.fail_reason);
}
break;
default:
break;
}
}
esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
if (!this->connected()) {
this->log_gatt_not_connected_("read", "characteristic");
return GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
}
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8_t *data, size_t length,
bool response) {
if (!this->connected()) {
this->log_gatt_not_connected_("write", "characteristic");
return GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
// const_cast is safe here and was previously hidden by a C-style cast
esp_err_t err =
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_write_char", err);
}
esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
if (!this->connected()) {
this->log_gatt_not_connected_("read", "descriptor");
return GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
}
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response) {
if (!this->connected()) {
this->log_gatt_not_connected_("write", "descriptor");
return GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
// const_cast is safe here and was previously hidden by a C-style cast
esp_err_t err = esp_ble_gattc_write_char_descr(
this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
}
esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) {
if (!this->connected()) {
this->log_gatt_not_connected_("notify", "characteristic");
return GATT_NOT_CONNECTED;
}
if (enable) {
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_, handle);
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
}
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_, handle);
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
esp_bd_addr_t bda;
address_to_bda(address, bda);
esp_ble_gattc_cache_clean(bda);
return CONN_OK;
}
} // namespace esphome::bluetooth_connection
@@ -1,76 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esphome/components/esp32_ble_client/ble_client_base.h"
#include "bluetooth_connection.h"
namespace esphome::bluetooth_proxy {
class BluetoothProxy;
} // namespace esphome::bluetooth_proxy
namespace esphome::bluetooth_connection {
class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
public:
void dump_config() override;
void loop() override;
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;
// The proxy's connections never consume parsed ESPBTDevice objects.
bool wants_parsed_advertisements() override { return false; }
esp_err_t read_characteristic(uint16_t handle);
esp_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response);
esp_err_t read_descriptor(uint16_t handle);
esp_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response);
esp_err_t notify_characteristic(uint16_t handle, bool enable);
esp_err_t 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");
}
bool has_gatt_services() const { return this->service_count_ != 0; }
/// Start connecting: record the API address type and hand the client to the
/// tracker's promote loop (it pauses the scan and opens the connection).
void initiate_connection(uint8_t address_type) {
this->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(address_type));
this->set_state(esp32_ble_tracker::ClientState::DISCOVERED);
}
void set_address(uint64_t address) override;
protected:
friend class bluetooth_proxy::BluetoothProxy;
void on_disconnect_complete(esp_err_t reason) override;
void send_service_for_discovery_();
void reset_connection_(esp_err_t reason);
void log_connection_error_(const char *operation, esp_gatt_status_t status);
void log_connection_warning_(const char *operation, esp_err_t err);
void log_gatt_not_connected_(const char *action, const char *type);
void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status);
esp_err_t check_and_log_error_(const char *operation, esp_err_t err);
// Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)
bluetooth_proxy::BluetoothProxy *proxy_;
// Group 2: 2-byte types
int16_t send_service_{INIT_SENDING_SERVICES}; // see bluetooth_connection.h cursor states
// Group 3: 1-byte types
bool seen_mtu_or_services_{false};
// 1 byte used, 1 byte padding
};
} // namespace esphome::bluetooth_connection
#endif // USE_ESP32
@@ -1,7 +1,7 @@
// Hub-platform connection wrapper (USE_RP2 hub builds today).
#include "bluetooth_connection_hub.h"
#if !defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
#ifdef USE_BLE_GATT_CLIENT
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
@@ -433,4 +433,4 @@ void BluetoothConnection::send_service_for_discovery_() {
} // namespace esphome::bluetooth_connection
#endif // !USE_ESP32 && USE_BLE_GATT_CLIENT
#endif // USE_BLE_GATT_CLIENT
@@ -8,7 +8,7 @@
#include "esphome/core/defines.h"
#if !defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
#ifdef USE_BLE_GATT_CLIENT
#include "bluetooth_connection.h"
@@ -52,10 +52,11 @@ class BluetoothConnection final {
bool is_paired() const { return this->paired_; }
void set_unpaired() { this->paired_ = false; }
conn_err_t pair() { return this->backend_->pair(); }
// A backend disconnect() is a single call that also cancels an in-progress
// connect; there is no deferred-disconnect state to track.
bool disconnect_pending() const { return false; }
void cancel_pending_disconnect() {}
// Backends with deferred-disconnect state (bluedroid) answer through the
// detected forwards; the rest have nothing to track. Templates so the
// discarded branch is not odr-checked against backends without the methods.
bool disconnect_pending() const { return disconnect_pending_(this->backend_); }
void cancel_pending_disconnect() { cancel_pending_(this->backend_); }
void set_address(uint64_t address);
uint64_t get_address() const { return this->address_; }
@@ -65,7 +66,12 @@ class BluetoothConnection final {
ClientState state() const { return this->state_; }
void set_state(ClientState st) { this->state_ = st; }
bool connected() const { return this->state_ == ClientState::ESTABLISHED; }
void set_connection_type(ConnectionType ct) { this->connection_type_ = ct; }
void set_connection_type(ConnectionType ct) {
this->connection_type_ = ct;
// The bluedroid backend branches on the type itself (prefer-params and
// the with-cache report at OPEN_EVT).
forward_connection_type_(this->backend_, ct);
}
// Latched at discovery completion rather than read from the backend table:
// streaming frees the table, and this must stay true for the connection's
// lifetime (esp32 parity — a repeat GetServices is silently ignored there,
@@ -102,6 +108,23 @@ class BluetoothConnection final {
// response in place from its stack cache; the rest use the table streamer.
// Template so the discarded branch is not odr-checked against backends
// that lack the method.
template<typename Backend> static bool disconnect_pending_(Backend *backend) {
if constexpr (requires { backend->disconnect_pending(); }) {
return backend->disconnect_pending();
} else {
return false;
}
}
template<typename Backend> static void cancel_pending_(Backend *backend) {
if constexpr (requires { backend->cancel_pending_disconnect(); }) {
backend->cancel_pending_disconnect();
}
}
template<typename Backend> static void forward_connection_type_(Backend *backend, ConnectionType ct) {
if constexpr (requires { backend->set_connection_type(ct); }) {
backend->set_connection_type(ct);
}
}
template<typename Backend> void stream_pending_(Backend *backend) {
if constexpr (requires { backend->stream_service_batch(*this); }) {
backend->stream_service_batch(*this);
@@ -146,4 +169,4 @@ static_assert(ble_device_base::GattClientEventSinkContract<BluetoothConnection>,
} // namespace esphome::bluetooth_connection
#endif // !USE_ESP32 && USE_BLE_GATT_CLIENT
#endif // USE_BLE_GATT_CLIENT
+15 -8
View File
@@ -27,7 +27,7 @@ def AUTO_LOAD(config: ConfigType | None = None) -> list[str]:
target platform set, so it takes one of the concrete branches.
"""
if CORE.is_esp32:
return ["bluetooth_connection", "esp32_ble_client", "esp32_ble_tracker"]
return ["bluetooth_connection", "esp32_ble_tracker"]
if CORE.target_platform in _HUB_PLATFORMS:
return ["ble_device_base", "bluetooth_connection"]
# No target platform, or one this component does not support: tooling
@@ -36,7 +36,6 @@ def AUTO_LOAD(config: ConfigType | None = None) -> list[str]:
return [
"ble_device_base",
"bluetooth_connection",
"esp32_ble_client",
"esp32_ble_tracker",
]
@@ -86,10 +85,12 @@ def _esp32_config_schema() -> cv.All:
f"update _IDF_MAX_CONNECTIONS in bluetooth_proxy/__init__.py"
)
BluetoothConnection = bluetooth_connection.esp32_connection_class()
CONNECTION_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BluetoothConnection),
cv.GenerateID(): cv.declare_id(bluetooth_connection.HubBluetoothConnection),
cv.GenerateID(CONF_BACKEND_ID): cv.declare_id(
bluetooth_connection.BluedroidGattClient
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -386,10 +387,16 @@ async def _to_code_esp32(config: ConfigType) -> None:
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count)
for connection_conf in config.get(CONF_CONNECTIONS, []):
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
await cg.register_component(connection_var, connection_conf)
cg.add(var.register_connection(connection_var))
await esp32_ble_tracker.register_raw_client(connection_var, connection_conf)
ble_device_base.request_gatt_client()
backend = cg.new_Pvariable(connection_conf[CONF_BACKEND_ID])
await cg.register_component(backend, connection_conf)
# The tracker promote loop drives connect timing through the shim.
await esp32_ble_tracker.register_raw_client(
backend.tracker_client(), connection_conf
)
connection = cg.new_Pvariable(connection_conf[CONF_ID])
cg.add(connection.set_backend(backend))
cg.add(var.register_connection(connection))
if config.get(CONF_CACHE_SERVICES):
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True)
@@ -132,13 +132,6 @@ void BluetoothProxy::log_advertisement_flush_() {
}
void BluetoothProxy::dump_config() {
#ifdef USE_ESP32
ESP_LOGCONFIG(TAG,
"Bluetooth Proxy:\n"
" Active: %s\n"
" Connections: %d",
YESNO(this->active_), this->connection_count_);
#else
// Print configured facts. dump_config runs right after setup, before the
// radio is up, so live scan state would always read "stopped" here — the
// loop's BluetoothScannerStateResponse carries the changing value instead.
@@ -162,32 +155,8 @@ void BluetoothProxy::dump_config() {
" Adapter MAC: %s",
scan_mode, mac_out);
#endif
#endif
}
#ifdef USE_ESP32
void BluetoothProxy::loop() {
// Run advertisement flush / connection cleanup every 100ms
uint32_t now = App.get_loop_component_start_time();
if (now - this->last_advertisement_flush_time_ < 100)
return;
this->last_advertisement_flush_time_ = now;
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
this->flush_pending_advertisements_();
return;
}
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
}
}
#endif // USE_ESP32
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
// maybe_unused: in a passive proxy (active: false) MAX is 0, the body is removed, and connection is unused.
@@ -200,11 +169,8 @@ void BluetoothProxy::register_connection([[maybe_unused]] BluetoothConnection *c
ESP_LOGE(TAG, "Connection registry full, dropping registration");
return;
}
#ifndef USE_ESP32
// esp32 assigns connection_index_ in BLEClientBase::setup(); the hub
// class has no Component lifecycle, so the index is assigned here.
// The hub wrapper has no Component lifecycle, so the index is assigned here.
connection->connection_index_ = this->connection_count_;
#endif
this->connections_[this->connection_count_++] = connection;
connection->proxy_ = this;
#endif
@@ -486,6 +452,29 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
#else // !USE_ESP32
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
if (this->hub_->scan_active() != active) {
ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
if (!this->hub_->request_scan_mode(active)) {
// Passive-only controller asked for active scanning; the state report
// below carries the real, unchanged mode so the subscriber does not
// assume the change happened.
ESP_LOGW(TAG, "Scanner mode %s not supported by this tracker", active ? "active" : "passive");
}
}
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
if (this->api_connection_ != nullptr) {
// Reports the mode change; the sender also refreshes last_scan_running_, so
// a failed restart (scan_running_ dropped by the tracker) is not reported
// again by loop() on the next tick. A push hub reports the restart's
// transitions (mode rides along) instead.
this->send_polled_scanner_state_();
}
#endif
}
#endif // USE_ESP32
void BluetoothProxy::loop() {
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
// Stream pending service-discovery batches every iteration (esp32 parity:
@@ -508,7 +497,7 @@ void BluetoothProxy::loop() {
// (disconnect() on an already-disconnecting backend is a no-op).
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0) {
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
}
@@ -595,29 +584,6 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn
#endif // !BLUETOOTH_CONNECTION_HAS_GATT
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
if (this->hub_->scan_active() != active) {
ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
if (!this->hub_->request_scan_mode(active)) {
// Passive-only controller asked for active scanning; the state report
// below carries the real, unchanged mode so the subscriber does not
// assume the change happened.
ESP_LOGW(TAG, "Scanner mode %s not supported by this tracker", active ? "active" : "passive");
}
}
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
if (this->api_connection_ != nullptr) {
// Reports the mode change; the sender also refreshes last_scan_running_, so
// a failed restart (scan_running_ dropped by the tracker) is not reported
// again by loop() on the next tick. A push hub reports the restart's
// transitions (mode rides along) instead.
this->send_polled_scanner_state_();
}
#endif
}
#endif // USE_ESP32
void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) {
if (this->api_connection_ != nullptr && this->api_connection_ != api_connection) {
// A previous subscriber still holds the slot. This is almost always a stale
@@ -17,9 +17,7 @@
#include "esphome/components/ble_device_base/ble_hub_impl.h"
#ifdef USE_ESP32
#include "esphome/components/bluetooth_connection/bluetooth_connection_esp32.h"
#elif defined(USE_BLE_GATT_CLIENT)
#ifdef USE_BLE_GATT_CLIENT
#include "esphome/components/bluetooth_connection/bluetooth_connection_hub.h"
#endif
+2
View File
@@ -306,6 +306,8 @@
#define USE_ESP32_BLE_SERVER_ON_CONNECT
#define USE_ESP32_BLE_SERVER_ON_DISCONNECT
#define USE_ESP32_BLE_TRACKER
#define USE_BLE_GATT_CLIENT
#define ESPHOME_BLE_GATT_CLIENT_COUNT 1
#define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1
#define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
@@ -109,6 +109,8 @@ def test_esp32_bluetooth_proxy_requests_client_slots_only(
generate_main(component_config_path("esp32_bluetooth_proxy.yaml"))
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") is None
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") == "3"
# One neutral GATT backend slot per connection (the hub-model flip).
assert get_define_value("ESPHOME_BLE_GATT_CLIENT_COUNT") == "3"
def test_counts_reset_between_compiles(
@@ -178,17 +178,11 @@ def test_rp2_rejects_esp32_only_keys_by_name(
def test_bluetooth_connection_auto_load_covers_its_includes() -> None:
# The esp32 connection header includes esp32_ble_client; the auto load
# must satisfy that closure itself (regression: it once relied on the
# consumer's auto loads).
_set_platform("esp32")
assert "esp32_ble_client" in bluetooth_connection.AUTO_LOAD()
_set_platform("rp2")
assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base"]
# No target platform (tooling resolving the manifest): the union, so
# dependency closures stay complete for build_codeowners and friends.
_set_platform(None)
assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base", "esp32_ble_client"]
# Every backend builds on ble_device_base alone; the Bluedroid backend
# talks to IDF directly, so esp32_ble_client is no longer in the closure.
for platform in ("esp32", "rp2", None):
_set_platform(platform)
assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base"]
def test_every_registered_hub_platform_has_a_schema_arm() -> None: