[ble_device_base] Add platform-neutral GATT client contract (#18128)

This commit is contained in:
J. Nick Koston
2026-08-06 21:45:57 -05:00
committed by GitHub
parent e58ba59288
commit 634515ecc5
17 changed files with 450 additions and 64 deletions
@@ -150,6 +150,21 @@ def request_irk_support() -> None:
cg.add_define("USE_BLE_DEVICE_IRK")
# Number of GATT client connection slots in this build; sizes the platform
# backend's connection storage.
GATT_CLIENT_COUNT_DEFINE = "ESPHOME_BLE_GATT_CLIENT_COUNT"
_request_gatt_connection_slot = cg.slot_counter(GATT_CLIENT_COUNT_DEFINE)
def request_gatt_client() -> None:
"""Compile in the neutral GATT client contract (ble_gatt_client.h) and
claim one connection slot. Called by bluetooth_proxy once per connection
it instantiates on a hub platform."""
cg.add_define("USE_BLE_GATT_CLIENT")
_request_gatt_connection_slot()
_request_listener_slot = cg.slot_counter(LISTENER_COUNT_DEFINE)
@@ -0,0 +1,26 @@
#include "ble_client_state.h"
namespace esphome::ble_device_base {
const char *client_state_to_string(ClientState state) {
switch (state) {
case ClientState::INIT:
return "INIT";
case ClientState::DISCONNECTING:
return "DISCONNECTING";
case ClientState::IDLE:
return "IDLE";
case ClientState::DISCOVERED:
return "DISCOVERED";
case ClientState::CONNECTING:
return "CONNECTING";
case ClientState::CONNECTED:
return "CONNECTED";
case ClientState::ESTABLISHED:
return "ESTABLISHED";
default:
return "UNKNOWN";
}
}
} // namespace esphome::ble_device_base
@@ -0,0 +1,53 @@
// ble_client_state.h
//
// Platform-neutral GATT client connection state types, shared by every
// platform's GATT client implementation (esp32_ble_client, bluetooth_connection
// backends). Moved here from esp32_ble_tracker, which re-exports them under its
// own namespace for backward compatibility.
#pragma once
#include <cstdint>
namespace esphome::ble_device_base {
/// ESPHome-private errors for the API's plain-int error fields, outside the
/// ATT code range so they cannot be mistaken for spec errors. -1 is
/// understood by API clients as "not connected". Shared by every GATT
/// client backend.
static constexpr int GATT_ERR_NOT_CONNECTED = -1;
static constexpr int GATT_ERR_NO_MEMORY = -2;
enum class ClientState : uint8_t {
// Connection is allocated
INIT,
// Client is disconnecting
DISCONNECTING,
// Connection is idle, no device detected.
IDLE,
// Device advertisement found.
DISCOVERED,
// Connection in progress.
CONNECTING,
// Initial connection established.
CONNECTED,
// The client and sub-clients have completed setup.
ESTABLISHED,
};
// Helper function to convert ClientState to string
const char *client_state_to_string(ClientState state);
enum class ConnectionType : uint8_t {
// The default connection type, we hold all the services in ram
// for the duration of the connection.
V1,
// The client has a cache of the services and mtu so we should not
// fetch them again
V3_WITH_CACHE,
// The client does not need the services and mtu once we send them
// so we should wipe them from memory as soon as we send them
V3_WITHOUT_CACHE
};
} // namespace esphome::ble_device_base
@@ -349,7 +349,8 @@ void ESPBTDevice::from_scan_result(const uint8_t *mac, int rssi, uint8_t addr_ty
this->address_[i] = mac[5 - i];
this->address_type_ = addr_type;
this->rssi_ = rssi;
this->name_.clear();
this->name_len_ = 0;
this->name_[0] = '\0';
this->service_uuids_.clear();
this->manufacturer_datas_.clear();
this->service_datas_.clear();
@@ -365,7 +366,7 @@ void ESPBTDevice::from_scan_result(const uint8_t *mac, int rssi, uint8_t addr_ty
" Address: %s (%s)\n"
" RSSI: %d\n"
" Name: '%s'",
this->address_str_to(addr_buf), this->address_type_str(), this->rssi_, this->name_.c_str());
this->address_str_to(addr_buf), this->address_type_str(), this->rssi_, this->name_);
for (auto &it : this->tx_powers_) {
ESP_LOGVV(TAG, " TX Power: %d", it);
}
@@ -477,8 +478,12 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint16_t len) {
// Keep the longest name seen — a merged adv + scan-response frame may carry both the
// shortened and the complete name, and the shortened form must never replace the
// complete one (same rule as esp32_ble_tracker's parse_adv_).
if (ad_data_len > this->name_.length())
this->name_.assign(reinterpret_cast<const char *>(ad_data), ad_data_len);
if (ad_data_len > this->name_len_) {
uint8_t name_len = ad_data_len > MAX_ADV_NAME_LEN ? MAX_ADV_NAME_LEN : static_cast<uint8_t>(ad_data_len);
memcpy(this->name_, ad_data, name_len);
this->name_[name_len] = '\0';
this->name_len_ = name_len;
}
break;
case 0x0A: // TX Power Level
@@ -14,6 +14,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
#include <cstdint>
#include <cstring>
@@ -166,6 +167,13 @@ inline uint64_t mac_lsb_first_to_uint64(const uint8_t *mac) {
return addr;
}
/// Unpack a uint64 BLE address into printable (MSB-first) byte order —
/// the order bd_addr_t / esp_bd_addr_t style APIs expect.
inline void uint64_to_mac_msb_first(uint64_t address, uint8_t out[6]) {
for (int i = 0; i < 6; i++)
out[i] = (address >> ((5 - i) * 8)) & 0xFF;
}
// ---------------------------------------------------------------------------
// ESPBTDevice — parsed BLE advertisement
// ---------------------------------------------------------------------------
@@ -211,7 +219,9 @@ class ESPBTDevice {
const char *address_type_str() const;
int get_rssi() const { return rssi_; }
const std::string &get_name() const { return name_; }
/// Advertised name as a view into the fixed buffer (always NUL-terminated,
/// so c_str() is safe); converts implicitly to std::string where needed.
StringRef get_name() const { return StringRef(this->name_, this->name_len_); }
const std::vector<ESPBTUUID> &get_service_uuids() const { return service_uuids_; }
const std::vector<ServiceData> &get_manufacturer_datas() const { return manufacturer_datas_; }
@@ -230,10 +240,17 @@ class ESPBTDevice {
protected:
void parse_adv_(const uint8_t *payload, uint16_t len);
// Max name bytes in a legacy advertisement AD element (31-byte PDU minus
// the 2-byte element header); every in-tree tracker scans legacy PDUs only.
static constexpr uint8_t MAX_ADV_NAME_LEN = 29;
uint8_t address_[6]{0};
uint8_t address_type_{0};
int rssi_{0};
std::string name_{};
// Fixed buffer instead of std::string: no per-advertisement heap churn on
// the scan path, and no libstdc++ string/exception machinery in the image.
char name_[MAX_ADV_NAME_LEN + 1]{};
uint8_t name_len_{0};
std::vector<ESPBTUUID> service_uuids_{};
std::vector<ServiceData> manufacturer_datas_{};
std::vector<ServiceData> service_datas_{};
@@ -0,0 +1,140 @@
// ble_gatt_client.h
//
// 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.
//
// Error domain (plain int, forwarded to the API without translation):
// 0 success
// 1..0x11 ATT error codes (Bluetooth spec; BTstack and Bluedroid agree)
// GATT_ERR_NOT_CONNECTED (-1) no connection to the peer (on esp32 a raw
// ESP_FAIL from the stack shares this value; both read as a
// failed, unusable connection on the client side)
// GATT_ERR_NO_MEMORY (-2) backend storage exhausted
// anything else: platform stack error/status code, surfaced opaquely.
// Connection events carry HCI status/disconnect reason codes (same code
// space on every controller).
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLE_GATT_CLIENT
#include "ble_client_state.h"
#include "ble_device.h"
#include <cstdint>
namespace esphome::ble_device_base {
// Materialized GATT database of a connected peer, discovered by the backend
// and streamed to the API by the consumer. Flat arrays with index ranges
// (not pointers): a service owns characteristics
// [first_characteristic, first_characteristic + characteristic_count) and a
// characteristic owns descriptors [first_descriptor, ...) — discovery is
// depth-first, so the ranges are naturally contiguous.
struct GattDescriptor {
ESPBTUUID uuid;
uint16_t handle;
};
struct GattCharacteristic {
ESPBTUUID uuid;
uint16_t value_handle;
// Needed to rebuild the stack's characteristic object for CCCD operations.
uint16_t end_handle;
uint8_t properties; // Bluetooth spec property bitfield
uint16_t first_descriptor;
uint16_t descriptor_count;
};
struct GattService {
ESPBTUUID uuid;
uint16_t start_handle;
uint16_t end_handle;
uint16_t first_characteristic;
uint16_t characteristic_count;
};
/// Borrowed view of the backend-owned service table. Valid from a successful
/// on_service_discovery_done() until release_services(). Characteristics and
/// descriptors are reached through the per-service/per-characteristic index
/// ranges; the array totals let a consumer bounds-check those ranges instead
/// of trusting the backend's discovery bookkeeping blindly.
struct GattServiceTable {
const GattService *services{nullptr};
const GattCharacteristic *characteristics{nullptr};
const GattDescriptor *descriptors{nullptr};
uint16_t service_count{0};
uint16_t characteristic_count{0};
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;
};
/// 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;
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.
virtual void release_services() = 0;
protected:
GattClientEventListener *listener_{nullptr};
};
} // namespace esphome::ble_device_base
#endif // USE_BLE_GATT_CLIENT
+3 -2
View File
@@ -56,8 +56,9 @@ struct HubCapabilities {
/// frame. When false, consumers relying on scan-response fields (e.g. names)
/// may only see them where the receiver merges per address (Home Assistant does).
bool merges_scan_response;
/// GATT client connections are available (today: esp32 only, but a chip SDK
/// gaining GATT support only has to flip this bit).
/// GATT client connections are available: the platform has a
/// bluetooth_connection backend implementing ble_device_base::BLEGattConnection
/// (ble_gatt_client.h). Today: esp32; rp2 follows with its BTstack backend.
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
+1 -1
View File
@@ -23,7 +23,7 @@ class BLEScanner final : public text_sensor::TextSensor,
// Escape special characters in the device name for valid JSON. Control characters stay in the \u00XX form this
// sensor has always published.
char escaped_name[128];
json_escape_into_buffer(escaped_name, StringRef(device.get_name()), /*short_control_escapes=*/false);
json_escape_into_buffer(escaped_name, device.get_name(), /*short_control_escapes=*/false);
char buf[256];
snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}",
@@ -13,6 +13,8 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/components/ble_device_base/ble_client_state.h"
#ifdef USE_ESP32
#include "esphome/components/esp32_ble_client/ble_client_base.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
@@ -41,7 +43,7 @@ using proxy_err_t = int;
static constexpr proxy_err_t PROXY_OK = 0;
#endif
static constexpr proxy_err_t ESP_GATT_NOT_CONNECTED = -1;
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;
@@ -36,27 +36,6 @@ static const char *const TAG = "esp32_ble_tracker";
ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
const char *client_state_to_string(ClientState state) {
switch (state) {
case ClientState::INIT:
return "INIT";
case ClientState::DISCONNECTING:
return "DISCONNECTING";
case ClientState::IDLE:
return "IDLE";
case ClientState::DISCOVERED:
return "DISCOVERED";
case ClientState::CONNECTING:
return "CONNECTING";
case ClientState::CONNECTED:
return "CONNECTED";
case ClientState::ESTABLISHED:
return "ESTABLISHED";
default:
return "UNKNOWN";
}
}
float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void ESP32BLETracker::setup() {
@@ -18,6 +18,7 @@
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include "esphome/components/ble_device_base/ble_client_state.h"
#include "esphome/components/ble_device_base/ble_device.h"
#include "esphome/components/ble_device_base/ble_hub.h"
#include "esphome/components/esp32_ble/ble.h"
@@ -88,22 +89,11 @@ struct ClientStateCounts {
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
};
enum class ClientState : uint8_t {
// Connection is allocated
INIT,
// Client is disconnecting
DISCONNECTING,
// Connection is idle, no device detected.
IDLE,
// Device advertisement found.
DISCOVERED,
// Connection in progress.
CONNECTING,
// Initial connection established.
CONNECTED,
// The client and sub-clients have completed setup.
ESTABLISHED,
};
// The client connection state types are owned by the platform-neutral
// ble_device_base layer; re-exported here for backward compatibility.
using ClientState = ble_device_base::ClientState;
using ConnectionType = ble_device_base::ConnectionType;
using ble_device_base::client_state_to_string;
enum class ScannerState {
// Scanner is idle, init state
@@ -128,21 +118,6 @@ class BLEScannerStateListener {
virtual void on_scanner_state(ScannerState state) = 0;
};
// Helper function to convert ClientState to string
const char *client_state_to_string(ClientState state);
enum class ConnectionType : uint8_t {
// The default connection type, we hold all the services in ram
// for the duration of the connection.
V1,
// The client has a cache of the services and mtu so we should not
// fetch them again
V3_WITH_CACHE,
// The client does not need the services and mtu once we send them
// so we should wipe them from memory as soon as we send them
V3_WITHOUT_CACHE
};
/// Base class for BLE GATT clients that connect to remote devices.
///
/// State Change Tracking Design:
@@ -92,7 +92,7 @@ bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
return success;
}
void ThermoProBLE::update_device_type_(const std::string &device_name) {
void ThermoProBLE::update_device_type_(StringRef device_name) {
// check for changed device name (should only happen on initial call)
if (this->device_name_ == device_name) {
return;
@@ -41,7 +41,7 @@ class ThermoProBLE final : public Component, public esp32_ble_tracker::ESPBTDevi
sensor::Sensor *humidity_{nullptr};
sensor::Sensor *battery_level_{nullptr};
void update_device_type_(const std::string &device_name);
void update_device_type_(StringRef device_name);
};
} // namespace esphome::thermopro_ble
+2
View File
@@ -464,6 +464,8 @@
#define USE_RP2040_BLE
#define RP2040_BLE_SCAN_LISTENER_COUNT 1
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
#define USE_BLE_GATT_CLIENT
#define ESPHOME_BLE_GATT_CLIENT_COUNT 1
#define USE_RP2040_VARIANT_RP2040
#define USE_SPI
#ifndef USE_ETHERNET
@@ -50,4 +50,18 @@ TEST(BleDeviceAddress, MacLsbFirstToUint64AgreesWithParsedDevice) {
EXPECT_EQ(mac_lsb_first_to_uint64(MAC_LSB_FIRST), device.address_uint64());
}
// uint64_to_mac_msb_first() is the inverse: unpacking the wire value yields
// printable (MSB-first) order, and round-tripping through the LSB-first
// packer restores the original value.
TEST(BleDeviceAddress, Uint64ToMacMsbFirstRoundTrip) {
uint8_t msb_first[6];
uint64_to_mac_msb_first(0xAABBCCDDEEFFULL, msb_first);
EXPECT_EQ(msb_first[0], 0xaa);
EXPECT_EQ(msb_first[5], 0xff);
uint8_t lsb_first[6];
for (int i = 0; i < 6; i++)
lsb_first[i] = msb_first[5 - i];
EXPECT_EQ(mac_lsb_first_to_uint64(lsb_first), 0xAABBCCDDEEFFULL);
}
} // namespace esphome::ble_device_base::testing
@@ -0,0 +1,91 @@
#include "esphome/components/ble_device_base/ble_device.h"
#include <gtest/gtest.h>
#include <cstring>
#include <string>
#include <vector>
namespace esphome::ble_device_base {
namespace {
// AD types under test
constexpr uint8_t AD_SHORT_NAME = 0x08;
constexpr uint8_t AD_COMPLETE_NAME = 0x09;
void append_name(std::vector<uint8_t> &adv, uint8_t ad_type, const char *name) {
size_t len = strlen(name);
adv.push_back(static_cast<uint8_t>(len + 1));
adv.push_back(ad_type);
adv.insert(adv.end(), name, name + len);
}
ESPBTDevice device_from(const std::vector<uint8_t> &adv) {
const uint8_t mac[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
ESPBTDevice device;
device.from_scan_result(mac, -59, 0, adv.data(), static_cast<uint16_t>(adv.size()));
return device;
}
} // namespace
TEST(BleAdvName, ParsesACompleteName) {
std::vector<uint8_t> adv;
append_name(adv, AD_COMPLETE_NAME, "TP96");
ESPBTDevice device = device_from(adv);
EXPECT_EQ(device.get_name(), "TP96");
// The backing buffer is NUL-terminated so c_str() is usable directly.
EXPECT_STREQ(device.get_name().c_str(), "TP96");
}
TEST(BleAdvName, LongestNameWinsShortenedThenComplete) {
// A merged adv + scan-response frame can carry both forms; the shortened
// one must never replace the complete one.
std::vector<uint8_t> adv;
append_name(adv, AD_SHORT_NAME, "Radon");
append_name(adv, AD_COMPLETE_NAME, "RadonEye");
EXPECT_EQ(device_from(adv).get_name(), "RadonEye");
}
TEST(BleAdvName, LongestNameWinsCompleteThenShortened) {
std::vector<uint8_t> adv;
append_name(adv, AD_COMPLETE_NAME, "RadonEye");
append_name(adv, AD_SHORT_NAME, "Radon");
EXPECT_EQ(device_from(adv).get_name(), "RadonEye");
}
TEST(BleAdvName, MaxLengthNameFitsAndTerminates) {
// 29 bytes is the largest name a legacy AD element can carry and exactly
// fills the fixed buffer.
std::string max_name(29, 'a');
std::vector<uint8_t> adv;
append_name(adv, AD_COMPLETE_NAME, max_name.c_str());
ESPBTDevice device = device_from(adv);
EXPECT_EQ(device.get_name().size(), 29u);
EXPECT_EQ(device.get_name(), max_name);
EXPECT_STREQ(device.get_name().c_str(), max_name.c_str());
}
TEST(BleAdvName, ReparseResetsThePreviousName) {
const uint8_t mac[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
std::vector<uint8_t> first;
append_name(first, AD_COMPLETE_NAME, "RadonEye");
std::vector<uint8_t> second;
append_name(second, AD_COMPLETE_NAME, "TP96");
ESPBTDevice device;
device.from_scan_result(mac, -59, 0, first.data(), static_cast<uint16_t>(first.size()));
ASSERT_EQ(device.get_name(), "RadonEye");
// A shorter name from a fresh report must fully replace the longer one:
// the longest-name rule applies within one report, not across reports.
device.from_scan_result(mac, -59, 0, second.data(), static_cast<uint16_t>(second.size()));
EXPECT_EQ(device.get_name(), "TP96");
EXPECT_STREQ(device.get_name().c_str(), "TP96");
}
TEST(BleAdvName, NoNamePresentIsEmpty) {
std::vector<uint8_t> adv = {0x02, 0x0A, 0x00}; // TX power only
EXPECT_TRUE(device_from(adv).get_name().empty());
}
} // namespace esphome::ble_device_base
@@ -0,0 +1,66 @@
// 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.
#define USE_BLE_GATT_CLIENT
#include "esphome/components/ble_device_base/ble_gatt_client.h"
#include <gtest/gtest.h>
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 {}
bool connected_{false};
int discovery_error_{0};
};
class MinimalConnection : public BLEGattConnection {
public:
int connect(uint64_t address, uint8_t addr_type) override {
if (this->listener_ != nullptr)
this->listener_->on_connection_state(true, 517, 0);
return 0;
}
int disconnect() override { return 0; }
int discover_services() override {
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 {
return 0;
}
GattServiceTable get_service_table() override { return {}; }
void release_services() override {}
};
TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) {
MinimalConnection connection;
RecordingListener listener;
connection.set_listener(&listener);
EXPECT_EQ(connection.connect(0xAABBCCDDEEFFULL, 0), 0);
EXPECT_TRUE(listener.connected_);
EXPECT_EQ(connection.discover_services(), 0);
EXPECT_EQ(listener.discovery_error_, 0);
EXPECT_EQ(connection.read_characteristic(1), GATT_ERR_NOT_CONNECTED);
// A default table is empty and safe to walk.
GattServiceTable table = connection.get_service_table();
EXPECT_EQ(table.service_count, 0);
EXPECT_EQ(table.characteristic_count, 0);
EXPECT_EQ(table.descriptor_count, 0);
}
} // namespace esphome::ble_device_base::testing