From 04384e0f5bcfbdba770f7133583eb5a425761ced Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Aug 2026 15:59:32 -0500 Subject: [PATCH] [ble_device_base] Bind the GATT backend at compile time (#18185) --- .../ble_device_base/ble_gatt_client.h | 118 ++++++++---------- esphome/components/ble_device_base/ble_hub.h | 5 +- .../bluetooth_connection_gatt_backend.h | 64 ++++++++++ .../bluetooth_connection_hub.cpp | 2 +- .../bluetooth_connection_hub.h | 32 ++--- .../bluetooth_connection_rp2.cpp | 2 + .../bluetooth_connection_rp2.h | 47 +++---- .../test_gatt_client_contract.cpp | 65 +++++----- .../bluetooth_connection/__init__.py | 1 + 9 files changed, 199 insertions(+), 137 deletions(-) create mode 100644 esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h diff --git a/esphome/components/ble_device_base/ble_gatt_client.h b/esphome/components/ble_device_base/ble_gatt_client.h index 37edc570ec..74548f578f 100644 --- a/esphome/components/ble_device_base/ble_gatt_client.h +++ b/esphome/components/ble_device_base/ble_gatt_client.h @@ -2,12 +2,13 @@ // // Platform-neutral GATT client connection contract. // -// A platform's GATT client backend (bluetooth_connection/esp32, -// bluetooth_connection/rp2) implements BLEGattConnection; consumers -// (bluetooth_proxy) drive it through this interface and receive -// completions through GattClientEventListener. All listener callbacks are -// delivered on the ESPHome main loop; borrowed data pointers are valid only -// for the duration of the call. +// Exactly one GATT backend exists per build, so BLEGattConnection is a +// compile-time alias (bluetooth_connection_gatt_backend.h), not an abstract +// interface. +// The hub BluetoothConnection wrapper drives it and receives completions +// through its event-sink methods, which the backend calls directly. All sink +// calls are delivered on the ESPHome main loop; borrowed data pointers are +// valid only for the duration of the call. // // Error domain (plain int, forwarded to the API without translation): // 0 success @@ -29,6 +30,7 @@ #include "ble_client_state.h" #include "ble_device.h" +#include #include namespace esphome::ble_device_base { @@ -76,67 +78,53 @@ struct GattServiceTable { uint16_t descriptor_count{0}; }; -/// Completion/event sink for a GATT connection. Implemented by the consumer -/// (bluetooth_proxy's connection wrapper). Every callback runs on the main loop. -class GattClientEventListener { - public: - virtual ~GattClientEventListener() = default; - - /// Connected (with negotiated MTU) or disconnected/connect-failed - /// (error = HCI status or disconnect reason). - virtual void on_connection_state(bool connected, uint16_t mtu, int error) = 0; - /// Service discovery finished; on success the service table is populated. - virtual void on_service_discovery_done(int error) = 0; - /// Characteristic or descriptor read finished. data/len valid during the call. - virtual void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) = 0; - /// Characteristic write-with-response or descriptor write finished. - virtual void on_write_result(uint16_t handle, int error) = 0; - /// Notification/indication registration state changed. - virtual void on_notify_state(uint16_t handle, bool enabled, int error) = 0; - /// Notification/indication data from the peer. data/len valid during the call. - virtual void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) = 0; - virtual void on_pairing_result(int status) {} +// The BLEGattConnection op surface, asserted where the alias binds +// (bluetooth_connection_gatt_backend.h). Operations return 0 when accepted (completion arrives +// through the sink) or a synchronous error (busy, not connected, stack +// rejection); one operation may be outstanding at a time. Semantics beyond +// the signatures: +// - connect: addr_type is a BLE_ADDR_TYPE_* constant (ble_device.h). +// - disconnect: also cancels a connect in progress. +// - notify_characteristic: local registration only; the CCCD write is the +// API client's responsibility (a plain write_descriptor). +// - get_service_table/release_services: backend-owned transient storage, +// released after streaming (release is idempotent). +// - completions: connect and disconnect land in on_connection_state, +// discover_services in on_service_discovery_done, pair in +// on_pairing_result, reads in on_read_result, notify_characteristic in +// on_notify_state, characteristic writes with response and descriptor +// writes in on_write_result. +template +concept BLEGattConnectionContract = requires(T conn, Sink *sink, const uint8_t *data) { + conn.set_listener(sink); + { conn.connect(uint64_t{}, uint8_t{}) } -> std::same_as; + { conn.disconnect() } -> std::same_as; + { conn.discover_services() } -> std::same_as; + { conn.read_characteristic(uint16_t{}) } -> std::same_as; + { conn.write_characteristic(uint16_t{}, data, uint16_t{}, true) } -> std::same_as; + { conn.read_descriptor(uint16_t{}) } -> std::same_as; + { conn.write_descriptor(uint16_t{}, data, uint16_t{}) } -> std::same_as; + { conn.notify_characteristic(uint16_t{}, true) } -> std::same_as; + { conn.pair() } -> std::same_as; + { 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; }; -/// One GATT client connection slot. Operations return 0 when accepted -/// (completion arrives via the listener) or a synchronous error code -/// (busy, not connected, stack rejection). One operation may be outstanding -/// at a time; callers see a synchronous error otherwise. -class BLEGattConnection { - public: - virtual ~BLEGattConnection() = default; - - void set_listener(GattClientEventListener *listener) { this->listener_ = listener; } - - /// Start connecting to a peer. addr_type is a BLE_ADDR_TYPE_* constant - /// (ble_device.h). Completion: on_connection_state(). - virtual int connect(uint64_t address, uint8_t addr_type) = 0; - /// Disconnect (or cancel a connect in progress). Completion: on_connection_state(). - virtual int disconnect() = 0; - /// Discover the peer's services/characteristics/descriptors into the - /// service table. Completion: on_service_discovery_done(). - virtual int discover_services() = 0; - virtual int read_characteristic(uint16_t handle) = 0; - virtual int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) = 0; - virtual int read_descriptor(uint16_t handle) = 0; - virtual int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) = 0; - /// Enable/disable delivery of on_notify_data() for a characteristic value - /// handle. Local registration only — the CCCD write is the API client's - /// responsibility (it arrives as a plain write_descriptor). - virtual int notify_characteristic(uint16_t handle, bool enable) = 0; - /// Initiate pairing on the live link. Completion: on_pairing_result(). - virtual int pair() { return GATT_ERR_NOT_CONNECTED; } - virtual int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, - uint16_t timeout) = 0; - - /// Backend-owned service table (see GattServiceTable lifetime). - virtual GattServiceTable get_service_table() = 0; - /// Free the transient service table storage. Call after streaming; - /// idempotent (a call with no table held is a no-op). - virtual void release_services() = 0; - - protected: - GattClientEventListener *listener_{nullptr}; +// The event sink the backend calls directly (the hub BluetoothConnection +// wrapper), asserted where the wrapper is defined: on_connection_state +// carries the negotiated MTU and an HCI status/disconnect reason. The +// requirements check call validity, not exact parameter types; keep sink +// parameters at the documented widths (uint16_t handles and lengths). +template +concept GattClientEventSinkContract = requires(S sink, const uint8_t *data) { + { sink.on_connection_state(true, uint16_t{}, int{}) } -> std::same_as; + { sink.on_service_discovery_done(int{}) } -> std::same_as; + { sink.on_read_result(uint16_t{}, data, uint16_t{}, int{}) } -> std::same_as; + { sink.on_write_result(uint16_t{}, int{}) } -> std::same_as; + { sink.on_notify_state(uint16_t{}, true, int{}) } -> std::same_as; + { sink.on_notify_data(uint16_t{}, data, uint16_t{}) } -> std::same_as; + { sink.on_pairing_result(int{}) } -> std::same_as; }; } // namespace esphome::ble_device_base diff --git a/esphome/components/ble_device_base/ble_hub.h b/esphome/components/ble_device_base/ble_hub.h index d9a7731504..f4ad051430 100644 --- a/esphome/components/ble_device_base/ble_hub.h +++ b/esphome/components/ble_device_base/ble_hub.h @@ -58,8 +58,9 @@ struct HubCapabilities { /// may only see them where the receiver merges per address (Home Assistant does). bool merges_scan_response; /// GATT client connections are available: the platform has a - /// bluetooth_connection backend implementing ble_device_base::BLEGattConnection - /// (ble_gatt_client.h). Today: esp32 and rp2. + /// bluetooth_connection backend (rp2 binds the BLEGattConnection alias in + /// bluetooth_connection_gatt_backend.h; esp32 uses its Bluedroid client). + /// Today: esp32 and rp2. bool gatt; /// request_scan_mode() is honored at runtime. Distinct from active_scan: /// a passive-only controller (bk72xx) can never switch, and a hub may diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h b/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h new file mode 100644 index 0000000000..d8792b88c1 --- /dev/null +++ b/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h @@ -0,0 +1,64 @@ +// bluetooth_connection_gatt_backend.h +// +// Binds ble_device_base::BLEGattConnection to the build's one GATT backend. +// Backend and consumer both live in this component, so the ladder does too; +// backends implement ble_gatt_client.h (the neutral contract). + +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_BLE_GATT_CLIENT + +#include "esphome/components/ble_device_base/ble_gatt_client.h" + +#if defined(USE_RP2040_BLE) +#include "bluetooth_connection_rp2.h" +#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::RP2GattClient +#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 +// build hits the #error below. +namespace esphome::bluetooth_connection { + +class BluetoothConnection; + +class StubGattBackend { + public: + void set_listener(BluetoothConnection *listener) {} + int connect(uint64_t address, uint8_t addr_type) { return ble_device_base::GATT_ERR_NOT_CONNECTED; } + int disconnect() { return ble_device_base::GATT_ERR_NOT_CONNECTED; } + int discover_services() { return ble_device_base::GATT_ERR_NOT_CONNECTED; } + int read_characteristic(uint16_t handle) { return ble_device_base::GATT_ERR_NOT_CONNECTED; } + int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) { + return ble_device_base::GATT_ERR_NOT_CONNECTED; + } + int read_descriptor(uint16_t handle) { return ble_device_base::GATT_ERR_NOT_CONNECTED; } + int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) { + return ble_device_base::GATT_ERR_NOT_CONNECTED; + } + int notify_characteristic(uint16_t handle, bool enable) { return ble_device_base::GATT_ERR_NOT_CONNECTED; } + int pair() { return ble_device_base::GATT_ERR_NOT_CONNECTED; } + int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) { + return ble_device_base::GATT_ERR_NOT_CONNECTED; + } + ble_device_base::GattServiceTable get_service_table() { return {}; } + void release_services() {} +}; + +} // namespace esphome::bluetooth_connection +#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::StubGattBackend +#else +#error "USE_BLE_GATT_CLIENT is set but this build has no GATT backend; add an alias arm here" +#endif + +namespace esphome::ble_device_base { + +using BLEGattConnection = ESPHOME_BLE_GATT_CONNECTION_TYPE; +static_assert(BLEGattConnectionContract, + "The build's GATT backend is missing part of the BLEGattConnection surface (ble_gatt_client.h)"); +#undef ESPHOME_BLE_GATT_CONNECTION_TYPE + +} // namespace esphome::ble_device_base + +#endif // USE_BLE_GATT_CLIENT diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp index b69a07fc31..ec03f18e1d 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp @@ -96,7 +96,7 @@ void BluetoothConnection::reset_connection_(conn_err_t reason) { this->proxy_->reset_connection_slot_(this, reason); } -// ---- GattClientEventListener ---- +// ---- backend event sink ---- void BluetoothConnection::on_connection_state(bool connected, uint16_t mtu, int error) { if (connected && this->address_ == 0) { diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h index 34e400ac01..e79ee9e7a8 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h @@ -1,6 +1,6 @@ -// Hub-platform BluetoothConnection: drives a platform GATT client backend -// through the neutral ble_device_base::BLEGattConnection interface and -// translates its events into the same API messages the esp32 class emits. +// Hub-platform BluetoothConnection: drives the build's GATT backend (the +// ble_device_base::BLEGattConnection alias) and translates its events into +// the same API messages the esp32 class emits. // Presents the identical method surface, so the proxy's GATT dispatch // compiles against either class unchanged. @@ -13,7 +13,7 @@ #include "bluetooth_connection.h" #include "esphome/components/ble_device_base/ble_client_state.h" -#include "esphome/components/ble_device_base/ble_gatt_client.h" +#include "bluetooth_connection_gatt_backend.h" #include "esphome/core/helpers.h" namespace esphome::bluetooth_proxy { @@ -25,7 +25,7 @@ namespace esphome::bluetooth_connection { using ClientState = ble_device_base::ClientState; using ConnectionType = ble_device_base::ConnectionType; -class BluetoothConnection final : public ble_device_base::GattClientEventListener { +class BluetoothConnection final { public: /// Wire the platform backend. Called from codegen before setup. void set_backend(ble_device_base::BLEGattConnection *backend) { @@ -83,14 +83,14 @@ class BluetoothConnection final : public ble_device_base::GattClientEventListene this->check_disconnect_timeout_(); } - // ---- ble_device_base::GattClientEventListener ---- - void on_connection_state(bool connected, uint16_t mtu, int error) override; - void on_service_discovery_done(int error) override; - void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) override; - void on_write_result(uint16_t handle, int error) override; - void on_notify_state(uint16_t handle, bool enabled, int error) override; - void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) override; - void on_pairing_result(int status) override; + // ---- backend event sink (called directly by the backend, main loop) ---- + void on_connection_state(bool connected, uint16_t mtu, int error); + void on_service_discovery_done(int error); + void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error); + void on_write_result(uint16_t handle, int error); + void on_notify_state(uint16_t handle, bool enabled, int error); + void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len); + void on_pairing_result(int status); protected: friend class bluetooth_proxy::BluetoothProxy; @@ -102,8 +102,7 @@ class BluetoothConnection final : public ble_device_base::GattClientEventListene conn_err_t check_connected_op_(const char *action, const char *type) const; void log_gatt_operation_error_(const char *operation, uint16_t handle, int status); - // Memory optimized layout for 32-bit systems (a vptr precedes: pointers and - // 2-byte members first fill to an 8-byte boundary before address_) + // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) bluetooth_proxy::BluetoothProxy *proxy_{nullptr}; ble_device_base::BLEGattConnection *backend_{nullptr}; @@ -129,6 +128,9 @@ class BluetoothConnection final : public ble_device_base::GattClientEventListene bool services_discovered_{false}; }; +static_assert(ble_device_base::GattClientEventSinkContract, + "The hub wrapper is missing part of the event-sink surface (ble_gatt_client.h)"); + } // namespace esphome::bluetooth_connection #endif // !USE_ESP32 && USE_BLE_GATT_CLIENT diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp index cd7577e7f7..dc730659f5 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp @@ -1,4 +1,6 @@ #include "bluetooth_connection_rp2.h" + +#include "bluetooth_connection_hub.h" #include "bluetooth_connection.h" #if defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT) diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h index 1a3671354d..d5bf76e6ee 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h @@ -1,11 +1,10 @@ // RP2 (Pico W / Pico 2 W) GATT client backend over BTstack. // -// Implements ble_device_base::BLEGattConnection for the hub BluetoothConnection -// wrapper. BTstack packet handlers run in the CYW43 async-context low-priority -// IRQ (or on the main-loop stack during BluetoothLock release), so handlers -// only copy into per-instance lock-free queues/storage; loop() drains them and -// drives the state machine. Every BTstack call issued from the main loop is -// wrapped in BluetoothLock. +// The build's ble_device_base::BLEGattConnection backend (bound by alias in +// bluetooth_connection_gatt_backend.h) for the hub BluetoothConnection wrapper. BTstack packet handlers run in the +// CYW43 async-context low-priority IRQ (or on the main-loop stack during BluetoothLock release), so handlers only copy +// into per-instance lock-free queues/storage; loop() drains them and drives the state machine. Every BTstack call +// issued from the main loop is wrapped in BluetoothLock. #pragma once @@ -27,6 +26,8 @@ namespace esphome::bluetooth_connection { +class BluetoothConnection; + // Caps for the transient service table. Sized generously for real devices // (typical peripherals expose < 8 services / < 30 characteristics); a peer // exceeding a cap fails discovery with INSUFFICIENT_RESOURCES rather than @@ -72,29 +73,28 @@ static constexpr uint8_t RP2_GATT_EVENT_QUEUE_SIZE = 8; // full 512 B ATT payload, so depth buys burst tolerance at ~516 B per slot. static constexpr uint8_t RP2_GATT_NOTIFY_QUEUE_SIZE = 4; -class RP2GattClient final : public Component, - public ble_device_base::BLEGattConnection, - public Parented { +class RP2GattClient final : public Component, public Parented { public: void setup() override; void loop() override; void dump_config() override; float get_setup_priority() const override; - // ---- ble_device_base::BLEGattConnection ---- - int connect(uint64_t address, uint8_t addr_type) override; - int disconnect() override; - int discover_services() override; - int read_characteristic(uint16_t handle) override; - int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) override; - int read_descriptor(uint16_t handle) override; - int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) override; - int notify_characteristic(uint16_t handle, bool enable) override; - int pair() override; - int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, - uint16_t timeout) override; - ble_device_base::GattServiceTable get_service_table() override; - void release_services() override; + void set_listener(BluetoothConnection *listener) { this->listener_ = listener; } + + // ---- 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(); protected: // Link/engine state. Discovery and GATT ops have their own cursors below — @@ -150,6 +150,7 @@ class RP2GattClient final : public Component, } // Group 1: containers / large storage + BluetoothConnection *listener_{nullptr}; ServiceArena *arena_{nullptr}; esphome::LockFreeQueue event_queue_; esphome::EventPool event_pool_; 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 b4491295db..25b6cbf002 100644 --- a/tests/components/ble_device_base/test_gatt_client_contract.cpp +++ b/tests/components/ble_device_base/test_gatt_client_contract.cpp @@ -1,5 +1,8 @@ // The GATT client contract compiles in no real build until a hub backend is // configured; this TU pins it on the host so the header cannot rot unseen. +// The contract is a concept (BLEGattConnection is a per-platform alias), so +// the minimal backend here proves the concept stays satisfiable and routes +// events through the duck-typed sink the way a real backend does. #define USE_BLE_GATT_CLIENT #include "esphome/components/ble_device_base/ble_gatt_client.h" @@ -8,57 +11,57 @@ namespace esphome::ble_device_base::testing { -class RecordingListener : public GattClientEventListener { - public: - void on_connection_state(bool connected, uint16_t mtu, int error) override { this->connected_ = connected; } - void on_service_discovery_done(int error) override { this->discovery_error_ = error; } - void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) override {} - void on_write_result(uint16_t handle, int error) override {} - void on_notify_state(uint16_t handle, bool enabled, int error) override {} - void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) override {} +struct RecordingSink { + void on_connection_state(bool connected, uint16_t mtu, int error) { this->connected_ = connected; } + void on_service_discovery_done(int error) { this->discovery_error_ = error; } + void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {} + void on_write_result(uint16_t handle, int error) {} + void on_notify_state(uint16_t handle, bool enabled, int error) {} + void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) {} + void on_pairing_result(int status) {} bool connected_{false}; int discovery_error_{0}; }; -class MinimalConnection : public BLEGattConnection { +static_assert(GattClientEventSinkContract, "the recording sink must cover the full event-sink surface"); + +class MinimalConnection { public: - int connect(uint64_t address, uint8_t addr_type) override { + void set_listener(RecordingSink *listener) { this->listener_ = listener; } + + int connect(uint64_t address, uint8_t addr_type) { if (this->listener_ != nullptr) this->listener_->on_connection_state(true, 517, 0); return 0; } - int disconnect() override { return 0; } - int discover_services() override { + int disconnect() { return 0; } + int discover_services() { if (this->listener_ != nullptr) this->listener_->on_service_discovery_done(0); return 0; } - int read_characteristic(uint16_t handle) override { return GATT_ERR_NOT_CONNECTED; } - int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) override { return 0; } - int read_descriptor(uint16_t handle) override { return 0; } - int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) override { return 0; } - int notify_characteristic(uint16_t handle, bool enable) override { return 0; } - int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, - uint16_t timeout) override { + int read_characteristic(uint16_t handle) { return GATT_ERR_NOT_CONNECTED; } + int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) { return 0; } + int read_descriptor(uint16_t handle) { return 0; } + int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) { return 0; } + int notify_characteristic(uint16_t handle, bool enable) { return 0; } + int pair() { return GATT_ERR_NOT_CONNECTED; } + int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) { return 0; } - GattServiceTable get_service_table() override { return {}; } - void release_services() override {} + GattServiceTable get_service_table() { return {}; } + void release_services() {} + + protected: + RecordingSink *listener_{nullptr}; }; -TEST(BleGattClientContract, PairingDefaultsAreSafeForNonPairingBackends) { - // pair() defaults to not-connected and on_pairing_result() to a no-op, so - // a backend without pairing still answers the client through the dispatch. - MinimalConnection conn; - RecordingListener listener; - conn.set_listener(&listener); - EXPECT_EQ(conn.pair(), GATT_ERR_NOT_CONNECTED); - listener.on_pairing_result(0); // must not crash: default body -} +static_assert(BLEGattConnectionContract, + "a minimal backend must satisfy the contract the alias asserts"); TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) { MinimalConnection connection; - RecordingListener listener; + RecordingSink listener; connection.set_listener(&listener); EXPECT_EQ(connection.connect(0xAABBCCDDEEFFULL, 0), 0); EXPECT_TRUE(listener.connected_); diff --git a/tests/components/bluetooth_connection/__init__.py b/tests/components/bluetooth_connection/__init__.py index eae98931ec..eb6e174c0c 100644 --- a/tests/components/bluetooth_connection/__init__.py +++ b/tests/components/bluetooth_connection/__init__.py @@ -9,6 +9,7 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: # These defines are global to the merged host test binary; safe # because no co-compiled test observes them. cg.add_define("USE_BLE_GATT_CLIENT") + cg.add_define("USE_BLE_GATT_CLIENT_STUB_BACKEND") cg.add_define("USE_BLUETOOTH_PROXY") cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 1)