From 269d52ac2ea17f5881ef4a298a490554ab27f5cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Aug 2026 01:33:45 -0500 Subject: [PATCH] Revive backend-only builds, the slot ledger, and the table gate --- .../ble_device_base/ble_gatt_client.h | 6 +- .../bluetooth_connection/__init__.py | 82 +++++++++++++++++-- .../bluetooth_connection.cpp | 4 +- .../bluetooth_connection.h | 18 ++-- .../bluetooth_connection_bluedroid.cpp | 9 +- .../bluetooth_connection_bluedroid.h | 8 +- .../bluetooth_connection_gatt_backend.h | 2 - .../bluetooth_connection_hub.cpp | 4 +- .../bluetooth_connection_hub.h | 4 +- .../bluetooth_connection_rp2.h | 5 +- .../components/bluetooth_proxy/__init__.py | 9 +- .../bluetooth_proxy/bluetooth_proxy.cpp | 18 ++-- .../bluetooth_proxy/bluetooth_proxy.h | 14 ++-- esphome/core/defines.h | 1 + .../bluetooth_proxy/test_platform_gates.py | 26 ++++-- .../test_gatt_client_contract.cpp | 2 - 16 files changed, 146 insertions(+), 66 deletions(-) diff --git a/esphome/components/ble_device_base/ble_gatt_client.h b/esphome/components/ble_device_base/ble_gatt_client.h index 327f372f3a..6208831fd4 100644 --- a/esphome/components/ble_device_base/ble_gatt_client.h +++ b/esphome/components/ble_device_base/ble_gatt_client.h @@ -139,10 +139,8 @@ concept BLEGattConnectionContract = requires(T conn, GattClientListener *listene { conn.update_connection_params(uint16_t{}, uint16_t{}, uint16_t{}, uint16_t{}) } -> std::same_as; { conn.get_service_table() } -> std::same_as; { conn.release_services() } -> std::same_as; - // Deferred-disconnect visibility and the connection-type hint; backends - // without the underlying state carry inline no-ops. - { conn.disconnect_pending() } -> std::same_as; - { conn.cancel_pending_disconnect() } -> std::same_as; + // Connection-type hint for backends that tune parameters by it; others + // carry an inline no-op. { conn.set_connection_type(ConnectionType{}) } -> std::same_as; }; diff --git a/esphome/components/bluetooth_connection/__init__.py b/esphome/components/bluetooth_connection/__init__.py index ca34fcade6..fab9eebd07 100644 --- a/esphome/components/bluetooth_connection/__init__.py +++ b/esphome/components/bluetooth_connection/__init__.py @@ -1,12 +1,14 @@ """Per-platform GATT connection backends and the helpers to embed one. -Backends: esp32 Bluedroid, rp2 BTstack. No user-facing configuration; the -Bluetooth proxy's codegen declares and registers the backend instances -through gatt_client_schema()/hub_connection_schema() + new_gatt_backend(). +Backends: esp32 Bluedroid, rp2 BTstack. No user-facing configuration; a +consumer's codegen declares and registers the backend instances — the +Bluetooth proxy through its per-slot connection wrappers (a streaming +consumer), and the neutral ble_client through gatt_client_schema() + +new_gatt_backend(). """ from collections.abc import Awaitable, Callable -from dataclasses import dataclass +from dataclasses import dataclass, field import esphome.codegen as cg from esphome.config_helpers import filter_source_files_from_platform @@ -19,6 +21,15 @@ DOMAIN = "bluetooth_connection" def AUTO_LOAD() -> list[str]: + """ble_device_base plus the platform BLE stack the build's backend + registers with, so consumers stay platform-blind. The platform-less arm + serves tooling that resolves the manifest without a target.""" + if CORE.is_esp32: + return ["ble_device_base", "esp32_ble_tracker"] + if CORE.is_rp2: + return ["ble_device_base", "rp2040_ble"] + if CORE.target_platform is None: + return ["ble_device_base", "esp32_ble_tracker", "rp2040_ble"] return ["ble_device_base"] @@ -92,6 +103,9 @@ _PLATFORM_BACKENDS: dict[str, _PlatformBackend] = { PLATFORM_RP2: _PlatformBackend(RP2GattClient, _rp2_schema_fragment, _rp2_register), } +# Gates dedicated-backend consumers (cv.only_on). +GATT_CLIENT_PLATFORMS = list(_PLATFORM_BACKENDS) + def _backend_entry(platform: str | None = None) -> _PlatformBackend: key = platform if platform is not None else CORE.target_platform @@ -122,14 +136,70 @@ def hub_connection_schema(platform: str | None = None) -> cv.Schema: ) -async def new_gatt_backend(config: ConfigType) -> cg.MockObj: +@dataclass +class _SlotLedger: + """GATT connection slots claimed this run, for the platform cap check.""" + + consumers: list[str] = field(default_factory=list) + + +def _ledger() -> _SlotLedger: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = _SlotLedger() + return CORE.data[DOMAIN] + + +def consume_gatt_slot(consumer: str, count: int = 1): + """Validator claiming GATT connection slots — the one spelling for every + claimant (the proxy per configured slot, dedicated backends once). The + neutral ledger feeds the platform cap check in FINAL_VALIDATE_SCHEMA; + esp32 additionally charges the controller's connection budget.""" + + def validator(config: ConfigType) -> ConfigType: + _ledger().consumers.extend([consumer] * count) + if CORE.is_esp32: + from esphome.components import esp32_ble + + esp32_ble.consume_connection_slots(count, consumer)(config) + return config + + return validator + + +def _validate_slot_totals(config: ConfigType) -> ConfigType: + # esp32 has its own controller budget (esp32_ble); the hub platforms cap + # at the prebuilt stack's client count, and nothing else counts claims + # across components (e.g. a proxy plus a radon_eye_rd200 on rp2). + if (cap := HUB_MAX_CONNECTIONS.get(CORE.target_platform)) is None: + return config + claimed = _ledger().consumers + if len(claimed) > cap: + raise cv.Invalid( + f"{CORE.target_platform} supports at most {cap} GATT client " + f"connection(s); {len(claimed)} requested by: {', '.join(claimed)}" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _validate_slot_totals + + +async def new_gatt_backend( + config: ConfigType, *, service_table: bool = True +) -> cg.MockObj: """Instantiate the backend declared by gatt_client_schema() and register it with its platform stack. The connection slot is claimed at validation - (the proxy's slot validators), not here. + (the consume_gatt_slot validators), not here. + + service_table compiles the on-demand service-table materializer into the + backend; direct consumers need it, the streaming proxy does not, so + proxy-only builds keep the smaller footprint. """ from esphome.components import ble_device_base ble_device_base.request_gatt_client() + if service_table: + cg.add_define("USE_BLE_GATT_SERVICE_TABLE") backend = cg.new_Pvariable(config[CONF_BACKEND_ID]) # The backend has no user-facing component options; an empty config keeps # the consumer's own keys (update_interval, ...) off it. diff --git a/esphome/components/bluetooth_connection/bluetooth_connection.cpp b/esphome/components/bluetooth_connection/bluetooth_connection.cpp index ed4a6e73ac..c2d089d101 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection.cpp @@ -5,7 +5,7 @@ #include #endif -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY #include "esphome/components/api/api_pb2.h" #include "esphome/core/log.h" @@ -44,7 +44,7 @@ BatchClose close_service_batch(api::BluetoothGATTGetServicesResponse &resp, size } // namespace esphome::bluetooth_connection -#endif // BLUETOOTH_CONNECTION_HAS_GATT +#endif // BLUETOOTH_CONNECTION_SERVES_PROXY #ifdef USE_ESP32 namespace esphome::bluetooth_connection { diff --git a/esphome/components/bluetooth_connection/bluetooth_connection.h b/esphome/components/bluetooth_connection/bluetooth_connection.h index cdf7076ab7..bacfd693a4 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection.h @@ -16,14 +16,16 @@ #include #endif -// The connection-aware API request handlers are compiled: a GATT backend is -// wired by codegen (one slot per connection). This is the single spelling of -// that predicate - the hub wrapper and the API request handlers gate on it. +// The proxy-serving surface is compiled: a proxy is present and a GATT +// backend is wired by codegen (one slot per connection). This is the single +// spelling of that predicate - the hub wrapper, the connection-aware API +// request handlers, and the Bluedroid in-place streamer all gate on it. // Advertisement-only builds get the clean-error handlers; address-scoped // maintenance (unpair, cache clear) still works there through the -// per-platform free functions below. -#ifdef USE_BLE_GATT_CLIENT -#define BLUETOOTH_CONNECTION_HAS_GATT +// per-platform free functions below. Backend-only builds (a dedicated-backend +// consumer without bluetooth_proxy) compile none of this API surface. +#if defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUETOOTH_PROXY) +#define BLUETOOTH_CONNECTION_SERVES_PROXY #endif namespace esphome::api { @@ -139,7 +141,7 @@ inline void fill_gatt_uuid(std::array &uuid_128, uint32_t &short_uu } } -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY /// Result of close_service_batch: keep filling the batch or send it now. /// An oversized service is packed alone; a failed (backpressured) send is /// retried from the batch start, so no service is silently skipped. @@ -151,6 +153,6 @@ enum class BatchClose : uint8_t { CONTINUE, SEND }; /// cannot drift. BatchClose close_service_batch(api::BluetoothGATTGetServicesResponse &resp, size_t ¤t_size, int16_t &send_service, uint8_t connection_index, const char *address_str); -#endif // BLUETOOTH_CONNECTION_HAS_GATT +#endif // BLUETOOTH_CONNECTION_SERVES_PROXY } // namespace esphome::bluetooth_connection diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index cedc3898c6..8b1cd7fc36 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -2,10 +2,11 @@ #if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT) +#include "bluetooth_connection.h" + // The in-place streamer serves the proxy's service-discovery API; backend-only // builds compile without the proxy headers or the streamer. -#ifdef USE_BLUETOOTH_PROXY -#include "bluetooth_connection.h" +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY #include "bluetooth_connection_hub.h" #include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" @@ -485,7 +486,7 @@ void BluedroidGattClient::handle_search_cmpl_() { this->listener_->on_service_discovery_done(0); } -#ifdef USE_BLUETOOTH_PROXY +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) { if (this->services_released_ || conn.send_service_ >= this->service_total_) { conn.send_service_ = DONE_SENDING_SERVICES; @@ -624,7 +625,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) { conn.send_service_ = batch_start; } } -#endif // USE_BLUETOOTH_PROXY +#endif // BLUETOOTH_CONNECTION_SERVES_PROXY // ---- events ---- diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h index 500c0a3f83..8b9546a6b0 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h @@ -13,6 +13,8 @@ #if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT) +#include "bluetooth_connection.h" + #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" @@ -23,7 +25,7 @@ namespace esphome::bluetooth_connection { class BluedroidGattClient; -#ifdef USE_BLUETOOTH_PROXY +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY class BluetoothConnection; #endif @@ -82,7 +84,7 @@ class BluedroidGattClient final : public Component { #endif void release_services(); -#ifdef USE_BLUETOOTH_PROXY +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY /// In-place service streamer (the proxy wrapper detects and prefers it): /// builds one api response batch directly from Bluedroid's cached database, /// so the streaming peak is the response itself - the old esp32 model. @@ -90,8 +92,6 @@ class BluedroidGattClient final : public Component { #endif void set_connection_type(ble_device_base::ConnectionType ct) { this->connection_type_ = ct; } - bool disconnect_pending() const { return this->shim_.disconnect_pending(); } - void cancel_pending_disconnect() { this->shim_.cancel_pending_disconnect(); } protected: friend class BluedroidTrackerShim; diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h b/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h index f0615e34f3..d0ff875c7e 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h @@ -44,8 +44,6 @@ class StubGattBackend { return ble_device_base::GATT_ERR_NOT_CONNECTED; } ble_device_base::GattServiceTable get_service_table() { return {}; } - bool disconnect_pending() const { return false; } - void cancel_pending_disconnect() {} void set_connection_type(ble_device_base::ConnectionType ct) {} void release_services() {} }; diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp index c9c746cdf5..cb398193d4 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp @@ -1,7 +1,7 @@ // The proxy's per-slot connection wrapper, shared by every platform. #include "bluetooth_connection_hub.h" -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY #include "esphome/components/api/api_pb2.h" #include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" @@ -436,4 +436,4 @@ void BluetoothConnection::send_service_for_discovery_() { } // namespace esphome::bluetooth_connection -#endif // BLUETOOTH_CONNECTION_HAS_GATT +#endif // BLUETOOTH_CONNECTION_SERVES_PROXY diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h index 59aa1d6bb6..cb8e2a1b1e 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h @@ -10,7 +10,7 @@ // The wrapper exists to serve the proxy's API surface; direct consumers // drive the backend themselves, so backend-only builds compile this header // empty. -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY #include "esphome/components/ble_device_base/ble_client_state.h" #include "bluetooth_connection_gatt_backend.h" @@ -147,4 +147,4 @@ class BluetoothConnection final : public ble_device_base::GattClientListener { } // namespace esphome::bluetooth_connection -#endif // BLUETOOTH_CONNECTION_HAS_GATT +#endif // BLUETOOTH_CONNECTION_SERVES_PROXY diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h index 9b96bad8bf..727952c66e 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h @@ -92,10 +92,7 @@ class RP2GattClient final : public Component, public Parented cv.All: ) elif config[CONF_ACTIVE]: connection_slots: int = config[CONF_CONNECTION_SLOTS] - esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")( + bluetooth_connection.consume_gatt_slot("bluetooth_proxy", connection_slots)( config ) @@ -160,6 +160,9 @@ def _rp2_config_schema() -> cv.All: # their ids exist for codegen (the esp32 arm's `connections` pattern). if not config[CONF_ACTIVE]: return config + bluetooth_connection.consume_gatt_slot( + "bluetooth_proxy", config[CONF_CONNECTION_SLOTS] + )(config) return { **config, CONF_CONNECTIONS: [ @@ -201,7 +204,9 @@ async def _connections_to_code(var: cg.MockObj, config: ConfigType) -> None: """One wrapper + backend pair per slot; the platform-specific backend registration lives in bluetooth_connection.new_gatt_backend().""" for connection_conf in config.get(CONF_CONNECTIONS, []): - backend = await bluetooth_connection.new_gatt_backend(connection_conf) + backend = await bluetooth_connection.new_gatt_backend( + connection_conf, service_table=False + ) connection = cg.new_Pvariable(connection_conf[CONF_ID]) cg.add(connection.set_backend(backend)) cg.add(var.register_connection(connection)) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index ea1dacda32..3f0d37b92e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -106,7 +106,7 @@ void BluetoothProxy::on_raw_advertisement_(const ble_device_base::RawAdvertiseme } } -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, ClientState state) { ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(), connection->address_str(), ble_device_base::client_state_to_string(state)); @@ -115,7 +115,7 @@ void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connec void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) { ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message); } -#endif // BLUETOOTH_CONNECTION_HAS_GATT +#endif // BLUETOOTH_CONNECTION_SERVES_PROXY void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) { ESP_LOGW(TAG, "Cannot %s GATT %s, not connected", action, type); @@ -139,7 +139,7 @@ void BluetoothProxy::dump_config() { this->get_bluetooth_mac_address_pretty(mac_str); const char *mac_out = mac_str[0] != '\0' ? mac_str : "unavailable (adapter not up yet)"; const char *scan_mode = this->configured_scan_active_ ? "active" : "passive"; -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY ESP_LOGCONFIG(TAG, "Bluetooth Proxy:\n" " Active: %s\n" @@ -157,7 +157,7 @@ void BluetoothProxy::dump_config() { #endif } -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY // maybe_unused: in a passive proxy (active: false) MAX is 0, the body is removed, and connection is unused. void BluetoothProxy::register_connection([[maybe_unused]] BluetoothConnection *connection) { @@ -426,7 +426,7 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn this->api_connection_->send_message(resp); } -#endif // BLUETOOTH_CONNECTION_HAS_GATT +#endif // BLUETOOTH_CONNECTION_SERVES_PROXY #ifdef USE_ESP32 @@ -469,7 +469,7 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { #endif // USE_ESP32 void BluetoothProxy::loop() { -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY // Stream pending service-discovery batches every iteration; the streamer // handles a vanished API connection itself. for (uint8_t i = 0; i < this->connection_count_; i++) { @@ -484,7 +484,7 @@ void BluetoothProxy::loop() { this->last_advertisement_flush_time_ = now; if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY // The API subscriber is gone: tear down any connections it left behind // (disconnect() on an already-disconnecting slot is a no-op). for (uint8_t i = 0; i < this->connection_count_; i++) { @@ -508,7 +508,7 @@ void BluetoothProxy::loop() { this->flush_pending_advertisements_(); } -#ifndef BLUETOOTH_CONNECTION_HAS_GATT +#ifndef BLUETOOTH_CONNECTION_SERVES_PROXY // Advertisement-only proxy. GATT client connections are excluded at compile // time (no connection backend on this platform, or active: false), so every @@ -580,7 +580,7 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn this->api_connection_->send_message(resp); } -#endif // !BLUETOOTH_CONNECTION_HAS_GATT +#endif // !BLUETOOTH_CONNECTION_SERVES_PROXY void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { if (this->api_connection_ != nullptr && this->api_connection_ != api_connection) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 17e480b671..785cc51fb8 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -26,7 +26,7 @@ using bluetooth_connection::conn_err_t; using bluetooth_connection::GATT_NOT_CONNECTED; using bluetooth_connection::INIT_SENDING_SERVICES; -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY using BluetoothConnection = bluetooth_connection::BluetoothConnection; using ClientState = ble_device_base::ClientState; #endif @@ -58,7 +58,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t { }; class BluetoothProxy final : public Component { -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY // Allow the connection to update connections_free_response_ friend bluetooth_connection::BluetoothConnection; #endif @@ -69,9 +69,9 @@ class BluetoothProxy final : public Component { void setup() override; void loop() override; -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY void register_connection(BluetoothConnection *connection); -#endif // BLUETOOTH_CONNECTION_HAS_GATT +#endif // BLUETOOTH_CONNECTION_SERVES_PROXY #ifndef USE_ESP32 // Run after the hub's setup() (the trackers use AFTER_WIFI): setup() below // snapshots scan_active()/scan_running() and installs the raw callback, and @@ -189,7 +189,7 @@ class BluetoothProxy final : public Component { } void log_advertisement_flush_(); -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY BluetoothConnection *get_connection_(uint64_t address, bool reserve); void log_connection_request_ignored_(BluetoothConnection *connection, ClientState state); void log_connection_info_(BluetoothConnection *connection, const char *message); @@ -197,7 +197,7 @@ class BluetoothProxy final : public Component { void log_not_connected_gatt_(const char *action, const char *type); void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type); -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY /// Keep the pre-allocated connections-free message in step when a /// connection slot changes address (0 = free). Called from the connection /// classes' set_address(). @@ -237,7 +237,7 @@ class BluetoothProxy final : public Component { // Group 1: Pointers (4 bytes each, naturally aligned) api::APIConnection *api_connection_{nullptr}; -#ifdef BLUETOOTH_CONNECTION_HAS_GATT +#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY // Group 2: Fixed-size array of connection pointers std::array connections_{}; #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index e3c4dd87ce..e87c8044f4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -307,6 +307,7 @@ #define USE_ESP32_BLE_SERVER_ON_DISCONNECT #define USE_ESP32_BLE_TRACKER #define USE_BLE_GATT_CLIENT +#define USE_BLE_GATT_SERVICE_TABLE #define ESPHOME_BLE_GATT_CLIENT_COUNT 1 #define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1 #define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1 diff --git a/tests/component_tests/bluetooth_proxy/test_platform_gates.py b/tests/component_tests/bluetooth_proxy/test_platform_gates.py index a6f7618b87..244c4bdfce 100644 --- a/tests/component_tests/bluetooth_proxy/test_platform_gates.py +++ b/tests/component_tests/bluetooth_proxy/test_platform_gates.py @@ -177,12 +177,22 @@ def test_rp2_rejects_esp32_only_keys_by_name( bluetooth_proxy.CONFIG_SCHEMA({"connections": [{}]}) -def test_bluetooth_connection_auto_load_covers_its_includes() -> None: - # 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_bluetooth_connection_auto_load_matches_the_platform_stack() -> None: + # The backend registers with its platform BLE stack, so that dependency + # lives here and consumers (proxy, radon_eye_rd200) stay platform-blind; + # the platform-less arm is the union for manifest-resolving tooling. + _set_platform("esp32") + assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base", "esp32_ble_tracker"] + _set_platform("rp2") + assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base", "rp2040_ble"] + _set_platform("ln882x") + assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base"] + _set_platform(None) + assert bluetooth_connection.AUTO_LOAD() == [ + "ble_device_base", + "esp32_ble_tracker", + "rp2040_ble", + ] def test_every_registered_hub_platform_has_a_schema_arm() -> None: @@ -193,9 +203,9 @@ def test_every_registered_hub_platform_has_a_schema_arm() -> None: registered = set(bluetooth_connection.HUB_MAX_CONNECTIONS) assert registered <= set(bluetooth_proxy._GATT_HUB_SCHEMAS) assert registered <= set(bluetooth_proxy._HUB_PLATFORMS) - # Hub platforms must also be in the backend registry the shared codegen + # Hub platforms must also be in the backend registry the shared consumer # helpers dispatch on. - assert registered <= set(bluetooth_connection._PLATFORM_BACKENDS) + assert registered <= set(bluetooth_connection.GATT_CLIENT_PLATFORMS) # The outer walkable schema's bound must stay the loosest platform cap. assert ( max(bluetooth_connection.HUB_MAX_CONNECTIONS.values()) diff --git a/tests/components/ble_device_base/test_gatt_client_contract.cpp b/tests/components/ble_device_base/test_gatt_client_contract.cpp index 76b6d8194f..ec3756157d 100644 --- a/tests/components/ble_device_base/test_gatt_client_contract.cpp +++ b/tests/components/ble_device_base/test_gatt_client_contract.cpp @@ -50,8 +50,6 @@ class MinimalConnection { } GattServiceTable get_service_table() { return {}; } void release_services() {} - bool disconnect_pending() const { return false; } - void cancel_pending_disconnect() {} void set_connection_type(ConnectionType ct) {} protected: