[bluetooth_connection] Move BluetoothConnection out of bluetooth_proxy (#18129)

This commit is contained in:
J. Nick Koston
2026-08-07 08:54:28 -05:00
committed by GitHub
parent 9c93d8925f
commit 2f2634bf6b
9 changed files with 163 additions and 60 deletions
+1
View File
@@ -78,6 +78,7 @@ esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/ble_device_base/* @Bl00d-B0b
esphome/components/ble_nus/* @tomaszduda23
esphome/components/bluetooth_connection/* @bdraco @jesserockz
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bm8563/* @abmantis
esphome/components/bme280_base/* @esphome/core
@@ -0,0 +1,46 @@
"""Per-platform GATT connection backends the Bluetooth proxy drives.
Auto-loaded by bluetooth_proxy, no 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 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"]
CODEOWNERS = ["@bdraco", "@jesserockz"]
bluetooth_connection_ns = cg.esphome_ns.namespace("bluetooth_connection")
@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_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
}
)
@@ -0,0 +1,37 @@
// Shared types for the per-platform GATT connection backends and the
// Bluetooth proxy that drives them.
#pragma once
#include "esphome/core/defines.h"
#include "esphome/components/ble_device_base/ble_client_state.h"
#ifdef USE_ESP32
#include <esp_err.h>
#endif
namespace esphome::bluetooth_connection {
// Connection-owned error type for the API error fields, which are plain
// integers on the wire. Aliases esp_err_t on esp32 (where the values come from
// IDF calls); a bare int elsewhere. Owning the name instead of probing for
// esp_err_t keeps the header independent of how a platform's SDK spells its
// error type.
#ifdef USE_ESP32
using conn_err_t = esp_err_t;
static constexpr conn_err_t CONN_OK = ESP_OK;
#else
using conn_err_t = int;
static constexpr conn_err_t CONN_OK = 0;
#endif
// The ESPHome-private "not connected" wire value, shared with the neutral
// GATT contract so backend and wrapper cannot drift.
static constexpr conn_err_t GATT_NOT_CONNECTED = ble_device_base::GATT_ERR_NOT_CONNECTED;
// send_service_ cursor states; >= 0 is the next service index to stream.
static constexpr int DONE_SENDING_SERVICES = -2;
static constexpr int INIT_SENDING_SERVICES = -3;
} // namespace esphome::bluetooth_connection
@@ -1,4 +1,4 @@
#include "bluetooth_connection.h"
#include "bluetooth_connection_esp32.h"
#include "esphome/components/api/api_pb2.h"
#include "esphome/core/helpers.h"
@@ -6,11 +6,13 @@
#ifdef USE_ESP32
#include "bluetooth_proxy.h"
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
namespace esphome::bluetooth_proxy {
namespace esphome::bluetooth_connection {
static const char *const TAG = "bluetooth_proxy.connection";
namespace espbt = esphome::esp32_ble_tracker;
static const char *const TAG = "bluetooth_connection";
// This function is allocation-free and directly packs UUIDs into the output array
// using precalculated constants for the Bluetooth base UUID
@@ -516,7 +518,7 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
if (!this->connected()) {
this->log_gatt_not_connected_("read", "characteristic");
return ESP_GATT_NOT_CONNECTED;
return GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
@@ -529,7 +531,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8
bool response) {
if (!this->connected()) {
this->log_gatt_not_connected_("write", "characteristic");
return ESP_GATT_NOT_CONNECTED;
return GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
@@ -545,7 +547,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8
esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
if (!this->connected()) {
this->log_gatt_not_connected_("read", "descriptor");
return ESP_GATT_NOT_CONNECTED;
return GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
@@ -556,7 +558,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
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 ESP_GATT_NOT_CONNECTED;
return GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
@@ -572,7 +574,7 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *
esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) {
if (!this->connected()) {
this->log_gatt_not_connected_("notify", "characteristic");
return ESP_GATT_NOT_CONNECTED;
return GATT_NOT_CONNECTED;
}
if (enable) {
@@ -592,6 +594,6 @@ esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisemen
return this->proxy_->get_advertisement_parser_type();
}
} // namespace esphome::bluetooth_proxy
} // namespace esphome::bluetooth_connection
#endif // USE_ESP32
@@ -1,12 +1,18 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esphome/components/esp32_ble_client/ble_client_base.h"
namespace esphome::bluetooth_proxy {
#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:
@@ -31,7 +37,7 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
void set_address(uint64_t address) override;
protected:
friend class BluetoothProxy;
friend class bluetooth_proxy::BluetoothProxy;
void on_disconnect_complete(esp_err_t reason) override;
@@ -47,16 +53,16 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
// Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)
BluetoothProxy *proxy_;
bluetooth_proxy::BluetoothProxy *proxy_;
// Group 2: 2-byte types
int16_t send_service_{-3}; // -3 = INIT_SENDING_SERVICES, -2 = DONE_SENDING_SERVICES, >=0 = service index
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_proxy
} // namespace esphome::bluetooth_connection
#endif // USE_ESP32
+11 -8
View File
@@ -2,7 +2,7 @@ import functools
import logging
import esphome.codegen as cg
from esphome.components import ble_device_base
from esphome.components import ble_device_base, bluetooth_connection
import esphome.config_validation as cv
from esphome.const import CONF_ACTIVE, CONF_ID, PLATFORM_LN882X, PLATFORM_RP2
from esphome.core import CORE
@@ -27,13 +27,18 @@ 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 ["esp32_ble_client", "esp32_ble_tracker"]
return ["bluetooth_connection", "esp32_ble_client", "esp32_ble_tracker"]
if CORE.target_platform in _HUB_PLATFORMS:
return ["ble_device_base"]
return ["ble_device_base", "bluetooth_connection"]
# No target platform, or one this component does not support: tooling
# resolving the manifest (including the host-pinned dependency resolver) —
# expose every arm so the closure keeps the esp32 BLE stack.
return ["ble_device_base", "esp32_ble_client", "esp32_ble_tracker"]
return [
"ble_device_base",
"bluetooth_connection",
"esp32_ble_client",
"esp32_ble_tracker",
]
# Platforms with an in-tree ble_device_base BLE tracker hub whose controller
@@ -67,7 +72,7 @@ _IDF_MAX_CONNECTIONS = 9
@functools.cache
def _esp32_config_schema() -> cv.All:
"""Build the esp32 schema, importing the esp32 BLE stack only when used."""
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
from esphome.components import esp32_ble, esp32_ble_tracker
if esp32_ble.IDF_MAX_CONNECTIONS != _IDF_MAX_CONNECTIONS:
raise cv.Invalid(
@@ -77,9 +82,7 @@ def _esp32_config_schema() -> cv.All:
f"update _IDF_MAX_CONNECTIONS in bluetooth_proxy/__init__.py"
)
BluetoothConnection = bluetooth_proxy_ns.class_(
"BluetoothConnection", esp32_ble_client.BLEClientBase
)
BluetoothConnection = bluetooth_connection.esp32_connection_class()
CONNECTION_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BluetoothConnection),
@@ -124,7 +124,7 @@ void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *typ
void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action,
const char *type) {
this->log_not_connected_gatt_(action, type);
this->send_gatt_error(address, handle, ESP_GATT_NOT_CONNECTED);
this->send_gatt_error(address, handle, GATT_NOT_CONNECTED);
}
#ifdef USE_ESP32
@@ -438,7 +438,7 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn
ESP_LOGW(TAG, "[%d] [%s] Cannot set connection params, not connected",
connection ? static_cast<int>(connection->connection_index_) : -1,
connection ? connection->address_str() : "unknown");
resp.error = ESP_GATT_NOT_CONNECTED;
resp.error = GATT_NOT_CONNECTED;
this->api_connection_->send_message(resp);
return;
}
@@ -498,7 +498,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE:
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT:
ESP_LOGW(TAG, "Active connections are not supported on this platform");
this->send_device_connection(msg.address, false, 0, ESP_GATT_NOT_CONNECTED);
this->send_device_connection(msg.address, false, 0, GATT_NOT_CONNECTED);
break;
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT:
// Not an error: the device is already disconnected, which is the requested state.
@@ -506,13 +506,13 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
this->send_connections_free();
break;
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR:
this->send_device_pairing(msg.address, false, ESP_GATT_NOT_CONNECTED);
this->send_device_pairing(msg.address, false, GATT_NOT_CONNECTED);
break;
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR:
this->send_device_unpairing(msg.address, false, ESP_GATT_NOT_CONNECTED);
this->send_device_unpairing(msg.address, false, GATT_NOT_CONNECTED);
break;
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE:
this->send_device_clear_cache(msg.address, false, ESP_GATT_NOT_CONNECTED);
this->send_device_clear_cache(msg.address, false, GATT_NOT_CONNECTED);
break;
}
}
@@ -546,7 +546,7 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn
return;
api::BluetoothSetConnectionParamsResponse resp;
resp.address = msg.address;
resp.error = ESP_GATT_NOT_CONNECTED;
resp.error = GATT_NOT_CONNECTED;
this->api_connection_->send_message(resp);
}
@@ -605,7 +605,7 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti
#endif
}
void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, proxy_err_t error) {
void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, conn_err_t error) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothDeviceConnectionResponse call;
@@ -633,7 +633,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) {
this->api_connection_->send_message(call);
}
void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, proxy_err_t error) {
void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, conn_err_t error) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothGATTErrorResponse call;
@@ -643,7 +643,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, proxy_er
this->api_connection_->send_message(call);
}
void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, proxy_err_t error) {
void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, conn_err_t error) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothDevicePairingResponse call;
@@ -654,7 +654,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, proxy_er
this->api_connection_->send_message(call);
}
void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, proxy_err_t error) {
void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, conn_err_t error) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothDeviceUnpairingResponse call;
@@ -667,7 +667,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, proxy
// Shared by both platform paths: the neutral bluetooth_device_request() uses it to
// answer a clear-cache request with a clean error, so it must not be esp32-guarded.
void BluetoothProxy::send_device_clear_cache(uint64_t address, bool success, proxy_err_t error) {
void BluetoothProxy::send_device_clear_cache(uint64_t address, bool success, conn_err_t error) {
if (this->api_connection_ == nullptr)
return;
api::BluetoothDeviceClearCacheResponse call;
@@ -13,13 +13,13 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/components/ble_device_base/ble_client_state.h"
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
#ifdef USE_ESP32
#include "esphome/components/esp32_ble_client/ble_client_base.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "bluetooth_connection.h"
#include "esphome/components/bluetooth_connection/bluetooth_connection_esp32.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
@@ -31,23 +31,16 @@
namespace esphome::bluetooth_proxy {
// Proxy-owned error type for the API error fields, which are plain integers on
// the wire. Aliases esp_err_t on esp32 (where the values come from IDF calls);
// a bare int elsewhere. Owning the name instead of probing for esp_err_t keeps
// the header independent of how a hub platform's SDK spells its error type.
#ifdef USE_ESP32
using proxy_err_t = esp_err_t;
static constexpr proxy_err_t PROXY_OK = ESP_OK;
#else
using proxy_err_t = int;
static constexpr proxy_err_t PROXY_OK = 0;
#endif
static constexpr proxy_err_t ESP_GATT_NOT_CONNECTED = ble_device_base::GATT_ERR_NOT_CONNECTED;
static constexpr int DONE_SENDING_SERVICES = -2;
static constexpr int INIT_SENDING_SERVICES = -3;
// The connection-domain types live in the bluetooth_connection component;
// re-exported here so the proxy code reads unqualified.
using bluetooth_connection::CONN_OK;
using bluetooth_connection::conn_err_t;
using bluetooth_connection::DONE_SENDING_SERVICES;
using bluetooth_connection::GATT_NOT_CONNECTED;
using bluetooth_connection::INIT_SENDING_SERVICES;
#ifdef USE_ESP32
using BluetoothConnection = bluetooth_connection::BluetoothConnection;
using namespace esp32_ble_client;
#endif
@@ -79,7 +72,8 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
public esp32_ble_tracker::BLEScannerStateListener,
public Component {
friend class BluetoothConnection; // Allow connection to update connections_free_response_
// Allow the connection to update connections_free_response_
friend bluetooth_connection::BluetoothConnection;
#else
class BluetoothProxy final : public Component {
#endif
@@ -129,14 +123,14 @@ class BluetoothProxy final : public Component {
void unsubscribe_api_connection(api::APIConnection *api_connection);
api::APIConnection *get_api_connection() { return this->api_connection_; }
void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, proxy_err_t error = PROXY_OK);
void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, conn_err_t error = CONN_OK);
void send_connections_free();
void send_connections_free(api::APIConnection *api_connection);
void send_gatt_services_done(uint64_t address);
void send_gatt_error(uint64_t address, uint16_t handle, proxy_err_t error);
void send_device_pairing(uint64_t address, bool paired, proxy_err_t error = PROXY_OK);
void send_device_unpairing(uint64_t address, bool success, proxy_err_t error = PROXY_OK);
void send_device_clear_cache(uint64_t address, bool success, proxy_err_t error = PROXY_OK);
void send_gatt_error(uint64_t address, uint16_t handle, conn_err_t error);
void send_device_pairing(uint64_t address, bool paired, conn_err_t error = CONN_OK);
void send_device_unpairing(uint64_t address, bool success, conn_err_t error = CONN_OK);
void send_device_clear_cache(uint64_t address, bool success, conn_err_t error = CONN_OK);
void bluetooth_scanner_set_mode(bool active);
@@ -5,12 +5,12 @@ advertisement-only arm applies its own defaults."""
import pytest
from esphome import config_validation as cv
from esphome.components import bluetooth_proxy
from esphome.components import bluetooth_connection, bluetooth_proxy
from esphome.const import CONF_ACTIVE, KEY_TARGET_PLATFORM
from esphome.core import CORE, KEY_CORE
def _set_platform(platform: str) -> None:
def _set_platform(platform: str | None) -> None:
CORE.data.setdefault(KEY_CORE, {})[KEY_TARGET_PLATFORM] = platform
@@ -46,3 +46,17 @@ def test_hub_platform_accepts_the_advertisement_only_shape() -> None:
_set_platform("ln882x")
validated = bluetooth_proxy.CONFIG_SCHEMA({})
assert validated[CONF_ACTIVE] is False
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"]