[ble_device_base] Bind the GATT backend at compile time (#18185)

This commit is contained in:
J. Nick Koston
2026-08-08 15:59:32 -05:00
committed by GitHub
parent 7218aa4803
commit 04384e0f5b
9 changed files with 199 additions and 137 deletions
@@ -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<RecordingSink>, "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<MinimalConnection, RecordingSink>,
"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_);
@@ -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)