mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
Compare commits
112
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e67cd5b978 | ||
|
|
06a52f682f | ||
|
|
7e89d962f4 | ||
|
|
409ff027cc | ||
|
|
4d90dbda97 | ||
|
|
3c7e5000a5 | ||
|
|
7af211ab54 | ||
|
|
5420b0106a | ||
|
|
964835c523 | ||
|
|
cc81069d02 | ||
|
|
20f63026a2 | ||
|
|
ba567cecc3 | ||
|
|
ee7fb83334 | ||
|
|
9db31db24d | ||
|
|
ceca66c01a | ||
|
|
dc97c5a161 | ||
|
|
97bbcf55d9 | ||
|
|
d135ac97e5 | ||
|
|
296dd19361 | ||
|
|
138304cfbe | ||
|
|
9fef1862be | ||
|
|
80758f11aa | ||
|
|
9d8362738f | ||
|
|
b2c22b5802 | ||
|
|
f62009ecc9 | ||
|
|
c09e3e0d45 | ||
|
|
cdd7bef74a | ||
|
|
0e1321a813 | ||
|
|
022b612366 | ||
|
|
2dc9fbec5e | ||
|
|
9dafff6a61 | ||
|
|
3c7a1062c8 | ||
|
|
8164cdd018 | ||
|
|
4ea491d46f | ||
|
|
7fdb6cd2d5 | ||
|
|
2d50e07e90 | ||
|
|
78a451a1c0 | ||
|
|
2483621500 | ||
|
|
4f086b6b25 | ||
|
|
c4850a9320 | ||
|
|
91bcb14c09 | ||
|
|
85e4a6cc33 | ||
|
|
149d7cd801 | ||
|
|
ce2e113a6d | ||
|
|
ae5b6d271f | ||
|
|
e1572d7520 | ||
|
|
536afce394 | ||
|
|
152b6a5d50 | ||
|
|
f68ebcf7d1 | ||
|
|
ca5fa6dfaf | ||
|
|
962a82d157 | ||
|
|
c6d4ca7377 | ||
|
|
465a6a3596 | ||
|
|
03c9b001f4 | ||
|
|
d680200e5a | ||
|
|
98dae49440 | ||
|
|
7ec366c1ba | ||
|
|
b1e1b0d7c5 | ||
|
|
ec49eb12dd | ||
|
|
949474efc6 | ||
|
|
708f095b6f | ||
|
|
bf4af703b6 | ||
|
|
541616e19c | ||
|
|
952c3e6584 | ||
|
|
2ea55bee7b | ||
|
|
1931ef32ee | ||
|
|
f2da6c1a56 | ||
|
|
6e9e83bdfd | ||
|
|
fe29a53b71 | ||
|
|
fa2dafcbca | ||
|
|
dc18fced19 | ||
|
|
389d3629c2 | ||
|
|
36a4d87611 | ||
|
|
65db614f22 | ||
|
|
591856e7dc | ||
|
|
b680482092 | ||
|
|
7fe447a4a2 | ||
|
|
8fae4f6767 | ||
|
|
3d458bd9c8 | ||
|
|
fd7510d6e5 | ||
|
|
ffe55f7691 | ||
|
|
40ac344bf2 | ||
|
|
667ffe98d9 | ||
|
|
270f776675 | ||
|
|
965bbd4425 | ||
|
|
e485e5af90 | ||
|
|
c795d0734d | ||
|
|
0e77d47df4 | ||
|
|
47c2f8f31a | ||
|
|
fe53d24e46 | ||
|
|
daf988fde8 | ||
|
|
6604a9cca7 | ||
|
|
ddd45e393b | ||
|
|
0974e8a1f5 | ||
|
|
1478307280 | ||
|
|
fc77cdcb7d | ||
|
|
fcb9da489e | ||
|
|
ab458fffab | ||
|
|
c0c3afe6b1 | ||
|
|
b092f64744 | ||
|
|
6b2938c1f7 | ||
|
|
0e5d902c95 | ||
|
|
11fc8e4c07 | ||
|
|
d9f0f40373 | ||
|
|
e671a602e9 | ||
|
|
7f44b9b826 | ||
|
|
47804f8d08 | ||
|
|
67cc43c8ef | ||
|
|
3e9808162b | ||
|
|
a6f81f3185 | ||
|
|
5296539849 | ||
|
|
de9074fb5e |
@@ -144,6 +144,9 @@ async def stop_scan_action_to_code(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# Selects the BLEHub alias arm in ble_device_base/ble_hub_impl.h.
|
||||
cg.add_define("USE_BK72XX_BLE_TRACKER")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ namespace esphome::bk72xx_ble_tracker {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class BK72xxBLETracker : public Component,
|
||||
public ble_device_base::BLEHub,
|
||||
public bk72xx_ble::BLEScanListener,
|
||||
public Parented<bk72xx_ble::BK72xxBLE>
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
@@ -93,15 +92,15 @@ class BK72xxBLETracker : public Component,
|
||||
void stop_scan();
|
||||
|
||||
// ---- ble_device_base::BLEHub contract ----
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) override {
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) {
|
||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||
this->listeners_.push_back(listener);
|
||||
#endif
|
||||
}
|
||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override {
|
||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) {
|
||||
this->raw_advertisement_callback_ = callback;
|
||||
}
|
||||
ble_device_base::HubCapabilities get_capabilities() const override {
|
||||
static constexpr ble_device_base::HubCapabilities get_capabilities() {
|
||||
// The Beken BDK exposes no active-scan path (passive scanning only), so the
|
||||
// controller never solicits scan responses and never merges them; consumers
|
||||
// relying on scan-response fields (device names) get them only where the
|
||||
@@ -110,21 +109,21 @@ class BK72xxBLETracker : public Component,
|
||||
// path there is no mode to switch to.
|
||||
return {.active_scan = false, .merges_scan_response = false, .gatt = false, .scan_mode_switch = false};
|
||||
}
|
||||
bool request_scan_mode(bool active) override {
|
||||
bool request_scan_mode(bool active) {
|
||||
// Passive-only controller: a passive request is already honored, an active
|
||||
// one cannot be.
|
||||
return !active;
|
||||
}
|
||||
// The controller stores the address LSB-first (BLE convention); the contract
|
||||
// wants printable (MSB-first) order.
|
||||
void get_adapter_mac(uint8_t out[6]) override {
|
||||
void get_adapter_mac(uint8_t out[6]) {
|
||||
uint8_t mac[6];
|
||||
this->parent_->get_mac_lsb_first(mac);
|
||||
for (int i = 0; i < 6; i++)
|
||||
out[i] = mac[5 - i];
|
||||
}
|
||||
bool scan_running() override { return this->scan_running_; }
|
||||
bool scan_active() override { return false; } // BK72xx scan is passive-only
|
||||
bool scan_running() { return this->scan_running_; }
|
||||
bool scan_active() { return false; } // BK72xx scan is passive-only
|
||||
|
||||
// ---- bk72xx_ble::BLEScanListener ----
|
||||
// Delivered by the controller's loop() on the ESPHome main task — the
|
||||
|
||||
@@ -3,19 +3,22 @@ ble_device_base — the platform-neutral BLE layer.
|
||||
|
||||
Owns the shared advertisement types (ESPBTUUID / ESPBTDevice / ServiceData /
|
||||
ESPBLEiBeacon / ESPBTDeviceListener, in ble_device.h) and the tracker contract
|
||||
(BLEHub, in ble_hub.h) on every platform.
|
||||
(BLEHub, in ble_hub.h; C++-side a per-platform alias bound in ble_hub_impl.h)
|
||||
on every platform.
|
||||
|
||||
BLE consumers (sensor components, bluetooth_proxy) bind to whichever tracker the
|
||||
configuration declares via `cv.use_id(BLEHub)` — ESPHome resolves any declared
|
||||
subclass, so there is no platform table here and no dependency in either
|
||||
direction. A sensor extends BLE_DEVICE_SCHEMA in its CONFIG_SCHEMA (so an
|
||||
explicit ble_hub_id: is a declared key even on strict schemas) and calls
|
||||
register_ble_device() in to_code; a tracker component subclasses BLEHub (C++
|
||||
and codegen class) and MUST call register_hub_provider() at import time —
|
||||
without it _require_hub rejects configs that bind through the generated id
|
||||
(an explicit ble_hub_id: bypasses the registry). Adding a new BLE chip
|
||||
requires only a new in-tree tracker component; out-of-tree BLE hubs are
|
||||
not supported.
|
||||
subclass, so there is no Python platform table here and no dependency in
|
||||
either direction (C++-side, the compile-time alias header ble_hub_impl.h and the
|
||||
defines.h mirror are the deliberate exceptions). A sensor extends
|
||||
BLE_DEVICE_SCHEMA in its CONFIG_SCHEMA (so an explicit ble_hub_id: is a
|
||||
declared key even on strict schemas) and calls register_ble_device() in
|
||||
to_code; a tracker component declares BLEHub as its codegen-class parent and
|
||||
MUST call register_hub_provider() at import time — without it _require_hub
|
||||
rejects configs that bind through the generated id (an explicit ble_hub_id:
|
||||
bypasses the registry). Adding a new BLE chip requires a new in-tree tracker
|
||||
component plus its alias arm and define (see above); out-of-tree BLE hubs
|
||||
are not supported.
|
||||
|
||||
AES-CCM decryption for encrypted advertisements is provided portably in
|
||||
ble_aes_ccm.h.
|
||||
@@ -48,8 +51,9 @@ LISTENER_COUNT_DEFINE = "ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT"
|
||||
|
||||
ble_device_base_ns = cg.esphome_ns.namespace("ble_device_base")
|
||||
|
||||
# The neutral tracker contract. Every tracker's codegen class declares this as a
|
||||
# parent, which is what lets cv.use_id(BLEHub) resolve any of them.
|
||||
# The neutral tracker contract. Every tracker's codegen class declares this as
|
||||
# a parent, which is what lets cv.use_id(BLEHub) resolve any of them. Python
|
||||
# only: C++-side the name is a per-platform alias (ble_hub_impl.h).
|
||||
BLEHub = ble_device_base_ns.class_("BLEHub")
|
||||
|
||||
# The neutral listener base (C++: ble_device_base::ESPBTDeviceListener).
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Platform-neutral BLE advertisement triggers: ESPBTDeviceListener subclasses
|
||||
// registered on a BLEHub, exposed by each tracker under its own automation
|
||||
// names. parse_device()'s return feeds the "Found device" suppression.
|
||||
// Constructors are templated on the hub type so this header also builds with
|
||||
// no tracker present (host unit tests).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ble_device.h"
|
||||
#include "ble_hub.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -18,7 +19,7 @@ namespace esphome::ble_device_base {
|
||||
// on_ble_advertise: fires on every BLE advertisement, optionally filtered to one or more MACs.
|
||||
class ESPBTAdvertiseTrigger final : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
|
||||
public:
|
||||
explicit ESPBTAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); }
|
||||
template<typename Hub> explicit ESPBTAdvertiseTrigger(Hub *parent) { parent->register_listener(this); }
|
||||
|
||||
void set_addresses(std::initializer_list<uint64_t> addresses) { this->addresses_ = addresses; }
|
||||
|
||||
@@ -39,7 +40,7 @@ class ESPBTAdvertiseTrigger final : public Trigger<const ESPBTDevice &>, public
|
||||
// data for the given UUID. Optional single-MAC filter.
|
||||
class BLEServiceDataAdvertiseTrigger final : public Trigger<const adv_data_t &>, public ESPBTDeviceListener {
|
||||
public:
|
||||
explicit BLEServiceDataAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); }
|
||||
template<typename Hub> explicit BLEServiceDataAdvertiseTrigger(Hub *parent) { parent->register_listener(this); }
|
||||
|
||||
void set_service_uuid16(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(static_cast<uint16_t>(uuid)); }
|
||||
void set_service_uuid32(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(static_cast<uint32_t>(uuid)); }
|
||||
@@ -73,7 +74,7 @@ class BLEServiceDataAdvertiseTrigger final : public Trigger<const adv_data_t &>,
|
||||
// manufacturer data for the given ID. Optional single-MAC filter.
|
||||
class BLEManufacturerDataAdvertiseTrigger final : public Trigger<const adv_data_t &>, public ESPBTDeviceListener {
|
||||
public:
|
||||
explicit BLEManufacturerDataAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); }
|
||||
template<typename Hub> explicit BLEManufacturerDataAdvertiseTrigger(Hub *parent) { parent->register_listener(this); }
|
||||
|
||||
void set_manufacturer_uuid16(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(static_cast<uint16_t>(uuid)); }
|
||||
void set_manufacturer_uuid32(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(static_cast<uint32_t>(uuid)); }
|
||||
@@ -108,7 +109,7 @@ class BLEManufacturerDataAdvertiseTrigger final : public Trigger<const adv_data_
|
||||
// claims devices (parse_device always returns false).
|
||||
class BLEEndOfScanTrigger final : public Trigger<>, public ESPBTDeviceListener {
|
||||
public:
|
||||
explicit BLEEndOfScanTrigger(BLEHub *parent) { parent->register_listener(this); }
|
||||
template<typename Hub> explicit BLEEndOfScanTrigger(Hub *parent) { parent->register_listener(this); }
|
||||
|
||||
bool parse_device(const ESPBTDevice &device) override { return false; }
|
||||
void on_scan_end() override { this->trigger(); }
|
||||
|
||||
@@ -18,6 +18,10 @@ namespace esphome::ble_device_base {
|
||||
static constexpr int GATT_ERR_NOT_CONNECTED = -1;
|
||||
static constexpr int GATT_ERR_NO_MEMORY = -2;
|
||||
|
||||
/// Safety net shared by every GATT backend: force IDLE when the stack never
|
||||
/// delivers its disconnect completion.
|
||||
static constexpr uint32_t GATT_DISCONNECT_TIMEOUT_MS = 10000;
|
||||
|
||||
// Preferred connection parameters shared by every platform's GATT client so
|
||||
// the backends cannot drift (units: interval 1.25 ms, timeout 10 ms; latency
|
||||
// 0). FAST covers connection setup and service discovery; MEDIUM is the
|
||||
|
||||
@@ -5,8 +5,12 @@
|
||||
// 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
|
||||
// A consumer - a streaming consumer that forwards the raw database (the hub
|
||||
// BluetoothConnection wrapper) or a direct consumer owning a dedicated
|
||||
// backend and resolving handles by UUID - drives it and receives
|
||||
// completions through the GattClientListener interface (one build can hold
|
||||
// several consumer types while the backend stays a single non-virtual
|
||||
// class). All listener
|
||||
// calls are delivered on the ESPHome main loop; borrowed data pointers are
|
||||
// valid only for the duration of the call.
|
||||
//
|
||||
@@ -78,9 +82,31 @@ struct GattServiceTable {
|
||||
uint16_t descriptor_count{0};
|
||||
};
|
||||
|
||||
/// The event surface a backend delivers completions through. Genuine runtime
|
||||
/// polymorphism lives here - one build can hold several consumer types (the
|
||||
/// proxy's connection wrapper, dedicated-backend components) against the one
|
||||
/// non-virtual backend class - so this is a plain virtual interface: every
|
||||
/// method defaults to a no-op, consumers override what they consume (override
|
||||
/// makes a misspelled name a compile error), and adding an event touches no
|
||||
/// existing consumer. No destructor: components are never destroyed, and
|
||||
/// nothing deletes through this base.
|
||||
/// on_connection_state carries the negotiated MTU and an HCI
|
||||
/// status/disconnect reason. Codegen wires the listener before setup(), so
|
||||
/// backends may call without a null check.
|
||||
class GattClientListener {
|
||||
public:
|
||||
virtual void on_connection_state(bool connected, uint16_t mtu, int error) {}
|
||||
virtual void on_service_discovery_done(int error) {}
|
||||
virtual void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {}
|
||||
virtual void on_write_result(uint16_t handle, int error) {}
|
||||
virtual void on_notify_state(uint16_t handle, bool enabled, int error) {}
|
||||
virtual void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) {}
|
||||
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
|
||||
// through the listener) 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).
|
||||
@@ -88,15 +114,19 @@ struct GattServiceTable {
|
||||
// - 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).
|
||||
// released after streaming (release is idempotent). A backend may
|
||||
// additionally provide its own service streamer (stream_service_batch on
|
||||
// the concrete type, detected by the consumer at compile time) for
|
||||
// arbitrary-size databases; the table then materializes only for consumers
|
||||
// that ask for it.
|
||||
// - 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<typename T, typename Sink>
|
||||
concept BLEGattConnectionContract = requires(T conn, Sink *sink, const uint8_t *data) {
|
||||
conn.set_listener(sink);
|
||||
template<typename T>
|
||||
concept BLEGattConnectionContract = requires(T conn, GattClientListener *listener, const uint8_t *data) {
|
||||
conn.set_listener(listener);
|
||||
{ conn.connect(uint64_t{}, uint8_t{}) } -> std::same_as<int>;
|
||||
{ conn.disconnect() } -> std::same_as<int>;
|
||||
{ conn.discover_services() } -> std::same_as<int>;
|
||||
@@ -109,23 +139,55 @@ concept BLEGattConnectionContract = requires(T conn, Sink *sink, const uint8_t *
|
||||
{ conn.update_connection_params(uint16_t{}, uint16_t{}, uint16_t{}, uint16_t{}) } -> std::same_as<int>;
|
||||
{ conn.get_service_table() } -> std::same_as<GattServiceTable>;
|
||||
{ conn.release_services() } -> std::same_as<void>;
|
||||
// Connection-type hint for backends that tune parameters by it; others
|
||||
// carry an inline no-op.
|
||||
{ conn.set_connection_type(ConnectionType{}) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
// 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<typename S>
|
||||
concept GattClientEventSinkContract = requires(S sink, const uint8_t *data) {
|
||||
{ sink.on_connection_state(true, uint16_t{}, int{}) } -> std::same_as<void>;
|
||||
{ sink.on_service_discovery_done(int{}) } -> std::same_as<void>;
|
||||
{ sink.on_read_result(uint16_t{}, data, uint16_t{}, int{}) } -> std::same_as<void>;
|
||||
{ sink.on_write_result(uint16_t{}, int{}) } -> std::same_as<void>;
|
||||
{ sink.on_notify_state(uint16_t{}, true, int{}) } -> std::same_as<void>;
|
||||
{ sink.on_notify_data(uint16_t{}, data, uint16_t{}) } -> std::same_as<void>;
|
||||
{ sink.on_pairing_result(int{}) } -> std::same_as<void>;
|
||||
};
|
||||
// ---- service table lookup helpers ----
|
||||
//
|
||||
// Neutral, bounds-checked walks over a materialized GattServiceTable for
|
||||
// direct consumers that resolve a known device's handles by UUID (streaming
|
||||
// consumers forward the raw database and never need these). Linear search:
|
||||
// the table exists only between discovery and release_services(), for one
|
||||
// small known device.
|
||||
|
||||
/// Client Characteristic Configuration descriptor UUID (Bluetooth spec).
|
||||
static constexpr uint16_t CCCD_UUID = 0x2902;
|
||||
|
||||
inline const GattService *find_service(const GattServiceTable &table, const ESPBTUUID &uuid) {
|
||||
for (uint16_t i = 0; i < table.service_count; i++) {
|
||||
if (table.services[i].uuid == uuid)
|
||||
return &table.services[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
inline const GattCharacteristic *find_characteristic(const GattServiceTable &table, const GattService &service,
|
||||
const ESPBTUUID &uuid) {
|
||||
uint16_t end = service.first_characteristic + service.characteristic_count;
|
||||
if (end > table.characteristic_count)
|
||||
return nullptr;
|
||||
for (uint16_t i = service.first_characteristic; i < end; i++) {
|
||||
if (table.characteristics[i].uuid == uuid)
|
||||
return &table.characteristics[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// Handle of the characteristic's Client Characteristic Configuration
|
||||
/// descriptor (0x2902), or 0 when it has none.
|
||||
inline uint16_t find_cccd(const GattServiceTable &table, const GattCharacteristic &characteristic) {
|
||||
uint16_t end = characteristic.first_descriptor + characteristic.descriptor_count;
|
||||
if (end > table.descriptor_count)
|
||||
return 0;
|
||||
const ESPBTUUID cccd_uuid = ESPBTUUID::from_uint16(CCCD_UUID);
|
||||
for (uint16_t i = characteristic.first_descriptor; i < end; i++) {
|
||||
if (table.descriptors[i].uuid == cccd_uuid)
|
||||
return table.descriptors[i].handle;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// ble_hub.h
|
||||
//
|
||||
// BLEHub — the platform-neutral BLE tracker contract.
|
||||
//
|
||||
// Every BLE tracker component (esp32_ble_tracker, bk72xx_ble_tracker,
|
||||
// ln882h_ble_tracker, future chips) implements this interface; every BLE
|
||||
// consumer (sensor components, bluetooth_proxy) binds to it — in YAML via
|
||||
// `cv.use_id(BLEHub)`, which resolves whichever tracker the config declares.
|
||||
// Adding a new BLE chip therefore requires only a new tracker component that
|
||||
// implements BLEHub: no consumer, registry, or base changes.
|
||||
// The platform-neutral BLE tracker contract: shared types plus the method
|
||||
// surface every tracker provides (documented below). Exactly one tracker
|
||||
// exists per build, so BLEHub is a compile-time alias (ble_hub_impl.h), not
|
||||
// an abstract interface — no vtable, every hub call inlinable. Consumers
|
||||
// include ble_hub_impl.h and bind in YAML via cv.use_id(BLEHub).
|
||||
//
|
||||
// Chip differences are expressed as data (HubCapabilities), never as
|
||||
// platform conditionals in consumers.
|
||||
@@ -15,7 +12,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "ble_device.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::ble_device_base {
|
||||
@@ -61,9 +60,8 @@ enum class ScannerState : uint8_t {
|
||||
};
|
||||
|
||||
/// Subscriber slot for scanner-state transitions; same shape as
|
||||
/// RawAdvertisementCallback, delivered on the ESPHome main loop. Hubs that
|
||||
/// cannot push drop the registration and the consumer falls back to polling
|
||||
/// scan_running().
|
||||
/// RawAdvertisementCallback, delivered on the ESPHome main loop. Only hubs
|
||||
/// that push provide the setter; consumers of the rest poll scan_running().
|
||||
struct ScannerStateCallback {
|
||||
void *instance{nullptr};
|
||||
void (*fn)(void *instance, ScannerState state){nullptr};
|
||||
@@ -91,48 +89,34 @@ struct HubCapabilities {
|
||||
bool scan_mode_switch;
|
||||
};
|
||||
|
||||
class BLEHub {
|
||||
public:
|
||||
virtual ~BLEHub() = default;
|
||||
|
||||
/// Register a parsed-advertisement consumer (BLE sensors, automation triggers).
|
||||
virtual void register_listener(ESPBTDeviceListener *listener) = 0;
|
||||
|
||||
/// Wire the raw-advertisement stream (bluetooth_proxy). One consumer at a time.
|
||||
virtual void set_raw_advertisement_callback(RawAdvertisementCallback callback) = 0;
|
||||
|
||||
// The BLEHub method surface, asserted where ble_hub_impl.h binds the alias.
|
||||
// Semantics beyond the signatures:
|
||||
// - register_listener: parsed-advertisement consumers (sensors, triggers).
|
||||
// - set_raw_advertisement_callback: raw stream, one consumer at a time.
|
||||
// - get_adapter_mac: printable order, out[0] = MSB.
|
||||
// - scan_active: the current/configured mode sends scan requests.
|
||||
// - request_scan_mode: false = cannot honor, state untouched (the caller
|
||||
// reports the real state back); true = applied immediately, restarting a
|
||||
// running scan. Honoring is advertised by HubCapabilities::scan_mode_switch.
|
||||
// Push hubs additionally provide set_scanner_state_callback(ScannerStateCallback)
|
||||
// and get_scanner_state() under USE_BLE_SCANNER_STATE_CALLBACK; the concept
|
||||
// requires both exactly when that define is set. A push hub must emit a
|
||||
// transition for every accepted or refused mode request - consumers skip
|
||||
// their own mode report on push builds.
|
||||
template<typename T>
|
||||
concept BLEHubContract = requires(T hub, ESPBTDeviceListener *listener, RawAdvertisementCallback raw_callback,
|
||||
uint8_t *mac) {
|
||||
hub.register_listener(listener);
|
||||
hub.set_raw_advertisement_callback(raw_callback);
|
||||
{ T::get_capabilities() } -> std::same_as<HubCapabilities>;
|
||||
hub.get_adapter_mac(mac);
|
||||
{ hub.scan_running() } -> std::same_as<bool>;
|
||||
{ hub.scan_active() } -> std::same_as<bool>;
|
||||
{ hub.request_scan_mode(true) } -> std::same_as<bool>;
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
/// Push subscriber for scanner-state transitions; hubs that can push
|
||||
/// invoke scanner_state_callback_ where their state changes. Compiled only
|
||||
/// when a subscriber exists (bluetooth_proxy emits the define), so
|
||||
/// subscriber-less builds carry no storage.
|
||||
void set_scanner_state_callback(ScannerStateCallback callback) { this->scanner_state_callback_ = callback; }
|
||||
|
||||
protected:
|
||||
ScannerStateCallback scanner_state_callback_{};
|
||||
|
||||
public:
|
||||
#endif // USE_BLE_SCANNER_STATE_CALLBACK
|
||||
|
||||
virtual HubCapabilities get_capabilities() const = 0;
|
||||
|
||||
/// Adapter MAC in printable (MSB-first) order, out[0] = MSB.
|
||||
virtual void get_adapter_mac(uint8_t out[6]) = 0;
|
||||
|
||||
virtual bool scan_running() = 0;
|
||||
/// True when the current/configured scan mode is active (scan requests sent).
|
||||
virtual bool scan_active() = 0;
|
||||
/// Request a scan-mode change (active = send scan requests). Returns false
|
||||
/// when the hub cannot honor the request; the caller reports the real state
|
||||
/// back to its subscriber. A hub that returns true applies the mode
|
||||
/// immediately: a running scan is restarted with the new mode, an idle one
|
||||
/// picks it up on its next start. The default cannot-change keeps hubs
|
||||
/// without a mode switch (and out-of-tree trackers) building unchanged.
|
||||
/// Independent of HubCapabilities::active_scan: that bit describes what the
|
||||
/// CONTROLLER can do; whether this method honors requests is advertised by
|
||||
/// HubCapabilities::scan_mode_switch, so consumers can gate features on the
|
||||
/// switch without probing.
|
||||
virtual bool request_scan_mode(bool active) { return false; }
|
||||
hub.set_scanner_state_callback(ScannerStateCallback{});
|
||||
{ hub.get_scanner_state() } -> std::same_as<ScannerState>;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_device_base
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// ble_hub_impl.h
|
||||
//
|
||||
// Binds ble_device_base::BLEHub to the build's one tracker; each tracker's
|
||||
// codegen emits its USE_*_BLE_TRACKER define. Consumers include this header,
|
||||
// trackers include ble_hub.h (the contract).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ble_hub.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32_BLE_TRACKER)
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#define ESPHOME_BLE_HUB_TYPE esp32_ble_tracker::ESP32BLETracker
|
||||
#elif defined(USE_RP2_BLE_TRACKER)
|
||||
#include "esphome/components/rp2_ble_tracker/rp2_ble_tracker.h"
|
||||
#define ESPHOME_BLE_HUB_TYPE rp2_ble_tracker::RP2BLETracker
|
||||
#elif defined(USE_BK72XX_BLE_TRACKER)
|
||||
#include "esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h"
|
||||
#define ESPHOME_BLE_HUB_TYPE bk72xx_ble_tracker::BK72xxBLETracker
|
||||
#elif defined(USE_LN882H_BLE_TRACKER)
|
||||
#include "esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h"
|
||||
#define ESPHOME_BLE_HUB_TYPE ln882h_ble_tracker::LN882HBLETracker
|
||||
#endif
|
||||
// No #else on purpose: builds without a tracker (host unit tests) get no BLEHub.
|
||||
|
||||
namespace esphome::ble_device_base {
|
||||
|
||||
#ifdef ESPHOME_BLE_HUB_TYPE
|
||||
using BLEHub = ESPHOME_BLE_HUB_TYPE;
|
||||
static_assert(BLEHubContract<BLEHub>, "The build's BLE tracker is missing part of the BLEHub surface (ble_hub.h)");
|
||||
#undef ESPHOME_BLE_HUB_TYPE
|
||||
#endif
|
||||
|
||||
} // namespace esphome::ble_device_base
|
||||
@@ -1,23 +1,41 @@
|
||||
"""Per-platform GATT connection backends the Bluetooth proxy drives.
|
||||
"""Per-platform GATT connection backends and the helpers to embed one.
|
||||
|
||||
Backends: esp32 Bluedroid, rp2 BTstack. Auto-loaded by bluetooth_proxy, no
|
||||
user-facing configuration; the proxy's codegen declares and registers the
|
||||
connection instances.
|
||||
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 direct consumers owning a dedicated backend through
|
||||
gatt_client_config_schema() + new_gatt_backend().
|
||||
"""
|
||||
|
||||
import functools
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
from esphome.const import PLATFORM_RP2, PlatformFramework
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_MAC_ADDRESS,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_RP2,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DOMAIN = "bluetooth_connection"
|
||||
|
||||
|
||||
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"]
|
||||
"""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"]
|
||||
|
||||
|
||||
@@ -33,25 +51,208 @@ RP2_MAX_CONNECTIONS = 1
|
||||
# registry of which hub platforms run the connection-capable proxy.
|
||||
HUB_MAX_CONNECTIONS: dict[str, int] = {PLATFORM_RP2: RP2_MAX_CONNECTIONS}
|
||||
|
||||
# The hub-platform wrapper and the rp2 BTstack backend codegen classes.
|
||||
# The hub-platform wrapper and the backend codegen classes.
|
||||
HubBluetoothConnection = bluetooth_connection_ns.class_("BluetoothConnection")
|
||||
RP2GattClient = bluetooth_connection_ns.class_("RP2GattClient", cg.Component)
|
||||
BluedroidGattClient = bluetooth_connection_ns.class_(
|
||||
"BluedroidGattClient", cg.Component
|
||||
)
|
||||
|
||||
CONF_BACKEND_ID = "backend_id"
|
||||
CONF_ADDRESS_TYPE = "address_type"
|
||||
|
||||
# BLE_ADDR_TYPE_* code space shared with the API and the backends.
|
||||
ADDRESS_TYPES = {"public": 0, "random": 1}
|
||||
|
||||
|
||||
@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
|
||||
def _esp32_schema_fragment() -> cv.Schema:
|
||||
from esphome.components import esp32_ble_tracker
|
||||
|
||||
return bluetooth_connection_ns.class_(
|
||||
"BluetoothConnection", esp32_ble_client.BLEClientBase
|
||||
return esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA
|
||||
|
||||
|
||||
def _rp2_schema_fragment() -> cv.Schema:
|
||||
from esphome.components import rp2040_ble
|
||||
|
||||
return cv.Schema(
|
||||
{cv.GenerateID(rp2040_ble.CONF_RP2040_BLE_ID): cv.use_id(rp2040_ble.RP2040BLE)}
|
||||
)
|
||||
|
||||
|
||||
async def _esp32_register(backend: cg.MockObj, config: ConfigType) -> None:
|
||||
from esphome.components import esp32_ble_tracker
|
||||
|
||||
# The tracker's promote loop owns connect timing; the backend's
|
||||
# tracker-facing shim registers as a raw client.
|
||||
await esp32_ble_tracker.register_raw_client(backend.tracker_client(), config)
|
||||
|
||||
|
||||
async def _rp2_register(backend: cg.MockObj, config: ConfigType) -> None:
|
||||
from esphome.components import rp2040_ble
|
||||
|
||||
await cg.register_parented(backend, config[rp2040_ble.CONF_RP2040_BLE_ID])
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _PlatformBackend:
|
||||
"""One platform's backend: codegen class, extra schema keys (lazy so the
|
||||
platform stack is only imported when targeted), and stack registration."""
|
||||
|
||||
backend_class: cg.MockObjClass
|
||||
schema_fragment: Callable[[], cv.Schema]
|
||||
register: Callable[[cg.MockObj, ConfigType], Awaitable[None]]
|
||||
|
||||
|
||||
# The single registry of platforms with a GATT client backend; a platform
|
||||
# missing here fails loudly everywhere instead of falling into another
|
||||
# platform's arm.
|
||||
_PLATFORM_BACKENDS: dict[str, _PlatformBackend] = {
|
||||
PLATFORM_ESP32: _PlatformBackend(
|
||||
BluedroidGattClient, _esp32_schema_fragment, _esp32_register
|
||||
),
|
||||
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
|
||||
if (entry := _PLATFORM_BACKENDS.get(key)) is None:
|
||||
raise cv.Invalid(f"no GATT client backend is registered for {key}")
|
||||
return entry
|
||||
|
||||
|
||||
def gatt_client_schema(platform: str | None = None) -> cv.Schema:
|
||||
"""Schema fragment for one GATT backend instance: its generated id plus
|
||||
the platform-stack reference new_gatt_backend() resolves.
|
||||
|
||||
Defaults to the platform being validated; pass `platform` explicitly when
|
||||
building a schema outside validation (the language-schema dumper calls
|
||||
per-platform builders under arbitrary CORE platforms).
|
||||
"""
|
||||
entry = _backend_entry(platform)
|
||||
return entry.schema_fragment().extend(
|
||||
{cv.GenerateID(CONF_BACKEND_ID): cv.declare_id(entry.backend_class)}
|
||||
)
|
||||
|
||||
|
||||
def hub_connection_schema(platform: str | None = None) -> cv.Schema:
|
||||
"""Per-slot schema for the proxy's connection wrappers: the wrapper id on
|
||||
top of the backend fragment. Same platform rules as gatt_client_schema()."""
|
||||
return gatt_client_schema(platform).extend(
|
||||
{cv.GenerateID(): cv.declare_id(HubBluetoothConnection)}
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
# The peer keys every dedicated-backend consumer shares: one target device.
|
||||
_PEER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_ADDRESS_TYPE, default="public"): cv.enum(
|
||||
ADDRESS_TYPES, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def gatt_client_config_schema(base_schema: cv.Schema, consumer: str):
|
||||
"""Wrap a dedicated-backend consumer's schema so the consumer stays
|
||||
platform-blind: gates on the platforms with a backend, folds in
|
||||
gatt_client_schema() plus the peer keys (mac_address, address_type),
|
||||
and claims the connection slot. `consumer` names the component in
|
||||
slot-exhaustion errors."""
|
||||
|
||||
@schema_extractor("schema")
|
||||
def apply(config: ConfigType) -> ConfigType:
|
||||
if config is SCHEMA_EXTRACT:
|
||||
# The language-schema dumper runs without a platform; expose the
|
||||
# consumer's keys plus the platform-free peer keys.
|
||||
return base_schema.extend(_PEER_SCHEMA)
|
||||
cv.only_on(GATT_CLIENT_PLATFORMS)(config)
|
||||
schema = base_schema.extend(_PEER_SCHEMA).extend(gatt_client_schema())
|
||||
config = schema(config)
|
||||
return consume_gatt_slot(consumer)(config)
|
||||
|
||||
return apply
|
||||
|
||||
|
||||
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
|
||||
(gatt_client_config_schema / the proxy's 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.
|
||||
await cg.register_component(backend, {})
|
||||
await _backend_entry().register(backend, config)
|
||||
return backend
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
{
|
||||
"bluetooth_connection_esp32.cpp": {
|
||||
"bluetooth_connection_bluedroid.cpp": {
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
@@ -61,6 +262,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
"bluetooth_connection_hub.cpp": {
|
||||
PlatformFramework.RP2_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
"bluetooth_connection_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_bt_device.h>
|
||||
#include <esp_gattc_api.h>
|
||||
#endif
|
||||
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -39,4 +44,26 @@ 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 {
|
||||
|
||||
// Address-scoped Bluedroid maintenance shared by every esp32 proxy build,
|
||||
// including advertisement-only ones where no GATT backend (and none of the
|
||||
// gated surface above) is compiled - so this block sits outside that gate.
|
||||
|
||||
conn_err_t unpair_device(uint64_t address) {
|
||||
esp_bd_addr_t bda;
|
||||
ble_device_base::uint64_to_mac_msb_first(address, bda);
|
||||
return esp_ble_remove_bond_device(bda);
|
||||
}
|
||||
|
||||
conn_err_t clear_gatt_cache(uint64_t address) {
|
||||
esp_bd_addr_t bda;
|
||||
ble_device_base::uint64_to_mac_msb_first(address, bda);
|
||||
return esp_ble_gattc_cache_clean(bda);
|
||||
}
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -16,11 +16,16 @@
|
||||
#include <esp_err.h>
|
||||
#endif
|
||||
|
||||
// A GATT connection backend exists in this build: esp32 (Bluedroid) or a hub
|
||||
// platform with the neutral GATT client compiled in. Single-sourced here so
|
||||
// the proxy and this component cannot drift.
|
||||
#if defined(USE_ESP32) || defined(USE_BLE_GATT_CLIENT)
|
||||
#define BLUETOOTH_CONNECTION_HAS_GATT
|
||||
// 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. 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 {
|
||||
@@ -136,7 +141,7 @@ inline void fill_gatt_uuid(std::array<uint64_t, 2> &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.
|
||||
@@ -148,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
|
||||
|
||||
@@ -0,0 +1,828 @@
|
||||
#include "bluetooth_connection_bluedroid.h"
|
||||
|
||||
#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 BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
#include "bluetooth_connection_hub.h"
|
||||
|
||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_client_state.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <esp_gatt_common_api.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
static const char *const TAG = "bluetooth_connection.bluedroid";
|
||||
|
||||
using ble_device_base::FAST_CONN_TIMEOUT;
|
||||
using ble_device_base::FAST_MAX_CONN_INTERVAL;
|
||||
using ble_device_base::FAST_MIN_CONN_INTERVAL;
|
||||
using ble_device_base::MEDIUM_CONN_TIMEOUT;
|
||||
using ble_device_base::MEDIUM_MAX_CONN_INTERVAL;
|
||||
using ble_device_base::MEDIUM_MIN_CONN_INTERVAL;
|
||||
using esp32_ble_tracker::ClientState;
|
||||
using esp32_ble_tracker::ConnectionType;
|
||||
|
||||
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
|
||||
|
||||
// ---- shim forwarders ----
|
||||
|
||||
bool BluedroidTrackerShim::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
return this->engine_->handle_gattc_event_(event, gattc_if, param);
|
||||
}
|
||||
void BluedroidTrackerShim::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
this->engine_->handle_gap_event_(event, param);
|
||||
}
|
||||
void BluedroidTrackerShim::connect() { this->engine_->tracker_connect_(); }
|
||||
void BluedroidTrackerShim::disconnect() { this->engine_->disconnect(); }
|
||||
|
||||
// ---- component ----
|
||||
|
||||
void BluedroidGattClient::setup() {
|
||||
static uint8_t connection_index = 0;
|
||||
this->connection_index_ = connection_index++;
|
||||
}
|
||||
|
||||
void BluedroidGattClient::loop() {
|
||||
if (!esp32_ble::global_ble->is_active()) {
|
||||
// Stack down: re-register the app on the next enable.
|
||||
this->set_state_(ClientState::INIT);
|
||||
return;
|
||||
}
|
||||
auto st = this->state_();
|
||||
if (st == ClientState::INIT) {
|
||||
auto ret = esp_ble_gattc_app_register(this->shim_.app_id);
|
||||
if (ret) {
|
||||
ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->shim_.app_id, ret);
|
||||
this->mark_failed();
|
||||
}
|
||||
// Do not wait for REG_EVT; a dropped event must not wedge the slot.
|
||||
this->set_state_(ClientState::IDLE);
|
||||
} else if (st == ClientState::IDLE) {
|
||||
// The loop only drives the bootstrap and the disconnect safety timeout.
|
||||
this->disable_loop();
|
||||
} else if (st == ClientState::DISCONNECTING &&
|
||||
millis() - this->disconnecting_started_ > ble_device_base::GATT_DISCONNECT_TIMEOUT_MS) {
|
||||
ESP_LOGE(TAG, "[%d] Timeout waiting for CLOSE_EVT, forcing IDLE", this->connection_index_);
|
||||
// Release before idling: unconditional disconnect does not release, and a
|
||||
// lost CLOSE/DISCONNECT would otherwise leak the table and the cache.
|
||||
this->release_services();
|
||||
this->set_idle_();
|
||||
this->report_connection_state_(false, ESP_GATT_CONN_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
void BluedroidGattClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, " Registration failed; if the error was ESP_GATT_NO_RESOURCES, reduce the connection slots");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- contract ops ----
|
||||
|
||||
int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) {
|
||||
// Refuse anything but a fully idle slot. Clobbering DISCONNECTING with
|
||||
// DISCOVERED would let the tracker open a new link while the old one is
|
||||
// still closing - the stale CLOSE_EVT then tears the new attempt down.
|
||||
if (this->state_() != ClientState::IDLE) {
|
||||
ESP_LOGW(TAG, "[%d] Connect rejected, slot busy", this->connection_index_);
|
||||
return ESP_GATT_BUSY;
|
||||
}
|
||||
ble_device_base::uint64_to_mac_msb_first(address, this->remote_bda_);
|
||||
this->remote_addr_type_ = addr_type;
|
||||
// Hand the request to the tracker's promote loop: it stops the scan, raises
|
||||
// coex, and calls tracker_connect_() - the tracker owns connect timing here.
|
||||
this->set_state_(ClientState::DISCOVERED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void BluedroidGattClient::tracker_connect_() {
|
||||
auto st = this->state_();
|
||||
if (st == ClientState::CONNECTING || st == ClientState::CONNECTED || st == ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%d] Connection already in progress", this->connection_index_);
|
||||
return;
|
||||
}
|
||||
if (st == ClientState::DISCONNECTING) {
|
||||
ESP_LOGW(TAG, "[%d] Cannot connect, still waiting for CLOSE_EVT", this->connection_index_);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "[%d] 0x%02x Connecting", this->connection_index_, this->remote_addr_type_);
|
||||
this->services_released_ = false;
|
||||
this->seen_mtu_ = false;
|
||||
this->enable_loop();
|
||||
this->set_state_(ClientState::CONNECTING);
|
||||
if (this->connection_type_ == ConnectionType::V3_WITHOUT_CACHE) {
|
||||
// Fast params for the discovery phase; stepped down at SEARCH_CMPL.
|
||||
esp_ble_gap_set_prefer_conn_params(this->remote_bda_, FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0,
|
||||
FAST_CONN_TIMEOUT);
|
||||
} else {
|
||||
esp_ble_gap_set_prefer_conn_params(this->remote_bda_, MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0,
|
||||
MEDIUM_CONN_TIMEOUT);
|
||||
}
|
||||
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_,
|
||||
static_cast<esp_ble_addr_type_t>(this->remote_addr_type_), true);
|
||||
if (ret) {
|
||||
this->log_gattc_warning_("esp_ble_gattc_open", ret);
|
||||
// CONNECT_EVT never fired, so conn_id_ is legitimately unset: plain IDLE.
|
||||
this->set_state_(ClientState::IDLE);
|
||||
this->report_connection_state_(false, ret);
|
||||
}
|
||||
}
|
||||
|
||||
int BluedroidGattClient::disconnect() {
|
||||
auto st = this->state_();
|
||||
if (st == ClientState::DISCONNECTING) {
|
||||
return 0;
|
||||
}
|
||||
// Nothing was opened, so no completion event will follow: report
|
||||
// not-connected and the hub frees the slot at once (rp2 convention).
|
||||
if (st == ClientState::IDLE) {
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
}
|
||||
if (st == ClientState::DISCOVERED) {
|
||||
// Parked for the tracker promote loop, never opened.
|
||||
this->set_state_(ClientState::IDLE);
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
}
|
||||
if (st == ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
||||
ESP_LOGD(TAG, "[%d] Disconnect scheduled", this->connection_index_);
|
||||
this->shim_.schedule_disconnect();
|
||||
return 0;
|
||||
}
|
||||
this->unconditional_disconnect_();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void BluedroidGattClient::unconditional_disconnect_() {
|
||||
ESP_LOGI(TAG, "[%d] Disconnecting (conn_id: %d)", this->connection_index_, this->conn_id_);
|
||||
if (this->conn_id_ == UNSET_CONN_ID) {
|
||||
ESP_LOGE(TAG, "[%d] conn id unset, cannot disconnect", this->connection_index_);
|
||||
return;
|
||||
}
|
||||
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
|
||||
if (err != ESP_OK) {
|
||||
// The stack is now in an indeterminate state for this link.
|
||||
ESP_LOGE(TAG, "[%d] esp_ble_gattc_close error: %d", this->connection_index_, err);
|
||||
}
|
||||
this->set_disconnecting_();
|
||||
}
|
||||
|
||||
int BluedroidGattClient::discover_services() {
|
||||
if (this->conn_id_ == UNSET_CONN_ID) {
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
}
|
||||
return this->check_and_log_error_("esp_ble_gattc_search_service",
|
||||
esp_ble_gattc_search_service(this->gattc_if_, this->conn_id_, nullptr));
|
||||
}
|
||||
|
||||
int BluedroidGattClient::read_characteristic(uint16_t handle) {
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char", esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_,
|
||||
handle, ESP_GATT_AUTH_REQ_NONE));
|
||||
}
|
||||
|
||||
int BluedroidGattClient::write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
|
||||
// The BTC layer copies the payload immediately, so the const_cast is safe.
|
||||
return this->check_and_log_error_(
|
||||
"esp_ble_gattc_write_char",
|
||||
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP,
|
||||
ESP_GATT_AUTH_REQ_NONE));
|
||||
}
|
||||
|
||||
int BluedroidGattClient::read_descriptor(uint16_t handle) {
|
||||
return this->check_and_log_error_(
|
||||
"esp_ble_gattc_read_char_descr",
|
||||
esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE));
|
||||
}
|
||||
|
||||
int BluedroidGattClient::write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
|
||||
return this->check_and_log_error_(
|
||||
"esp_ble_gattc_write_char_descr",
|
||||
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
|
||||
ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE));
|
||||
}
|
||||
|
||||
int BluedroidGattClient::notify_characteristic(uint16_t handle, bool enable) {
|
||||
// Local registration only; the CCCD write is the API client's responsibility.
|
||||
if (enable) {
|
||||
return this->check_and_log_error_("esp_ble_gattc_register_for_notify",
|
||||
esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle));
|
||||
}
|
||||
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify",
|
||||
esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle));
|
||||
}
|
||||
|
||||
int BluedroidGattClient::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
|
||||
|
||||
int BluedroidGattClient::update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
|
||||
uint16_t timeout) {
|
||||
return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom");
|
||||
}
|
||||
|
||||
void BluedroidGattClient::release_services() {
|
||||
this->service_total_ = 0;
|
||||
#ifdef USE_BLE_GATT_SERVICE_TABLE
|
||||
this->free_service_table_();
|
||||
#endif
|
||||
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
|
||||
// Only the cache clean makes the stack's database unsafe to walk.
|
||||
this->services_released_ = true;
|
||||
esp_ble_gattc_cache_clean(this->remote_bda_);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_BLE_GATT_SERVICE_TABLE
|
||||
ble_device_base::GattServiceTable BluedroidGattClient::get_service_table() {
|
||||
if (this->table_storage_ == nullptr &&
|
||||
(this->services_released_ || this->service_total_ == 0 || !this->build_service_table_())) {
|
||||
return {};
|
||||
}
|
||||
return this->table_view_();
|
||||
}
|
||||
|
||||
// The view is carved from the storage block and the counts on each call
|
||||
// (a cold path) rather than cached, saving a per-instance table member.
|
||||
ble_device_base::GattServiceTable BluedroidGattClient::table_view_() const {
|
||||
size_t svc_bytes = this->service_total_ * sizeof(ble_device_base::GattService);
|
||||
size_t char_bytes = this->table_char_total_ * sizeof(ble_device_base::GattCharacteristic);
|
||||
return {reinterpret_cast<const ble_device_base::GattService *>(this->table_storage_),
|
||||
reinterpret_cast<const ble_device_base::GattCharacteristic *>(this->table_storage_ + svc_bytes),
|
||||
reinterpret_cast<const ble_device_base::GattDescriptor *>(this->table_storage_ + svc_bytes + char_bytes),
|
||||
this->service_total_,
|
||||
this->table_char_total_,
|
||||
this->table_desc_total_};
|
||||
}
|
||||
|
||||
void BluedroidGattClient::free_service_table_() {
|
||||
if (this->table_storage_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
||||
allocator.deallocate(this->table_storage_, 0);
|
||||
this->table_storage_ = nullptr;
|
||||
this->table_char_total_ = 0;
|
||||
this->table_desc_total_ = 0;
|
||||
}
|
||||
|
||||
template<typename ServiceFn, typename CharFn, typename DescFn>
|
||||
bool BluedroidGattClient::walk_database_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc) {
|
||||
// Shared enumeration for both table-build passes: an identical walk order
|
||||
// is what lets the counting pass size the block the filling pass fills.
|
||||
// INVALID_OFFSET/NOT_FOUND mean end-of-range; anything else is a failure.
|
||||
for (uint16_t s = 0; s < this->service_total_; s++) {
|
||||
esp_gattc_service_elem_t svc;
|
||||
uint16_t svc_count = 1;
|
||||
if (esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &svc, &svc_count, s) != ESP_GATT_OK ||
|
||||
svc_count == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!on_service(s, svc)) {
|
||||
return false;
|
||||
}
|
||||
uint16_t svc_chars = 0;
|
||||
if (esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC, svc.start_handle,
|
||||
svc.end_handle, 0, &svc_chars) != ESP_GATT_OK) {
|
||||
return false;
|
||||
}
|
||||
for (uint16_t c = 0; c < svc_chars; c++) {
|
||||
esp_gattc_char_elem_t chr;
|
||||
uint16_t char_count = 1;
|
||||
auto status = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, svc.start_handle, svc.end_handle, &chr,
|
||||
&char_count, c);
|
||||
if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (status != ESP_GATT_OK || char_count == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!on_char(svc, chr)) {
|
||||
return false;
|
||||
}
|
||||
for (uint16_t d = 0;; d++) {
|
||||
esp_gattc_descr_elem_t desc;
|
||||
uint16_t desc_count = 1;
|
||||
auto desc_status =
|
||||
esp_ble_gattc_get_all_descr(this->gattc_if_, this->conn_id_, chr.char_handle, &desc, &desc_count, d);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK || desc_count == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!on_desc(chr, desc)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BluedroidGattClient::build_service_table_() {
|
||||
// Pass 1: count, so one exact-size block holds the whole table.
|
||||
uint16_t char_total = 0;
|
||||
uint16_t desc_total = 0;
|
||||
bool counted = this->walk_database_([](uint16_t, const esp_gattc_service_elem_t &) { return true; },
|
||||
[&](const esp_gattc_service_elem_t &, const esp_gattc_char_elem_t &) {
|
||||
char_total++;
|
||||
return true;
|
||||
},
|
||||
[&](const esp_gattc_char_elem_t &, const esp_gattc_descr_elem_t &) {
|
||||
desc_total++;
|
||||
return true;
|
||||
});
|
||||
if (!counted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The arrays share one block; carving stays aligned because each struct's
|
||||
// strictest member is the UUID and array sizes are multiples of it.
|
||||
size_t svc_bytes = this->service_total_ * sizeof(ble_device_base::GattService);
|
||||
size_t char_bytes = char_total * sizeof(ble_device_base::GattCharacteristic);
|
||||
size_t total_bytes = svc_bytes + char_bytes + desc_total * sizeof(ble_device_base::GattDescriptor);
|
||||
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
||||
this->table_storage_ = allocator.allocate(total_bytes);
|
||||
if (this->table_storage_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[%d] Service table allocation failed (%u bytes)", this->connection_index_,
|
||||
static_cast<unsigned>(total_bytes));
|
||||
return false;
|
||||
}
|
||||
auto *services = reinterpret_cast<ble_device_base::GattService *>(this->table_storage_);
|
||||
auto *characteristics = reinterpret_cast<ble_device_base::GattCharacteristic *>(this->table_storage_ + svc_bytes);
|
||||
auto *descriptors =
|
||||
reinterpret_cast<ble_device_base::GattDescriptor *>(this->table_storage_ + svc_bytes + char_bytes);
|
||||
|
||||
// Pass 2: fill, bounded by the pass-1 totals. A bound trip or a shortfall
|
||||
// means the cached database changed between the passes; fail the build
|
||||
// rather than serve an inconsistent table (the consumer retries).
|
||||
uint16_t char_index = 0;
|
||||
uint16_t desc_index = 0;
|
||||
ble_device_base::GattService *cur_service = nullptr;
|
||||
ble_device_base::GattCharacteristic *cur_char = nullptr;
|
||||
bool filled = this->walk_database_(
|
||||
[&](uint16_t s, const esp_gattc_service_elem_t &svc) {
|
||||
cur_service = &services[s];
|
||||
cur_service->uuid = ble_device_base::ESPBTUUID::from_uuid(svc.uuid);
|
||||
cur_service->start_handle = svc.start_handle;
|
||||
cur_service->end_handle = svc.end_handle;
|
||||
cur_service->first_characteristic = char_index;
|
||||
cur_service->characteristic_count = 0;
|
||||
return true;
|
||||
},
|
||||
[&](const esp_gattc_service_elem_t &svc, const esp_gattc_char_elem_t &chr) {
|
||||
if (char_index >= char_total) {
|
||||
return false;
|
||||
}
|
||||
cur_char = &characteristics[char_index++];
|
||||
cur_char->uuid = ble_device_base::ESPBTUUID::from_uuid(chr.uuid);
|
||||
cur_char->value_handle = chr.char_handle;
|
||||
// Bluedroid addresses descriptors by characteristic handle, so the
|
||||
// table's end_handle only needs the service-bounded upper bound.
|
||||
cur_char->end_handle = svc.end_handle;
|
||||
cur_char->properties = chr.properties;
|
||||
cur_char->first_descriptor = desc_index;
|
||||
cur_char->descriptor_count = 0;
|
||||
cur_service->characteristic_count++;
|
||||
return true;
|
||||
},
|
||||
[&](const esp_gattc_char_elem_t &, const esp_gattc_descr_elem_t &desc) {
|
||||
if (desc_index >= desc_total) {
|
||||
return false;
|
||||
}
|
||||
descriptors[desc_index].uuid = ble_device_base::ESPBTUUID::from_uuid(desc.uuid);
|
||||
descriptors[desc_index].handle = desc.handle;
|
||||
desc_index++;
|
||||
cur_char->descriptor_count++;
|
||||
return true;
|
||||
});
|
||||
if (!filled || char_index != char_total || desc_index != desc_total) {
|
||||
this->free_service_table_();
|
||||
return false;
|
||||
}
|
||||
this->table_char_total_ = char_total;
|
||||
this->table_desc_total_ = desc_total;
|
||||
return true;
|
||||
}
|
||||
#endif // USE_BLE_GATT_SERVICE_TABLE
|
||||
|
||||
// ---- internals ----
|
||||
|
||||
bool BluedroidGattClient::check_addr_(const esp_bd_addr_t &addr) const {
|
||||
return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0;
|
||||
}
|
||||
|
||||
void BluedroidGattClient::set_idle_() {
|
||||
this->set_state_(ClientState::IDLE);
|
||||
this->conn_id_ = UNSET_CONN_ID;
|
||||
}
|
||||
|
||||
void BluedroidGattClient::set_disconnecting_() {
|
||||
this->disconnecting_started_ = millis();
|
||||
this->set_state_(ClientState::DISCONNECTING);
|
||||
// The loop may be disabled while idle; the safety timeout needs it.
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void BluedroidGattClient::report_connection_state_(bool connected, int error) {
|
||||
this->listener_->on_connection_state(connected, this->mtu_, error);
|
||||
}
|
||||
|
||||
esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
|
||||
uint16_t timeout, const char *param_type) {
|
||||
esp_ble_conn_update_params_t conn_params = {{0}};
|
||||
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
||||
conn_params.min_int = min_interval;
|
||||
conn_params.max_int = max_interval;
|
||||
conn_params.latency = latency;
|
||||
conn_params.timeout = timeout;
|
||||
ESP_LOGD(TAG, "[%d] %s conn params", this->connection_index_, param_type);
|
||||
return this->check_and_log_error_("esp_ble_gap_update_conn_params", esp_ble_gap_update_conn_params(&conn_params));
|
||||
}
|
||||
|
||||
int BluedroidGattClient::check_and_log_error_(const char *operation, esp_err_t err) {
|
||||
if (err != ESP_OK) {
|
||||
this->log_gattc_warning_(operation, err);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
void BluedroidGattClient::log_gattc_warning_(const char *operation, int code) {
|
||||
ESP_LOGW(TAG, "[%d] %s failed, status=%d", this->connection_index_, operation, code);
|
||||
}
|
||||
|
||||
// ---- service streaming ----
|
||||
|
||||
void BluedroidGattClient::handle_search_cmpl_() {
|
||||
// Step down from the fast discovery params.
|
||||
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
|
||||
uint16_t primary = 0;
|
||||
uint16_t secondary = 0;
|
||||
auto primary_status = esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_PRIMARY_SERVICE,
|
||||
0x0001, 0xFFFF, 0, &primary);
|
||||
auto secondary_status = esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_SECONDARY_SERVICE,
|
||||
0x0001, 0xFFFF, 0, &secondary);
|
||||
if (primary_status != ESP_GATT_OK || secondary_status != ESP_GATT_OK) {
|
||||
// A failed count must not become an authoritative empty database - V3
|
||||
// clients cache the streamed result permanently.
|
||||
auto status = primary_status != ESP_GATT_OK ? primary_status : secondary_status;
|
||||
this->log_gattc_warning_("esp_ble_gattc_get_attr_count", status);
|
||||
this->listener_->on_service_discovery_done(status);
|
||||
return;
|
||||
}
|
||||
this->service_total_ = primary + secondary;
|
||||
this->listener_->on_service_discovery_done(0);
|
||||
}
|
||||
|
||||
#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;
|
||||
conn.proxy_->send_gatt_services_done(conn.address_);
|
||||
this->release_services();
|
||||
return;
|
||||
}
|
||||
|
||||
// The subscriber vanished mid-stream: park the cursor at done WITHOUT
|
||||
// sending services-done (a resubscribing client gets silence and its 30 s
|
||||
// timeout, never an authoritative partial list).
|
||||
auto *api_conn = conn.proxy_->get_api_connection();
|
||||
if (api_conn == nullptr) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] API connection lost while streaming services", conn.connection_index_, conn.address_str_);
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
this->release_services();
|
||||
return;
|
||||
}
|
||||
|
||||
bool use_efficient_uuids = conn.proxy_->client_supports_efficient_uuids();
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
resp.address = conn.address_;
|
||||
size_t current_size = resp.calculate_size();
|
||||
int16_t batch_start = conn.send_service_;
|
||||
|
||||
while (conn.send_service_ < this->service_total_) {
|
||||
esp_gattc_service_elem_t service_result;
|
||||
uint16_t svc_count = 1;
|
||||
if (esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &service_result, &svc_count,
|
||||
conn.send_service_) != ESP_GATT_OK ||
|
||||
svc_count == 0) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] Service walk failed (service %d), aborting stream", conn.connection_index_,
|
||||
conn.address_str_, conn.send_service_);
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
uint16_t total_char_count = 0;
|
||||
if (esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||
service_result.start_handle, service_result.end_handle, 0,
|
||||
&total_char_count) != ESP_GATT_OK) {
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
|
||||
// If this service likely won't fit, send the current batch first.
|
||||
size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids);
|
||||
if (!resp.services.empty() && current_size + estimated_size > MAX_PACKET_SIZE) {
|
||||
break;
|
||||
}
|
||||
|
||||
resp.services.emplace_back();
|
||||
auto &service_resp = resp.services.back();
|
||||
fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid,
|
||||
ble_device_base::ESPBTUUID::from_uuid(service_result.uuid), use_efficient_uuids);
|
||||
service_resp.handle = service_result.start_handle;
|
||||
|
||||
if (total_char_count > 0) {
|
||||
service_resp.characteristics.init(total_char_count);
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
// Bounded by the count query: a misbehaving peripheral can make the
|
||||
// enumeration return more entries than it reported.
|
||||
while (char_offset < total_char_count) {
|
||||
uint16_t cc = 1;
|
||||
auto char_status = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||
service_result.end_handle, &char_result, &cc, char_offset);
|
||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (char_status != ESP_GATT_OK || cc == 0) {
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status);
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
service_resp.characteristics.emplace_back();
|
||||
auto &characteristic_resp = service_resp.characteristics.back();
|
||||
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid,
|
||||
ble_device_base::ESPBTUUID::from_uuid(char_result.uuid), use_efficient_uuids);
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
|
||||
uint16_t total_desc_count = 0;
|
||||
auto desc_count_status = esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR,
|
||||
0, 0, char_result.char_handle, &total_desc_count);
|
||||
if (desc_count_status != ESP_GATT_OK) {
|
||||
// Abort rather than stream the characteristic descriptor-less: a
|
||||
// missing CCCD in a cached database breaks notifications for good.
|
||||
this->log_gattc_warning_("esp_ble_gattc_get_attr_count", desc_count_status);
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (total_desc_count > 0) {
|
||||
characteristic_resp.descriptors.init(total_desc_count);
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (desc_offset < total_desc_count) {
|
||||
uint16_t dc = 1;
|
||||
auto desc_status = esp_ble_gattc_get_all_descr(this->gattc_if_, this->conn_id_, char_result.char_handle,
|
||||
&desc_result, &dc, desc_offset);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK || dc == 0) {
|
||||
if (desc_status != ESP_GATT_OK) {
|
||||
this->log_gattc_warning_("esp_ble_gattc_get_all_descr", desc_status);
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
characteristic_resp.descriptors.emplace_back();
|
||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid,
|
||||
ble_device_base::ESPBTUUID::from_uuid(desc_result.uuid), use_efficient_uuids);
|
||||
descriptor_resp.handle = desc_result.handle;
|
||||
desc_offset++;
|
||||
}
|
||||
}
|
||||
char_offset++;
|
||||
}
|
||||
}
|
||||
|
||||
if (close_service_batch(resp, current_size, conn.send_service_, conn.connection_index_, conn.address_str_) !=
|
||||
BatchClose::CONTINUE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// On a failed send, rewind the cursor so the batch is retried instead of
|
||||
// silently skipped.
|
||||
if (!api_conn->send_message(resp)) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Failed to send service batch, retrying", conn.connection_index_, conn.address_str_);
|
||||
conn.send_service_ = batch_start;
|
||||
}
|
||||
}
|
||||
#endif // BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
// ---- events ----
|
||||
|
||||
void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) {
|
||||
auto st = this->state_();
|
||||
if (st == ClientState::IDLE) {
|
||||
// IDF can deliver OPEN_EVT after esp_ble_gattc_open already returned an
|
||||
// error and the slot went IDLE; do not resurrect it.
|
||||
ESP_LOGD(TAG, "[%d] OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, param->open.status);
|
||||
return;
|
||||
}
|
||||
if (st != ClientState::CONNECTING) {
|
||||
ESP_LOGE(TAG, "[%d] OPEN_EVT in unexpected state", this->connection_index_);
|
||||
}
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
this->log_gattc_warning_("Connection open", param->open.status);
|
||||
// Never established, CLOSE_EVT may not follow.
|
||||
this->set_idle_();
|
||||
this->report_connection_state_(false, param->open.status);
|
||||
return;
|
||||
}
|
||||
if (this->shim_.disconnect_pending()) {
|
||||
// Earliest point conn_id_ exists; keep it set so CLOSE_EVT still matches.
|
||||
this->unconditional_disconnect_();
|
||||
return;
|
||||
}
|
||||
this->set_state_(ClientState::CONNECTED);
|
||||
ESP_LOGI(TAG, "[%d] Connection open", this->connection_index_);
|
||||
if (this->connection_type_ == ConnectionType::V3_WITH_CACHE) {
|
||||
this->set_state_(ClientState::ESTABLISHED);
|
||||
// No discovery phase: report immediately; the MTU report below is
|
||||
// suppressed by seen_mtu_ (HA tolerates a post-connect MTU of 23 here,
|
||||
// matching the previous esp32 behavior).
|
||||
this->seen_mtu_ = true;
|
||||
this->report_connection_state_(true, 0);
|
||||
// Settled: only the disconnect safety net needs the loop, and
|
||||
// set_disconnecting_() re-enables it.
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void BluedroidGattClient::handle_disconnect_evt_(esp_ble_gattc_cb_param_t *param) {
|
||||
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && this->state_() == ClientState::CONNECTED) {
|
||||
ESP_LOGW(TAG, "[%d] Remote closed during discovery", this->connection_index_);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%d] DISCONNECT_EVT reason=0x%02x", this->connection_index_, param->disconnect.reason);
|
||||
}
|
||||
if (this->state_() == ClientState::IDLE) {
|
||||
// Active close delivers CLOSE_EVT first; never walk back to DISCONNECTING.
|
||||
return;
|
||||
}
|
||||
// Passive disconnect: wait for CLOSE_EVT before going IDLE (reconnecting
|
||||
// earlier makes the controller reject with 133 or assert) and before
|
||||
// reporting - the wrapper frees the slot on the report, and a freed slot
|
||||
// invites a reconnect into the still-closing link.
|
||||
this->release_services();
|
||||
this->set_disconnecting_();
|
||||
}
|
||||
|
||||
bool BluedroidGattClient::handle_gattc_event_(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
if (event == ESP_GATTC_REG_EVT && this->shim_.app_id != param->reg.app_id)
|
||||
return false;
|
||||
if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_)
|
||||
return false;
|
||||
|
||||
switch (event) {
|
||||
case ESP_GATTC_REG_EVT: {
|
||||
if (param->reg.status == ESP_GATT_OK) {
|
||||
this->gattc_if_ = esp_gattc_if;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "[%d] gattc app registration failed, status=%d", this->connection_index_, param->reg.status);
|
||||
this->mark_failed();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CONNECT_EVT: {
|
||||
if (!this->check_addr_(param->connect.remote_bda))
|
||||
return false;
|
||||
this->conn_id_ = param->connect.conn_id;
|
||||
// MTU request here rather than OPEN_EVT, matching the IDF examples.
|
||||
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
|
||||
if (ret) {
|
||||
this->log_gattc_warning_("esp_ble_gattc_send_mtu_req", ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
if (!this->check_addr_(param->open.remote_bda))
|
||||
return false;
|
||||
this->handle_open_evt_(param);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CFG_MTU_EVT: {
|
||||
if (this->conn_id_ != param->cfg_mtu.conn_id)
|
||||
return false;
|
||||
if (param->cfg_mtu.status != ESP_GATT_OK) {
|
||||
// Warn only; a disconnect will follow if the link is dead.
|
||||
this->log_gattc_warning_("MTU exchange", param->cfg_mtu.status);
|
||||
} else {
|
||||
this->mtu_ = param->cfg_mtu.mtu;
|
||||
}
|
||||
if (!this->seen_mtu_) {
|
||||
this->seen_mtu_ = true;
|
||||
// The connected report waited for the MTU so HA never sees 23.
|
||||
this->report_connection_state_(true, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
if (!this->check_addr_(param->disconnect.remote_bda))
|
||||
return false;
|
||||
this->handle_disconnect_evt_(param);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
if (this->conn_id_ != param->close.conn_id)
|
||||
return false;
|
||||
this->release_services();
|
||||
this->set_idle_();
|
||||
// The one connected=false report: the wrapper frees the slot on it,
|
||||
// so it must not fire before the controller finished closing.
|
||||
this->report_connection_state_(false, param->close.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
if (this->conn_id_ != param->search_cmpl.conn_id)
|
||||
return false;
|
||||
ESP_LOGI(TAG, "[%d] Service discovery complete", this->connection_index_);
|
||||
this->set_state_(ClientState::ESTABLISHED);
|
||||
this->handle_search_cmpl_();
|
||||
// Settled (see the V3_WITH_CACHE arm in handle_open_evt_).
|
||||
this->disable_loop();
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_CHAR_EVT:
|
||||
case ESP_GATTC_READ_DESCR_EVT: {
|
||||
if (this->conn_id_ != param->read.conn_id)
|
||||
return false;
|
||||
bool ok = param->read.status == ESP_GATT_OK;
|
||||
this->listener_->on_read_result(param->read.handle, ok ? param->read.value : nullptr,
|
||||
ok ? param->read.value_len : 0, ok ? 0 : param->read.status);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_CHAR_EVT:
|
||||
case ESP_GATTC_WRITE_DESCR_EVT: {
|
||||
if (this->conn_id_ != param->write.conn_id)
|
||||
return false;
|
||||
this->listener_->on_write_result(param->write.handle,
|
||||
param->write.status == ESP_GATT_OK ? 0 : param->write.status);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->listener_->on_notify_state(param->reg_for_notify.handle, true,
|
||||
param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
|
||||
this->listener_->on_notify_state(
|
||||
param->unreg_for_notify.handle, false,
|
||||
param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (this->conn_id_ != param->notify.conn_id)
|
||||
return false;
|
||||
ESP_LOGV(TAG, "[%d] NOTIFY_EVT handle=0x%2X", this->connection_index_, param->notify.handle);
|
||||
this->listener_->on_notify_data(param->notify.handle, param->notify.value, param->notify.value_len);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BluedroidGattClient::handle_gap_event_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GAP_BLE_SEC_REQ_EVT: {
|
||||
if (!this->check_addr_(param->ble_security.auth_cmpl.bd_addr))
|
||||
break;
|
||||
// Always accept a server-initiated security request.
|
||||
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
|
||||
break;
|
||||
}
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT: {
|
||||
if (!this->check_addr_(param->ble_security.auth_cmpl.bd_addr))
|
||||
break;
|
||||
this->listener_->on_pairing_result(
|
||||
param->ble_security.auth_cmpl.success ? 0 : param->ble_security.auth_cmpl.fail_reason);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT
|
||||
@@ -0,0 +1,166 @@
|
||||
// Bluedroid (esp32) GATT client backend: the esp32 arm of the
|
||||
// ble_device_base::BLEGattConnection alias for the hub BluetoothConnection
|
||||
// wrapper. Not a BLEClientBase: the tracker's promote loop owns
|
||||
// scan-stop/coex/one-connect-at-a-time, so the contract's connect() only
|
||||
// parks the address in DISCOVERED and the real esp_ble_gattc_open happens in
|
||||
// the tracker-invoked shim connect(). The shim exists because the tracker's
|
||||
// ESPBTClient::disconnect() returns void while the contract's returns int -
|
||||
// one class cannot carry both.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#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"
|
||||
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
class BluedroidGattClient;
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
class BluetoothConnection;
|
||||
#endif
|
||||
|
||||
// The tracker-facing half: owns the ClientState the promote loop reads and
|
||||
// forwards events/commands to the engine.
|
||||
class BluedroidTrackerShim final : public esp32_ble_tracker::ESPBTClient {
|
||||
public:
|
||||
explicit BluedroidTrackerShim(BluedroidGattClient *engine) : engine_(engine) {}
|
||||
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
void connect() override;
|
||||
void disconnect() override;
|
||||
bool wants_parsed_advertisements() override { return false; }
|
||||
void on_scan_end() override {}
|
||||
bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; }
|
||||
|
||||
void schedule_disconnect() { this->want_disconnect_ = true; }
|
||||
|
||||
protected:
|
||||
BluedroidGattClient *engine_;
|
||||
};
|
||||
|
||||
class BluedroidGattClient final : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_BLUETOOTH; }
|
||||
|
||||
// Wired by codegen before setup and invariant for the device lifetime.
|
||||
void set_listener(ble_device_base::GattClientListener *listener) { this->listener_ = listener; }
|
||||
esp32_ble_tracker::ESPBTClient *tracker_client() { return &this->shim_; }
|
||||
|
||||
// ---- 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);
|
||||
// Materialized on demand from Bluedroid's cached database for direct
|
||||
// consumers that resolve handles by UUID. The streaming consumer (the
|
||||
// proxy wrapper) never calls this - it uses stream_service_batch - so the
|
||||
// materializer only compiles when codegen declares a direct consumer
|
||||
// (USE_BLE_GATT_SERVICE_TABLE) and proxy-only builds keep the old
|
||||
// footprint; a direct consumer's peak is bounded by its one known device.
|
||||
#ifdef USE_BLE_GATT_SERVICE_TABLE
|
||||
ble_device_base::GattServiceTable get_service_table();
|
||||
#else
|
||||
ble_device_base::GattServiceTable get_service_table() { return {}; }
|
||||
#endif
|
||||
void release_services();
|
||||
|
||||
#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.
|
||||
void stream_service_batch(BluetoothConnection &conn);
|
||||
#endif
|
||||
|
||||
void set_connection_type(ble_device_base::ConnectionType ct) { this->connection_type_ = ct; }
|
||||
|
||||
protected:
|
||||
friend class BluedroidTrackerShim;
|
||||
|
||||
esp32_ble_tracker::ClientState state_() const { return this->shim_.state(); }
|
||||
void set_state_(esp32_ble_tracker::ClientState st) { this->shim_.set_state(st); }
|
||||
bool check_addr_(const esp_bd_addr_t &addr) const;
|
||||
void tracker_connect_();
|
||||
bool handle_gattc_event_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
|
||||
void handle_gap_event_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
|
||||
void handle_open_evt_(esp_ble_gattc_cb_param_t *param);
|
||||
void handle_disconnect_evt_(esp_ble_gattc_cb_param_t *param);
|
||||
void handle_search_cmpl_();
|
||||
void unconditional_disconnect_();
|
||||
void set_idle_();
|
||||
void set_disconnecting_();
|
||||
void report_connection_state_(bool connected, int error);
|
||||
esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
|
||||
const char *param_type);
|
||||
int check_and_log_error_(const char *operation, esp_err_t err);
|
||||
void log_gattc_warning_(const char *operation, int code);
|
||||
#ifdef USE_BLE_GATT_SERVICE_TABLE
|
||||
template<typename ServiceFn, typename CharFn, typename DescFn>
|
||||
bool walk_database_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc);
|
||||
bool build_service_table_();
|
||||
void free_service_table_();
|
||||
ble_device_base::GattServiceTable table_view_() const;
|
||||
#endif
|
||||
|
||||
// Group 1: pointers / composed objects
|
||||
BluedroidTrackerShim shim_{this};
|
||||
ble_device_base::GattClientListener *listener_{nullptr};
|
||||
#ifdef USE_BLE_GATT_SERVICE_TABLE
|
||||
// One exact-size block carved into the table's three arrays; owned here,
|
||||
// freed by release_services(). Null when no table is materialized. The
|
||||
// GattServiceTable view is rebuilt from this pointer and the counts on
|
||||
// each (cold) get_service_table() call instead of being cached.
|
||||
uint8_t *table_storage_{nullptr};
|
||||
#endif
|
||||
// Group 2: 4-byte types
|
||||
int gattc_if_{ESP_GATT_IF_NONE};
|
||||
uint32_t disconnecting_started_{0};
|
||||
|
||||
// Group 3: arrays
|
||||
esp_bd_addr_t remote_bda_{};
|
||||
|
||||
// Group 4: 2-byte types
|
||||
uint16_t conn_id_{0xFFFF};
|
||||
uint16_t mtu_{23};
|
||||
uint16_t service_total_{0};
|
||||
#ifdef USE_BLE_GATT_SERVICE_TABLE
|
||||
// Filled element counts of the materialized table (0 when none).
|
||||
uint16_t table_char_total_{0};
|
||||
uint16_t table_desc_total_{0};
|
||||
#endif
|
||||
|
||||
// Group 5: 1-byte types
|
||||
// Stored narrow (the enum is 4 bytes); widened at the esp_ble_gattc_open call.
|
||||
uint8_t remote_addr_type_{0};
|
||||
esp32_ble_tracker::ConnectionType connection_type_{esp32_ble_tracker::ConnectionType::V3_WITHOUT_CACHE};
|
||||
uint8_t connection_index_;
|
||||
// Set only when release_services() cleans the stack's GATT cache, which no
|
||||
// walk may then touch (Bluedroid asserts rather than erroring).
|
||||
bool services_released_{false};
|
||||
// The connected report waits for the MTU exchange; OPEN_EVT alone would
|
||||
// hand HA the default 23.
|
||||
bool seen_mtu_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT
|
||||
@@ -1,484 +0,0 @@
|
||||
#include "bluetooth_connection_esp32.h"
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
using ble_device_base::ESPBTUUID;
|
||||
|
||||
static const char *const TAG = "bluetooth_connection";
|
||||
|
||||
conn_err_t unpair_device(uint64_t address) {
|
||||
esp_bd_addr_t bd_addr;
|
||||
ble_device_base::uint64_to_mac_msb_first(address, bd_addr);
|
||||
return esp_ble_remove_bond_device(bd_addr);
|
||||
}
|
||||
|
||||
conn_err_t clear_gatt_cache(uint64_t address) {
|
||||
esp_bd_addr_t bd_addr;
|
||||
ble_device_base::uint64_to_mac_msb_first(address, bd_addr);
|
||||
return esp_ble_gattc_cache_clean(bd_addr);
|
||||
}
|
||||
|
||||
void BluetoothConnection::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BLE Connection:");
|
||||
BLEClientBase::dump_config();
|
||||
}
|
||||
|
||||
void BluetoothConnection::set_address(uint64_t address) {
|
||||
// Keep the proxy's pre-allocated connections-free message in step
|
||||
this->proxy_->update_address_slot_(this->address_, address);
|
||||
// Call parent implementation to actually set the address
|
||||
BLEClientBase::set_address(address);
|
||||
}
|
||||
|
||||
void BluetoothConnection::loop() {
|
||||
BLEClientBase::loop();
|
||||
|
||||
// Early return if no active connection
|
||||
if (this->address_ == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle service discovery if in valid range
|
||||
if (this->send_service_ >= 0 && this->send_service_ <= this->service_count_) {
|
||||
this->send_service_for_discovery_();
|
||||
}
|
||||
|
||||
// Check if we should disable the loop
|
||||
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
|
||||
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
|
||||
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
|
||||
// Never disable while DISCONNECTING — BLEClientBase::loop() needs to keep running so the
|
||||
// 10s safety timeout can force IDLE if CLOSE_EVT is never delivered.
|
||||
if (this->state() != espbt::ClientState::INIT && this->state() != espbt::ClientState::DISCONNECTING &&
|
||||
(this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::on_disconnect_complete(esp_err_t reason) {
|
||||
// Called from both the CLOSE_EVT handler and the DISCONNECTING safety timeout in the
|
||||
// base class. Free the proxy slot, notify the API client, and reset send_service_.
|
||||
// address_ may already be 0 if reset_connection_ ran earlier on this teardown.
|
||||
if (this->address_ == 0) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_, reason);
|
||||
this->reset_connection_(reason);
|
||||
}
|
||||
|
||||
void BluetoothConnection::reset_connection_(esp_err_t reason) { this->proxy_->reset_connection_slot_(this, reason); }
|
||||
|
||||
void BluetoothConnection::send_service_for_discovery_() {
|
||||
if (this->send_service_ >= this->service_count_) {
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->proxy_->send_gatt_services_done(this->address_);
|
||||
this->release_services();
|
||||
return;
|
||||
}
|
||||
|
||||
// Early return if no API connection
|
||||
auto *api_conn = this->proxy_->get_api_connection();
|
||||
if (api_conn == nullptr) {
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if client supports efficient UUIDs
|
||||
bool use_efficient_uuids = this->proxy_->client_supports_efficient_uuids();
|
||||
|
||||
// Prepare response
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
resp.address = this->address_;
|
||||
|
||||
// Dynamic batching based on actual size
|
||||
// Keep running total of actual message size
|
||||
size_t current_size = resp.calculate_size();
|
||||
int16_t batch_start = this->send_service_;
|
||||
|
||||
while (this->send_service_ < this->service_count_) {
|
||||
esp_gattc_service_elem_t service_result;
|
||||
uint16_t service_count = 1;
|
||||
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
||||
&service_result, &service_count, this->send_service_);
|
||||
|
||||
if (service_status != ESP_GATT_OK || service_count == 0) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
|
||||
this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing",
|
||||
service_status, service_count, this->send_service_);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the number of characteristics BEFORE adding to response
|
||||
uint16_t total_char_count = 0;
|
||||
esp_gatt_status_t char_count_status =
|
||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||
|
||||
if (char_count_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
|
||||
// If this service likely won't fit, send current batch (unless it's the first)
|
||||
size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids);
|
||||
if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) {
|
||||
// This service likely won't fit, send current batch
|
||||
break;
|
||||
}
|
||||
|
||||
// Now add the service since we know it will likely fit
|
||||
resp.services.emplace_back();
|
||||
auto &service_resp = resp.services.back();
|
||||
|
||||
fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, ESPBTUUID::from_uuid(service_result.uuid),
|
||||
use_efficient_uuids);
|
||||
|
||||
service_resp.handle = service_result.start_handle;
|
||||
|
||||
if (total_char_count > 0) {
|
||||
// Initialize FixedVector with exact count and process characteristics
|
||||
service_resp.characteristics.init(total_char_count);
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
// Bound by total_char_count: the vector is sized for it, and a malicious peripheral
|
||||
// can make enumeration return more entries than the count query reported
|
||||
while (char_offset < total_char_count) { // characteristics
|
||||
uint16_t char_count = 1;
|
||||
esp_gatt_status_t char_status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_all_char", char_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (char_count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
service_resp.characteristics.emplace_back();
|
||||
auto &characteristic_resp = service_resp.characteristics.back();
|
||||
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, ESPBTUUID::from_uuid(char_result.uuid),
|
||||
use_efficient_uuids);
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
char_offset++;
|
||||
|
||||
// Get the number of descriptors directly with one call
|
||||
uint16_t total_desc_count = 0;
|
||||
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
|
||||
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
|
||||
|
||||
if (desc_count_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (total_desc_count == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize FixedVector with exact count and process descriptors
|
||||
characteristic_resp.descriptors.init(total_desc_count);
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (desc_offset < total_desc_count) { // descriptors
|
||||
uint16_t desc_count = 1;
|
||||
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (desc_count == 0) {
|
||||
break; // No more descriptors
|
||||
}
|
||||
|
||||
characteristic_resp.descriptors.emplace_back();
|
||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, ESPBTUUID::from_uuid(desc_result.uuid),
|
||||
use_efficient_uuids);
|
||||
descriptor_resp.handle = desc_result.handle;
|
||||
desc_offset++;
|
||||
}
|
||||
}
|
||||
} // end if (total_char_count > 0)
|
||||
|
||||
if (close_service_batch(resp, current_size, this->send_service_, this->connection_index_, this->address_str()) !=
|
||||
BatchClose::CONTINUE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Send the message with dynamically batched services; on a failed send,
|
||||
// rewind the cursor so the batch is retried instead of silently skipped.
|
||||
if (!api_conn->send_message(resp)) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Failed to send service batch, retrying", this->connection_index_, this->address_str_);
|
||||
this->send_service_ = batch_start;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action,
|
||||
type);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(),
|
||||
operation, handle, status);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) {
|
||||
if (err != ESP_OK) {
|
||||
this->log_connection_warning_(operation, err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
|
||||
return false;
|
||||
|
||||
switch (event) {
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
// Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
|
||||
// This prevents race condition where we mark slot as free before controller cleanup is complete
|
||||
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_,
|
||||
param->disconnect.reason);
|
||||
// Send disconnection notification but don't free the slot yet
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
this->reset_connection_(param->open.status);
|
||||
} else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
|
||||
this->proxy_->send_connections_free();
|
||||
}
|
||||
this->seen_mtu_or_services_ = false;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CFG_MTU_EVT:
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
if (!this->seen_mtu_or_services_) {
|
||||
// We don't know if we will get the MTU or the services first, so
|
||||
// only send the device connection true if we have already received
|
||||
// the services.
|
||||
this->seen_mtu_or_services_ = true;
|
||||
break;
|
||||
}
|
||||
this->proxy_->send_device_connection(this->address_, true, this->mtu_);
|
||||
this->proxy_->send_connections_free();
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_DESCR_EVT:
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (param->read.status != ESP_GATT_OK) {
|
||||
this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status);
|
||||
break;
|
||||
}
|
||||
auto *api_connection = this->proxy_->get_api_connection();
|
||||
if (api_connection == nullptr)
|
||||
break;
|
||||
api::BluetoothGATTReadResponse resp;
|
||||
resp.address = this->address_;
|
||||
resp.handle = param->read.handle;
|
||||
resp.set_data(param->read.value, param->read.value_len);
|
||||
api_connection->send_message(resp);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_CHAR_EVT:
|
||||
case ESP_GATTC_WRITE_DESCR_EVT: {
|
||||
if (param->write.status != ESP_GATT_OK) {
|
||||
this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status);
|
||||
break;
|
||||
}
|
||||
auto *api_connection = this->proxy_->get_api_connection();
|
||||
if (api_connection == nullptr)
|
||||
break;
|
||||
api::BluetoothGATTWriteResponse resp;
|
||||
resp.address = this->address_;
|
||||
resp.handle = param->write.handle;
|
||||
api_connection->send_message(resp);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
|
||||
if (param->unreg_for_notify.status != ESP_GATT_OK) {
|
||||
this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle,
|
||||
param->unreg_for_notify.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
auto *api_connection = this->proxy_->get_api_connection();
|
||||
if (api_connection == nullptr)
|
||||
break;
|
||||
api::BluetoothGATTNotifyResponse resp;
|
||||
resp.address = this->address_;
|
||||
resp.handle = param->unreg_for_notify.handle;
|
||||
api_connection->send_message(resp);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
if (param->reg_for_notify.status != ESP_GATT_OK) {
|
||||
this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle,
|
||||
param->reg_for_notify.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
auto *api_connection = this->proxy_->get_api_connection();
|
||||
if (api_connection == nullptr)
|
||||
break;
|
||||
api::BluetoothGATTNotifyResponse resp;
|
||||
resp.address = this->address_;
|
||||
resp.handle = param->reg_for_notify.handle;
|
||||
api_connection->send_message(resp);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_,
|
||||
param->notify.handle);
|
||||
auto *api_connection = this->proxy_->get_api_connection();
|
||||
if (api_connection == nullptr)
|
||||
break;
|
||||
api::BluetoothGATTNotifyDataResponse resp;
|
||||
resp.address = this->address_;
|
||||
resp.handle = param->notify.handle;
|
||||
resp.set_data(param->notify.value, param->notify.value_len);
|
||||
api_connection->send_message(resp);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
BLEClientBase::gap_event_handler(event, param);
|
||||
|
||||
switch (event) {
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
||||
if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0)
|
||||
break;
|
||||
if (param->ble_security.auth_cmpl.success) {
|
||||
this->proxy_->send_device_pairing(this->address_, true);
|
||||
} else {
|
||||
this->proxy_->send_device_pairing(this->address_, false, param->ble_security.auth_cmpl.fail_reason);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
|
||||
if (!this->connected()) {
|
||||
this->log_gatt_not_connected_("read", "characteristic");
|
||||
return GATT_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8_t *data, size_t length,
|
||||
bool response) {
|
||||
if (!this->connected()) {
|
||||
this->log_gatt_not_connected_("write", "characteristic");
|
||||
return GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||
// const_cast is safe here and was previously hidden by a C-style cast
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_write_char", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
|
||||
if (!this->connected()) {
|
||||
this->log_gatt_not_connected_("read", "descriptor");
|
||||
return GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
|
||||
}
|
||||
|
||||
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 GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||
// const_cast is safe here and was previously hidden by a C-style cast
|
||||
esp_err_t err = esp_ble_gattc_write_char_descr(
|
||||
this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) {
|
||||
if (!this->connected()) {
|
||||
this->log_gatt_not_connected_("notify", "characteristic");
|
||||
return GATT_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_, handle);
|
||||
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_, handle);
|
||||
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
|
||||
}
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -1,76 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/esp32_ble_client/ble_client_base.h"
|
||||
|
||||
#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:
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
// The proxy's connections never consume parsed ESPBTDevice objects.
|
||||
bool wants_parsed_advertisements() override { return false; }
|
||||
|
||||
esp_err_t read_characteristic(uint16_t handle);
|
||||
esp_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response);
|
||||
esp_err_t read_descriptor(uint16_t handle);
|
||||
esp_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response);
|
||||
|
||||
esp_err_t notify_characteristic(uint16_t handle, bool enable);
|
||||
|
||||
esp_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) {
|
||||
return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom");
|
||||
}
|
||||
|
||||
bool has_gatt_services() const { return this->service_count_ != 0; }
|
||||
|
||||
/// Start connecting: record the API address type and hand the client to the
|
||||
/// tracker's promote loop (it pauses the scan and opens the connection).
|
||||
void initiate_connection(uint8_t address_type) {
|
||||
this->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(address_type));
|
||||
this->set_state(esp32_ble_tracker::ClientState::DISCOVERED);
|
||||
}
|
||||
|
||||
void set_address(uint64_t address) override;
|
||||
|
||||
protected:
|
||||
friend class bluetooth_proxy::BluetoothProxy;
|
||||
|
||||
void on_disconnect_complete(esp_err_t reason) override;
|
||||
|
||||
void send_service_for_discovery_();
|
||||
void reset_connection_(esp_err_t reason);
|
||||
void log_connection_error_(const char *operation, esp_gatt_status_t status);
|
||||
void log_connection_warning_(const char *operation, esp_err_t err);
|
||||
void log_gatt_not_connected_(const char *action, const char *type);
|
||||
void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status);
|
||||
esp_err_t check_and_log_error_(const char *operation, esp_err_t err);
|
||||
|
||||
// Memory optimized layout for 32-bit systems
|
||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||
bluetooth_proxy::BluetoothProxy *proxy_;
|
||||
|
||||
// Group 2: 2-byte types
|
||||
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_connection
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -15,17 +15,18 @@
|
||||
#if defined(USE_RP2040_BLE)
|
||||
#include "bluetooth_connection_rp2.h"
|
||||
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::RP2GattClient
|
||||
#elif defined(USE_ESP32_BLE)
|
||||
#include "bluetooth_connection_bluedroid.h"
|
||||
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::BluedroidGattClient
|
||||
#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) {}
|
||||
void set_listener(ble_device_base::GattClientListener *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; }
|
||||
@@ -43,6 +44,7 @@ class StubGattBackend {
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
}
|
||||
ble_device_base::GattServiceTable get_service_table() { return {}; }
|
||||
void set_connection_type(ble_device_base::ConnectionType ct) {}
|
||||
void release_services() {}
|
||||
};
|
||||
|
||||
@@ -55,7 +57,7 @@ class StubGattBackend {
|
||||
namespace esphome::ble_device_base {
|
||||
|
||||
using BLEGattConnection = ESPHOME_BLE_GATT_CONNECTION_TYPE;
|
||||
static_assert(BLEGattConnectionContract<BLEGattConnection, bluetooth_connection::BluetoothConnection>,
|
||||
static_assert(BLEGattConnectionContract<BLEGattConnection>,
|
||||
"The build's GATT backend is missing part of the BLEGattConnection surface (ble_gatt_client.h)");
|
||||
#undef ESPHOME_BLE_GATT_CONNECTION_TYPE
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Hub-platform connection wrapper (USE_RP2 hub builds today).
|
||||
// The proxy's per-slot connection wrapper, shared by every platform.
|
||||
#include "bluetooth_connection_hub.h"
|
||||
|
||||
#if !defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||
@@ -27,7 +27,7 @@ void BluetoothConnection::set_address(uint64_t address) {
|
||||
}
|
||||
|
||||
void BluetoothConnection::start_connect_() {
|
||||
// No connect timeout here (esp32 parity): the client's own timeout or
|
||||
// No connect timeout here: the API client's own timeout or
|
||||
// the api-gone sweep drives disconnect().
|
||||
this->state_ = ClientState::CONNECTING;
|
||||
int err = this->backend_->connect(this->address_, this->remote_addr_type_);
|
||||
@@ -38,7 +38,7 @@ void BluetoothConnection::start_connect_() {
|
||||
}
|
||||
|
||||
void BluetoothConnection::disconnect() {
|
||||
// Idempotent like the esp32 class: the proxy's teardown loop calls this
|
||||
// Idempotent: the proxy's teardown loop calls this
|
||||
// every 100 ms while the API subscriber is gone, and a repeat call must not
|
||||
// reach the backend (whose busy error would free the slot mid-teardown).
|
||||
if (this->state_ == ClientState::IDLE || this->state_ == ClientState::DISCONNECTING) {
|
||||
@@ -65,10 +65,10 @@ void BluetoothConnection::disconnect() {
|
||||
}
|
||||
|
||||
void BluetoothConnection::check_disconnect_timeout_() {
|
||||
// Safety net mirroring the esp32 base class: if the backend's disconnect
|
||||
// completion is lost, force the slot free instead of leaking it.
|
||||
static constexpr uint32_t DISCONNECT_TIMEOUT_MS = 10000;
|
||||
if (this->state_ == ClientState::DISCONNECTING && millis() - this->disconnecting_started_ > DISCONNECT_TIMEOUT_MS) {
|
||||
// Safety net: if the backend's disconnect completion is lost (or a refusal
|
||||
// left the teardown unresolved), force the slot free instead of leaking it.
|
||||
// The caller already gates on DISCONNECTING.
|
||||
if (millis() - this->disconnecting_started_ > ble_device_base::GATT_DISCONNECT_TIMEOUT_MS) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Disconnect timeout, freeing slot", this->connection_index_, this->address_str_);
|
||||
this->reset_connection_(GATT_NOT_CONNECTED);
|
||||
}
|
||||
@@ -96,7 +96,7 @@ void BluetoothConnection::reset_connection_(conn_err_t reason) {
|
||||
this->proxy_->reset_connection_slot_(this, reason);
|
||||
}
|
||||
|
||||
// ---- backend event sink ----
|
||||
// ---- backend event listener ----
|
||||
|
||||
void BluetoothConnection::on_connection_state(bool connected, uint16_t mtu, int error) {
|
||||
if (connected && this->address_ == 0) {
|
||||
@@ -130,7 +130,10 @@ void BluetoothConnection::on_connection_state(bool connected, uint16_t mtu, int
|
||||
if (this->connection_type_ == ConnectionType::V3_WITH_CACHE) {
|
||||
// The API client has the services cached; never discover them. No
|
||||
// discovery phase needs the fast interval, so settle straight into the
|
||||
// shared steady-state parameters (same lifecycle place as esp32).
|
||||
// shared steady-state parameters. On esp32 the backend already set the
|
||||
// same values as prefer-params before opening, so this request is
|
||||
// usually redundant there - kept because rp2 has no prefer-params and
|
||||
// the explicit update is its only path to the steady-state interval.
|
||||
this->state_ = ClientState::ESTABLISHED;
|
||||
int param_err = this->backend_->update_connection_params(ble_device_base::MEDIUM_MIN_CONN_INTERVAL,
|
||||
ble_device_base::MEDIUM_MAX_CONN_INTERVAL, 0,
|
||||
@@ -433,4 +436,4 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // !USE_ESP32 && USE_BLE_GATT_CLIENT
|
||||
#endif // BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// Hub-platform BluetoothConnection: drives the build's GATT backend (the
|
||||
// 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.
|
||||
// the proxy's API messages. One wrapper for every platform; per-backend
|
||||
// differences live behind the alias and the streamer cut-through.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if !defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
|
||||
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
// 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_SERVES_PROXY
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_client_state.h"
|
||||
#include "bluetooth_connection_gatt_backend.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -25,7 +25,7 @@ namespace esphome::bluetooth_connection {
|
||||
using ClientState = ble_device_base::ClientState;
|
||||
using ConnectionType = ble_device_base::ConnectionType;
|
||||
|
||||
class BluetoothConnection final {
|
||||
class BluetoothConnection final : public ble_device_base::GattClientListener {
|
||||
public:
|
||||
/// Wire the platform backend. Called from codegen before setup.
|
||||
void set_backend(ble_device_base::BLEGattConnection *backend) {
|
||||
@@ -33,7 +33,7 @@ class BluetoothConnection final {
|
||||
backend->set_listener(this);
|
||||
}
|
||||
|
||||
// ---- proxy dispatch surface (mirrors the esp32 class) ----
|
||||
// ---- proxy dispatch surface ----
|
||||
conn_err_t read_characteristic(uint16_t handle);
|
||||
conn_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response);
|
||||
conn_err_t read_descriptor(uint16_t handle);
|
||||
@@ -52,10 +52,6 @@ class BluetoothConnection final {
|
||||
bool is_paired() const { return this->paired_; }
|
||||
void set_unpaired() { this->paired_ = false; }
|
||||
conn_err_t pair() { return this->backend_->pair(); }
|
||||
// A backend disconnect() is a single call that also cancels an in-progress
|
||||
// connect; there is no deferred-disconnect state to track.
|
||||
bool disconnect_pending() const { return false; }
|
||||
void cancel_pending_disconnect() {}
|
||||
|
||||
void set_address(uint64_t address);
|
||||
uint64_t get_address() const { return this->address_; }
|
||||
@@ -65,37 +61,58 @@ class BluetoothConnection final {
|
||||
ClientState state() const { return this->state_; }
|
||||
void set_state(ClientState st) { this->state_ = st; }
|
||||
bool connected() const { return this->state_ == ClientState::ESTABLISHED; }
|
||||
void set_connection_type(ConnectionType ct) { this->connection_type_ = ct; }
|
||||
void set_connection_type(ConnectionType ct) {
|
||||
this->connection_type_ = ct;
|
||||
// The bluedroid backend branches on the type itself (prefer-params and
|
||||
// the with-cache report at OPEN_EVT); the others ignore it.
|
||||
this->backend_->set_connection_type(ct);
|
||||
}
|
||||
// Latched at discovery completion rather than read from the backend table:
|
||||
// streaming frees the table, and this must stay true for the connection's
|
||||
// lifetime (esp32 parity — a repeat GetServices is silently ignored there,
|
||||
// never answered with an authoritative empty database).
|
||||
// lifetime (a repeat GetServices is silently ignored, never answered with
|
||||
// an authoritative empty database).
|
||||
bool has_gatt_services() const { return this->services_discovered_; }
|
||||
|
||||
/// Stream any pending service-discovery batch and police the disconnect
|
||||
/// safety timeout. Called from the proxy's loop — hub connections have no
|
||||
/// Component loop of their own (the esp32 class streams from its own
|
||||
/// loop() and has the same 10 s safety net in its base class).
|
||||
/// safety timeout. Called from the proxy's loop — the wrapper has no
|
||||
/// Component loop of its own.
|
||||
void process_pending_services() {
|
||||
if (this->send_service_ >= 0) {
|
||||
this->send_service_for_discovery_();
|
||||
this->stream_pending_(this->backend_);
|
||||
}
|
||||
// Inline state gate: this runs per loop iteration for every slot, and the
|
||||
// 10 s safety net only matters while DISCONNECTING.
|
||||
if (this->state_ == ClientState::DISCONNECTING) {
|
||||
this->check_disconnect_timeout_();
|
||||
}
|
||||
this->check_disconnect_timeout_();
|
||||
}
|
||||
|
||||
// ---- 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);
|
||||
// ---- backend event listener (called directly by the backend, main loop) ----
|
||||
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;
|
||||
|
||||
protected:
|
||||
friend class bluetooth_proxy::BluetoothProxy;
|
||||
// The Bluedroid backend streams services in place from its stack cache.
|
||||
friend class BluedroidGattClient;
|
||||
|
||||
void start_connect_();
|
||||
// A backend providing its own streamer (see the contract doc) builds the
|
||||
// response in place from its stack cache; the rest use the table streamer.
|
||||
// Template so the discarded branch is not odr-checked against backends
|
||||
// that lack the method.
|
||||
template<typename Backend> void stream_pending_(Backend *backend) {
|
||||
if constexpr (requires { backend->stream_service_batch(*this); }) {
|
||||
backend->stream_service_batch(*this);
|
||||
} else {
|
||||
this->send_service_for_discovery_();
|
||||
}
|
||||
}
|
||||
void send_service_for_discovery_();
|
||||
void check_disconnect_timeout_();
|
||||
void reset_connection_(conn_err_t reason);
|
||||
@@ -128,9 +145,6 @@ class BluetoothConnection final {
|
||||
bool services_discovered_{false};
|
||||
};
|
||||
|
||||
static_assert(ble_device_base::GattClientEventSinkContract<BluetoothConnection>,
|
||||
"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
|
||||
#endif // BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "bluetooth_connection_rp2.h"
|
||||
|
||||
#include "bluetooth_connection_hub.h"
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
#if defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
@@ -26,7 +25,6 @@ using ble_device_base::GATT_ERR_NO_MEMORY;
|
||||
// and keeps the scan inhibited, so the engine cancels after 20 s. The
|
||||
// disconnect timeout mirrors the esp32 CLOSE_EVT safety net.
|
||||
static constexpr uint32_t CONNECT_TIMEOUT_MS = 20000;
|
||||
static constexpr uint32_t DISCONNECT_TIMEOUT_MS = 10000;
|
||||
// Can-send windows normally open within a connection interval (tens of ms).
|
||||
static constexpr uint32_t WRITE_NO_RSP_TIMEOUT_MS = 500;
|
||||
|
||||
@@ -384,7 +382,7 @@ void RP2GattClient::loop() {
|
||||
|
||||
RP2GattNotifyEvent *notify;
|
||||
while ((notify = this->notify_queue_.pop()) != nullptr) {
|
||||
if (this->listener_ != nullptr && this->notify_subscribed_(notify->handle)) {
|
||||
if (this->notify_subscribed_(notify->handle)) {
|
||||
this->listener_->on_notify_data(notify->handle, notify->data, notify->len);
|
||||
}
|
||||
this->notify_pool_.release(notify);
|
||||
@@ -430,7 +428,7 @@ void RP2GattClient::loop() {
|
||||
}
|
||||
}
|
||||
} else if (this->state_ == EngineState::DISCONNECTING) {
|
||||
if (millis() - this->disconnecting_started_ > DISCONNECT_TIMEOUT_MS) {
|
||||
if (millis() - this->disconnecting_started_ > ble_device_base::GATT_DISCONNECT_TIMEOUT_MS) {
|
||||
ESP_LOGW(TAG, "Disconnect timeout, forcing idle");
|
||||
this->handle_disconnected_(HCI_REASON_CONNECTION_TIMEOUT);
|
||||
}
|
||||
@@ -448,9 +446,7 @@ void RP2GattClient::loop() {
|
||||
}
|
||||
if (timed_out) {
|
||||
ESP_LOGW(TAG, "Deferred write timeout, handle=0x%04x", this->op_handle_);
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->on_write_result(this->op_handle_, GATT_CLIENT_BUSY);
|
||||
}
|
||||
this->listener_->on_write_result(this->op_handle_, GATT_CLIENT_BUSY);
|
||||
}
|
||||
} else if (this->state_ == EngineState::IDLE || (this->state_ == EngineState::READY && !this->op_in_flight_() &&
|
||||
this->event_queue_.empty() && this->notify_queue_.empty())) {
|
||||
@@ -474,9 +470,7 @@ void RP2GattClient::handle_event_(const RP2GattEvent &event) {
|
||||
this->state_ = EngineState::READY;
|
||||
// Scanning resumes and runs alongside the established connection.
|
||||
this->release_scan_inhibit_();
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->on_connection_state(true, this->mtu_, 0);
|
||||
}
|
||||
this->listener_->on_connection_state(true, this->mtu_, 0);
|
||||
}
|
||||
break;
|
||||
case RP2GattEvent::QUERY_COMPLETE:
|
||||
@@ -486,9 +480,7 @@ void RP2GattClient::handle_event_(const RP2GattEvent &event) {
|
||||
this->finish_write_no_rsp_(event.status);
|
||||
break;
|
||||
case RP2GattEvent::PAIRING_RESULT:
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->on_pairing_result(event.status);
|
||||
}
|
||||
this->listener_->on_pairing_result(event.status);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -514,9 +506,7 @@ void RP2GattClient::finish_write_no_rsp_(uint8_t status) {
|
||||
return;
|
||||
}
|
||||
this->op_type_ = OpType::NONE;
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->on_write_result(this->op_handle_, status);
|
||||
}
|
||||
this->listener_->on_write_result(this->op_handle_, status);
|
||||
}
|
||||
|
||||
void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
|
||||
@@ -576,9 +566,7 @@ void RP2GattClient::fail_connection_(uint8_t reason) {
|
||||
this->cleanup_link_state_();
|
||||
this->release_scan_inhibit_();
|
||||
this->state_ = EngineState::IDLE;
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->on_connection_state(false, 0, reason);
|
||||
}
|
||||
this->listener_->on_connection_state(false, 0, reason);
|
||||
}
|
||||
|
||||
void RP2GattClient::cleanup_link_state_() {
|
||||
@@ -621,9 +609,6 @@ void RP2GattClient::handle_query_complete_(uint8_t att_status) {
|
||||
if (this->op_type_ != OpType::NONE && this->op_type_ != OpType::WRITE_CHAR_NO_RSP) {
|
||||
OpType op = this->op_type_;
|
||||
this->op_type_ = OpType::NONE;
|
||||
if (this->listener_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
switch (op) {
|
||||
case OpType::READ_CHAR:
|
||||
case OpType::READ_DESC:
|
||||
@@ -796,9 +781,7 @@ void RP2GattClient::finish_discovery_(int error) {
|
||||
if (error != 0) {
|
||||
this->release_services();
|
||||
}
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->on_service_discovery_done(error);
|
||||
}
|
||||
this->listener_->on_service_discovery_done(error);
|
||||
}
|
||||
|
||||
ble_device_base::GattServiceTable RP2GattClient::get_service_table() {
|
||||
@@ -990,7 +973,7 @@ int RP2GattClient::write_characteristic(uint16_t handle, const uint8_t *data, ui
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (status == 0 && this->listener_ != nullptr) {
|
||||
if (status == 0) {
|
||||
this->listener_->on_write_result(handle, 0);
|
||||
}
|
||||
return status;
|
||||
@@ -1092,9 +1075,7 @@ int RP2GattClient::notify_characteristic(uint16_t handle, bool enable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->on_notify_state(handle, enable, 0);
|
||||
}
|
||||
this->listener_->on_notify_state(handle, enable, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
|
||||
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
|
||||
@@ -80,7 +78,7 @@ class RP2GattClient final : public Component, public Parented<rp2040_ble::RP2040
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void set_listener(BluetoothConnection *listener) { this->listener_ = listener; }
|
||||
void set_listener(ble_device_base::GattClientListener *listener) { this->listener_ = listener; }
|
||||
|
||||
// ---- ble_device_base::BLEGattConnection contract ----
|
||||
int connect(uint64_t address, uint8_t addr_type);
|
||||
@@ -94,6 +92,8 @@ class RP2GattClient final : public Component, public Parented<rp2040_ble::RP2040
|
||||
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();
|
||||
// No connection-type branching on this backend.
|
||||
void set_connection_type(ble_device_base::ConnectionType ct) {}
|
||||
void release_services();
|
||||
|
||||
protected:
|
||||
@@ -150,7 +150,7 @@ class RP2GattClient final : public Component, public Parented<rp2040_ble::RP2040
|
||||
}
|
||||
|
||||
// Group 1: containers / large storage
|
||||
BluetoothConnection *listener_{nullptr};
|
||||
ble_device_base::GattClientListener *listener_{nullptr};
|
||||
ServiceArena *arena_{nullptr};
|
||||
esphome::LockFreeQueue<RP2GattEvent, RP2_GATT_EVENT_QUEUE_SIZE> event_queue_;
|
||||
esphome::EventPool<RP2GattEvent, RP2_GATT_EVENT_QUEUE_SIZE - 1> event_pool_;
|
||||
|
||||
@@ -4,17 +4,23 @@ import logging
|
||||
import esphome.codegen as cg
|
||||
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.const import (
|
||||
CONF_ACTIVE,
|
||||
CONF_ID,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RP2,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# The esp32 BLE stack (esp32_ble, esp32_ble_client, esp32_ble_tracker) is
|
||||
# imported lazily inside _esp32_config_schema()/_to_code_esp32(): importing
|
||||
# those modules registers esp32-only automations (ble.enable, ble.disable, ...)
|
||||
# as a side effect, and a module-scope import would leak them into every
|
||||
# platform's registry the moment a config declares `bluetooth_proxy:` —
|
||||
# degrading "Unable to find action" config errors into C++ compile failures.
|
||||
# The esp32 BLE stack (esp32_ble, esp32_ble_tracker) is imported lazily
|
||||
# inside _esp32_config_schema()/_to_code_esp32(): importing those modules
|
||||
# registers esp32-only automations (ble.enable, ble.disable, ...) as a side
|
||||
# effect, and a module-scope import would leak them into every platform's
|
||||
# registry the moment a config declares `bluetooth_proxy:` — degrading
|
||||
# "Unable to find action" config errors into C++ compile failures.
|
||||
|
||||
|
||||
def AUTO_LOAD(config: ConfigType | None = None) -> list[str]:
|
||||
@@ -27,7 +33,7 @@ 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 ["bluetooth_connection", "esp32_ble_client", "esp32_ble_tracker"]
|
||||
return ["bluetooth_connection", "esp32_ble_tracker"]
|
||||
if CORE.target_platform in _HUB_PLATFORMS:
|
||||
return ["ble_device_base", "bluetooth_connection"]
|
||||
# No target platform, or one this component does not support: tooling
|
||||
@@ -36,7 +42,6 @@ def AUTO_LOAD(config: ConfigType | None = None) -> list[str]:
|
||||
return [
|
||||
"ble_device_base",
|
||||
"bluetooth_connection",
|
||||
"esp32_ble_client",
|
||||
"esp32_ble_tracker",
|
||||
]
|
||||
|
||||
@@ -59,7 +64,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_CONNECTION_SLOTS = "connection_slots"
|
||||
CONF_CACHE_SERVICES = "cache_services"
|
||||
CONF_CONNECTIONS = "connections"
|
||||
CONF_BACKEND_ID = "backend_id"
|
||||
DEFAULT_CONNECTION_SLOTS = 3
|
||||
|
||||
bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy")
|
||||
@@ -86,12 +90,7 @@ def _esp32_config_schema() -> cv.All:
|
||||
f"update _IDF_MAX_CONNECTIONS in bluetooth_proxy/__init__.py"
|
||||
)
|
||||
|
||||
BluetoothConnection = bluetooth_connection.esp32_connection_class()
|
||||
CONNECTION_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BluetoothConnection),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
CONNECTION_SCHEMA = bluetooth_connection.hub_connection_schema(PLATFORM_ESP32)
|
||||
|
||||
def validate_connections(config):
|
||||
if CONF_CONNECTIONS in config:
|
||||
@@ -101,7 +100,7 @@ def _esp32_config_schema() -> 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
|
||||
)
|
||||
|
||||
@@ -154,22 +153,16 @@ def _rp2_config_schema() -> cv.All:
|
||||
"""Full proxy on the rp2 BLE hub: active connections through the BTstack
|
||||
GATT client backend in bluetooth_connection. The slot limit comes from the
|
||||
prebuilt BTstack library (one connection today); the code is built for N."""
|
||||
from esphome.components import rp2040_ble
|
||||
|
||||
connection_schema = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(bluetooth_connection.HubBluetoothConnection),
|
||||
cv.GenerateID(CONF_BACKEND_ID): cv.declare_id(
|
||||
bluetooth_connection.RP2GattClient
|
||||
),
|
||||
}
|
||||
)
|
||||
connection_schema = bluetooth_connection.hub_connection_schema(PLATFORM_RP2)
|
||||
|
||||
def populate_connections(config: ConfigType) -> ConfigType:
|
||||
# One wrapper + backend pair per slot, declared during validation so
|
||||
# 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: [
|
||||
@@ -182,11 +175,6 @@ def _rp2_config_schema() -> cv.All:
|
||||
cv.Schema(
|
||||
{
|
||||
**_COMMON_SCHEMA_KEYS,
|
||||
# The GATT backend drives the controller directly (connect, GATT
|
||||
# ops), not through the tracker hub.
|
||||
cv.GenerateID(rp2040_ble.CONF_RP2040_BLE_ID): cv.use_id(
|
||||
rp2040_ble.RP2040BLE
|
||||
),
|
||||
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_CONNECTION_SLOTS,
|
||||
@@ -212,25 +200,22 @@ def _rp2_config_schema() -> cv.All:
|
||||
return cv.All(schema, populate_connections)
|
||||
|
||||
|
||||
async def _rp2_connections_to_code(var: cg.MockObj, config: ConfigType) -> None:
|
||||
from esphome.components import rp2040_ble
|
||||
|
||||
# One wrapper + backend pair per slot (the esp32 arm's pattern).
|
||||
for connection_conf in config[CONF_CONNECTIONS]:
|
||||
ble_device_base.request_gatt_client()
|
||||
backend = cg.new_Pvariable(connection_conf[CONF_BACKEND_ID])
|
||||
await cg.register_component(backend, connection_conf)
|
||||
await cg.register_parented(backend, config[rp2040_ble.CONF_RP2040_BLE_ID])
|
||||
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, service_table=False
|
||||
)
|
||||
connection = cg.new_Pvariable(connection_conf[CONF_ID])
|
||||
cg.add(connection.set_backend(backend))
|
||||
cg.add(var.register_connection(connection))
|
||||
|
||||
|
||||
# Per-platform schema builders and connection codegen; every key of
|
||||
# bluetooth_connection.HUB_MAX_CONNECTIONS needs an entry in both (pinned by
|
||||
# tests/component_tests/bluetooth_proxy/).
|
||||
# Per-platform schema builders; every key of
|
||||
# bluetooth_connection.HUB_MAX_CONNECTIONS needs an entry here (pinned by
|
||||
# tests/component_tests/bluetooth_proxy/). Connection codegen is shared.
|
||||
_GATT_HUB_SCHEMAS = {PLATFORM_RP2: _rp2_config_schema}
|
||||
_GATT_HUB_TO_CODE = {PLATFORM_RP2: _rp2_connections_to_code}
|
||||
|
||||
|
||||
# Keys every platform arm declares identically; each arm spreads this dict so
|
||||
@@ -374,20 +359,18 @@ async def _to_code_esp32(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_active(config[CONF_ACTIVE]))
|
||||
# Advertisements and scanner state arrive through the hub callbacks
|
||||
# (installed in setup()); the tracker stays typed for scan-mode calls.
|
||||
tracker = await cg.get_variable(config[esp32_ble_tracker.CONF_ESP32_BLE_ID])
|
||||
cg.add(var.set_parent(tracker))
|
||||
cg.add(var.set_ble_hub(tracker))
|
||||
|
||||
# Compiles the scanner-state push slot into the tracker and the matching
|
||||
# registration into the proxy; the other hubs are polled instead.
|
||||
cg.add_define("USE_BLE_SCANNER_STATE_CALLBACK")
|
||||
|
||||
# Define max connections for protobuf fixed array
|
||||
connection_count = len(config.get(CONF_CONNECTIONS, []))
|
||||
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count)
|
||||
|
||||
for connection_conf in config.get(CONF_CONNECTIONS, []):
|
||||
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
|
||||
await cg.register_component(connection_var, connection_conf)
|
||||
cg.add(var.register_connection(connection_var))
|
||||
await esp32_ble_tracker.register_raw_client(connection_var, connection_conf)
|
||||
await _connections_to_code(var, config)
|
||||
|
||||
if config.get(CONF_CACHE_SERVICES):
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True)
|
||||
@@ -407,10 +390,7 @@ async def _to_code_ble_hub(config: ConfigType) -> None:
|
||||
# from the loop below (the define sizes fixed storage in the proxy).
|
||||
slots = len(config.get(CONF_CONNECTIONS, ()))
|
||||
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", slots)
|
||||
if not slots:
|
||||
return
|
||||
|
||||
await _GATT_HUB_TO_CODE[CORE.target_platform](var, config)
|
||||
await _connections_to_code(var, config)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
@@ -426,5 +406,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
|
||||
|
||||
cg.add_define("USE_BLUETOOTH_PROXY")
|
||||
# Compiles the scanner-state push slot into the hub (see ble_hub.h).
|
||||
cg.add_define("USE_BLE_SCANNER_STATE_CALLBACK")
|
||||
|
||||
@@ -51,7 +51,7 @@ bool BluetoothProxy::send_bluetooth_scanner_state_(ble_device_base::ScannerState
|
||||
return this->api_connection_->send_message(resp);
|
||||
}
|
||||
|
||||
#ifndef USE_ESP32
|
||||
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
void BluetoothProxy::send_polled_scanner_state_() {
|
||||
// One read feeds both the frame and the change detector; the detector only
|
||||
// advances if the frame was accepted, so a dropped send (WOULD_BLOCK on a
|
||||
@@ -62,7 +62,7 @@ void BluetoothProxy::send_polled_scanner_state_() {
|
||||
this->last_scan_running_ = running;
|
||||
}
|
||||
}
|
||||
#endif // !USE_ESP32
|
||||
#endif // !USE_BLE_SCANNER_STATE_CALLBACK
|
||||
|
||||
void BluetoothProxy::setup() {
|
||||
// BLUETOOTH_PROXY_MAX_CONNECTIONS is 0 on an advertisement-only proxy.
|
||||
@@ -75,9 +75,12 @@ void BluetoothProxy::setup() {
|
||||
this->hub_->set_raw_advertisement_callback({this, [](void *self, const ble_device_base::RawAdvertisement &adv) {
|
||||
static_cast<BluetoothProxy *>(self)->on_raw_advertisement_(adv);
|
||||
}});
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
// Only push hubs compile the slot; elsewhere loop() polls scan_running().
|
||||
this->hub_->set_scanner_state_callback({this, [](void *self, ble_device_base::ScannerState state) {
|
||||
static_cast<BluetoothProxy *>(self)->send_bluetooth_scanner_state_(state);
|
||||
}});
|
||||
#endif
|
||||
}
|
||||
|
||||
// The hub delivers raw advertisements on the ESPHome main loop.
|
||||
@@ -103,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));
|
||||
@@ -112,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);
|
||||
@@ -129,13 +132,6 @@ void BluetoothProxy::log_advertisement_flush_() {
|
||||
}
|
||||
|
||||
void BluetoothProxy::dump_config() {
|
||||
#ifdef USE_ESP32
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Bluetooth Proxy:\n"
|
||||
" Active: %s\n"
|
||||
" Connections: %d",
|
||||
YESNO(this->active_), this->connection_count_);
|
||||
#else
|
||||
// Print configured facts. dump_config runs right after setup, before the
|
||||
// radio is up, so live scan state would always read "stopped" here — the
|
||||
// loop's BluetoothScannerStateResponse carries the changing value instead.
|
||||
@@ -143,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"
|
||||
@@ -159,33 +155,9 @@ void BluetoothProxy::dump_config() {
|
||||
" Adapter MAC: %s",
|
||||
scan_mode, mac_out);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
void BluetoothProxy::loop() {
|
||||
// Run advertisement flush / connection cleanup every 100ms
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_advertisement_flush_time_ < 100)
|
||||
return;
|
||||
this->last_advertisement_flush_time_ = now;
|
||||
|
||||
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
|
||||
this->flush_pending_advertisements_();
|
||||
return;
|
||||
}
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||||
connection->disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
#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) {
|
||||
@@ -197,11 +169,8 @@ void BluetoothProxy::register_connection([[maybe_unused]] BluetoothConnection *c
|
||||
ESP_LOGE(TAG, "Connection registry full, dropping registration");
|
||||
return;
|
||||
}
|
||||
#ifndef USE_ESP32
|
||||
// esp32 assigns connection_index_ in BLEClientBase::setup(); the hub
|
||||
// class has no Component lifecycle, so the index is assigned here.
|
||||
// The hub wrapper has no Component lifecycle, so the index is assigned here.
|
||||
connection->connection_index_ = this->connection_count_;
|
||||
#endif
|
||||
this->connections_[this->connection_count_++] = connection;
|
||||
connection->proxy_ = this;
|
||||
#endif
|
||||
@@ -271,16 +240,9 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
this->send_device_connection(msg.address, true);
|
||||
this->send_connections_free();
|
||||
return;
|
||||
} else if (connection->state() == ClientState::CONNECTING) {
|
||||
if (connection->disconnect_pending()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
|
||||
connection->get_connection_index(), connection->address_str());
|
||||
connection->cancel_pending_disconnect();
|
||||
return;
|
||||
}
|
||||
this->log_connection_request_ignored_(connection, connection->state());
|
||||
return;
|
||||
} else if (connection->state() != ClientState::INIT) {
|
||||
// Covers CONNECTING too: a repeat request during a connect attempt is
|
||||
// ignored the same way.
|
||||
this->log_connection_request_ignored_(connection, connection->state());
|
||||
return;
|
||||
}
|
||||
@@ -312,7 +274,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
break;
|
||||
}
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: {
|
||||
// Both connection classes expose the same pairing surface; success is
|
||||
// The connection wrapper exposes the pairing surface; success is
|
||||
// reported when the platform's pairing completion arrives.
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection != nullptr) {
|
||||
@@ -464,28 +426,52 @@ 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
|
||||
|
||||
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
|
||||
if (this->parent_()->get_scan_active() == active) {
|
||||
// esp32 only: BLEHub is the concrete tracker here, so these calls reach
|
||||
// tracker-native methods beyond the neutral contract.
|
||||
if (this->hub_->get_scan_active() == active) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
|
||||
this->parent_()->set_scan_active(active);
|
||||
this->parent_()->stop_scan();
|
||||
this->parent_()->set_scan_continuous(
|
||||
this->hub_->set_scan_active(active);
|
||||
this->hub_->stop_scan();
|
||||
this->hub_->set_scan_continuous(
|
||||
true); // Set this to true to automatically start scanning again when it has cleaned up.
|
||||
}
|
||||
|
||||
#else // !USE_ESP32
|
||||
|
||||
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
|
||||
if (this->hub_->scan_active() != active) {
|
||||
ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
|
||||
if (!this->hub_->request_scan_mode(active)) {
|
||||
// Passive-only controller asked for active scanning; the state report
|
||||
// below carries the real, unchanged mode so the subscriber does not
|
||||
// assume the change happened.
|
||||
ESP_LOGW(TAG, "Scanner mode %s not supported by this tracker", active ? "active" : "passive");
|
||||
}
|
||||
}
|
||||
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
if (this->api_connection_ != nullptr) {
|
||||
// Reports the mode change; the sender also refreshes last_scan_running_, so
|
||||
// a failed restart (scan_running_ dropped by the tracker) is not reported
|
||||
// again by loop() on the next tick. A push hub reports the restart's
|
||||
// transitions (mode rides along) instead.
|
||||
this->send_polled_scanner_state_();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
void BluetoothProxy::loop() {
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
// Stream pending service-discovery batches every iteration (esp32 parity:
|
||||
// its connections stream from their own per-iteration Component loop).
|
||||
// send_service_for_discovery_() handles a vanished API connection itself.
|
||||
#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++) {
|
||||
this->connections_[i]->process_pending_services();
|
||||
}
|
||||
@@ -498,9 +484,9 @@ 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 backend is a no-op).
|
||||
// (disconnect() on an already-disconnecting slot is a no-op).
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() != 0) {
|
||||
@@ -511,16 +497,18 @@ void BluetoothProxy::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
// This hub doesn't push scanner-state transitions; poll and report on
|
||||
// change. A hub gaining push must also refresh last_scan_running_ here.
|
||||
// change. A hub gaining push emits the define and drops this poll.
|
||||
if (this->hub_->scan_running() != this->last_scan_running_) {
|
||||
this->send_polled_scanner_state_();
|
||||
}
|
||||
#endif
|
||||
|
||||
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
|
||||
@@ -543,12 +531,18 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR:
|
||||
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, GATT_NOT_CONNECTED);
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: {
|
||||
// Address-scoped maintenance needs no connection slot: real on esp32
|
||||
// (Bluedroid bond table), the stub elsewhere keeps the old error reply.
|
||||
conn_err_t ret = bluetooth_connection::unpair_device(msg.address);
|
||||
this->send_device_unpairing(msg.address, ret == CONN_OK, ret);
|
||||
break;
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE:
|
||||
this->send_device_clear_cache(msg.address, false, GATT_NOT_CONNECTED);
|
||||
}
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: {
|
||||
conn_err_t ret = bluetooth_connection::clear_gatt_cache(msg.address);
|
||||
this->send_device_clear_cache(msg.address, ret == CONN_OK, ret);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,27 +580,7 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn
|
||||
this->api_connection_->send_message(resp);
|
||||
}
|
||||
|
||||
#endif // !BLUETOOTH_CONNECTION_HAS_GATT
|
||||
|
||||
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
|
||||
if (this->hub_->scan_active() != active) {
|
||||
ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
|
||||
if (!this->hub_->request_scan_mode(active)) {
|
||||
// Passive-only controller asked for active scanning; the state report
|
||||
// below carries the real, unchanged mode so the subscriber does not
|
||||
// assume the change happened.
|
||||
ESP_LOGW(TAG, "Scanner mode %s not supported by this tracker", active ? "active" : "passive");
|
||||
}
|
||||
}
|
||||
if (this->api_connection_ != nullptr) {
|
||||
// Reports the mode change; the sender also refreshes last_scan_running_, so
|
||||
// a failed restart (scan_running_ dropped by the tracker) is not reported
|
||||
// again by loop() on the next tick.
|
||||
this->send_polled_scanner_state_();
|
||||
}
|
||||
}
|
||||
|
||||
#endif // USE_ESP32
|
||||
#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) {
|
||||
@@ -622,8 +596,9 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
|
||||
this->api_connection_->get_peername_to(old_peername));
|
||||
}
|
||||
this->api_connection_ = api_connection;
|
||||
#ifdef USE_ESP32
|
||||
this->send_bluetooth_scanner_state_(this->parent_()->get_scanner_state());
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
// get_scanner_state() is part of the push-hub surface (see BLEHubContract).
|
||||
this->send_bluetooth_scanner_state_(this->hub_->get_scanner_state());
|
||||
#else
|
||||
this->send_polled_scanner_state_();
|
||||
#endif
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/api/api_connection.h"
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
@@ -15,17 +13,9 @@
|
||||
|
||||
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/ble_device_base/ble_hub_impl.h"
|
||||
|
||||
#include "esphome/components/bluetooth_connection/bluetooth_connection_esp32.h"
|
||||
|
||||
#else
|
||||
#include "esphome/components/ble_device_base/ble_hub.h"
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
#include "esphome/components/bluetooth_connection/bluetooth_connection_hub.h"
|
||||
#endif
|
||||
#endif // USE_ESP32
|
||||
|
||||
namespace esphome::bluetooth_proxy {
|
||||
|
||||
@@ -33,11 +23,10 @@ namespace esphome::bluetooth_proxy {
|
||||
// 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 BLUETOOTH_CONNECTION_HAS_GATT
|
||||
#ifdef BLUETOOTH_CONNECTION_SERVES_PROXY
|
||||
using BluetoothConnection = bluetooth_connection::BluetoothConnection;
|
||||
using ClientState = ble_device_base::ClientState;
|
||||
#endif
|
||||
@@ -69,26 +58,21 @@ 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
|
||||
public:
|
||||
BluetoothProxy();
|
||||
#ifdef USE_ESP32
|
||||
// Advertisements arrive through the hub's raw callback; parent_() below
|
||||
// recovers the tracker type for the esp32-only scan-mode calls.
|
||||
void set_parent(esp32_ble_tracker::ESP32BLETracker *parent) { this->hub_ = parent; }
|
||||
#endif // USE_ESP32
|
||||
void set_ble_hub(ble_device_base::BLEHub *hub) { this->hub_ = hub; }
|
||||
void dump_config() override;
|
||||
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
|
||||
void set_ble_hub(ble_device_base::BLEHub *hub) { this->hub_ = hub; }
|
||||
// Run after the hub's setup() (the trackers use AFTER_WIFI): setup() below
|
||||
// snapshots scan_active()/scan_running() and installs the raw callback, and
|
||||
// the BLEHub contract does not promise those are settled any earlier than
|
||||
@@ -152,7 +136,7 @@ class BluetoothProxy final : public Component {
|
||||
// scan_mode_switch is the capability bit for exactly that (#18079) —
|
||||
// active_scan alone is not enough, a hub may support active scanning yet
|
||||
// refuse the runtime switch.
|
||||
if (this->hub_->get_capabilities().scan_mode_switch) {
|
||||
if (ble_device_base::BLEHub::get_capabilities().scan_mode_switch) {
|
||||
flags |= BluetoothProxyFeature::FEATURE_STATE_AND_MODE;
|
||||
}
|
||||
#endif
|
||||
@@ -188,7 +172,7 @@ class BluetoothProxy final : public Component {
|
||||
|
||||
protected:
|
||||
bool send_bluetooth_scanner_state_(ble_device_base::ScannerState state);
|
||||
#ifndef USE_ESP32
|
||||
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
void send_polled_scanner_state_();
|
||||
#endif
|
||||
void on_raw_advertisement_(const ble_device_base::RawAdvertisement &raw);
|
||||
@@ -205,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);
|
||||
@@ -213,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().
|
||||
@@ -253,18 +237,11 @@ 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<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
|
||||
#endif
|
||||
ble_device_base::BLEHub *hub_{nullptr};
|
||||
#ifdef USE_ESP32
|
||||
// set_parent() is the only writer of hub_ on esp32, so the downcast is
|
||||
// exact; ESP32BLETracker derives from BLEHub non-virtually.
|
||||
esp32_ble_tracker::ESP32BLETracker *parent_() {
|
||||
return static_cast<esp32_ble_tracker::ESP32BLETracker *>(this->hub_);
|
||||
}
|
||||
#endif
|
||||
|
||||
// BLE advertisement batching
|
||||
api::BluetoothLERawAdvertisementsResponse response_;
|
||||
@@ -279,7 +256,7 @@ class BluetoothProxy final : public Component {
|
||||
bool active_;
|
||||
uint8_t connection_count_{0};
|
||||
bool configured_scan_active_{false}; // Configured scan mode from YAML
|
||||
#ifndef USE_ESP32
|
||||
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
bool last_scan_running_{false}; // Last scanner state reported to the subscriber
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -205,6 +205,9 @@ async def to_code(config):
|
||||
# available on esp32 (sensors with irk: worked without opting in).
|
||||
ble_device_base.request_irk_support()
|
||||
|
||||
# Selects the BLEHub alias arm in ble_device_base/ble_hub_impl.h.
|
||||
cg.add_define("USE_ESP32_BLE_TRACKER")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -161,7 +161,6 @@ class ESPBTClient : public ESPBTDeviceListener {
|
||||
};
|
||||
|
||||
class ESP32BLETracker final : public Component,
|
||||
public ble_device_base::BLEHub,
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
public ota::OTAGlobalStateListener,
|
||||
#endif
|
||||
@@ -186,19 +185,27 @@ class ESP32BLETracker final : public Component,
|
||||
void register_client(ESPBTClient *client);
|
||||
|
||||
// ---- ble_device_base::BLEHub (the platform-neutral tracker contract) ----
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) override;
|
||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override {
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener);
|
||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) {
|
||||
this->raw_advertisement_callback_ = callback;
|
||||
}
|
||||
ble_device_base::HubCapabilities get_capabilities() const override {
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
void set_scanner_state_callback(ble_device_base::ScannerStateCallback callback) {
|
||||
this->scanner_state_callback_ = callback;
|
||||
}
|
||||
#endif
|
||||
static constexpr ble_device_base::HubCapabilities get_capabilities() {
|
||||
// scan_mode_switch is false: the mode is driven through this tracker's own
|
||||
// API (set_scan_active + restart), not the neutral request_scan_mode().
|
||||
return {/* active_scan = */ true, /* merges_scan_response = */ true, /* gatt = */ true,
|
||||
/* scan_mode_switch = */ false};
|
||||
}
|
||||
void get_adapter_mac(uint8_t out[6]) override { this->parent_->get_mac_msb_first(out); }
|
||||
bool scan_running() override { return this->scanner_state_ == ScannerState::RUNNING; }
|
||||
bool scan_active() override { return this->scan_active_; }
|
||||
void get_adapter_mac(uint8_t out[6]) { this->parent_->get_mac_msb_first(out); }
|
||||
bool scan_running() { return this->scanner_state_ == ScannerState::RUNNING; }
|
||||
bool scan_active() { return this->scan_active_; }
|
||||
// The mode is driven through this tracker's own API (see get_capabilities);
|
||||
// the neutral request refuses without changing any state.
|
||||
bool request_scan_mode(bool active) { return false; }
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
void print_bt_device_info(const ESPBTDevice &device);
|
||||
@@ -288,6 +295,9 @@ class ESP32BLETracker final : public Component,
|
||||
StaticVector<ble_device_base::ESPBTDeviceListener *, ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT> neutral_listeners_;
|
||||
#endif
|
||||
ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{};
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
ble_device_base::ScannerStateCallback scanner_state_callback_{};
|
||||
#endif
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
/// Per-period "Found device" DEBUG log with MAC dedup (shared ble_device_base impl)
|
||||
ble_device_base::DiscoveredDeviceLog discovered_log_;
|
||||
|
||||
@@ -127,6 +127,9 @@ async def stop_scan_action_to_code(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# Selects the BLEHub alias arm in ble_device_base/ble_hub_impl.h.
|
||||
cg.add_define("USE_LN882H_BLE_TRACKER")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ namespace esphome::ln882h_ble_tracker {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class LN882HBLETracker : public Component,
|
||||
public ble_device_base::BLEHub,
|
||||
public Parented<ln882h_ble::LN882HBLE>,
|
||||
public ln882h_ble::BLEScanListener
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
@@ -75,15 +74,15 @@ class LN882HBLETracker : public Component,
|
||||
void stop_scan();
|
||||
|
||||
// ---- ble_device_base::BLEHub contract ----
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) override {
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) {
|
||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||
this->listeners_.push_back(listener);
|
||||
#endif
|
||||
}
|
||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override {
|
||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) {
|
||||
this->raw_advertisement_callback_ = callback;
|
||||
}
|
||||
ble_device_base::HubCapabilities get_capabilities() const override {
|
||||
static constexpr ble_device_base::HubCapabilities get_capabilities() {
|
||||
// The LN882H controller supports active scanning; adv + scan response arrive
|
||||
// as separate reports and are merged by this tracker (Bluedroid semantics).
|
||||
// The SDK's GATT client is not exposed.
|
||||
@@ -92,15 +91,15 @@ class LN882HBLETracker : public Component,
|
||||
}
|
||||
// The controller stores the address LSB-first (BLE convention); the contract
|
||||
// wants printable (MSB-first) order.
|
||||
void get_adapter_mac(uint8_t out[6]) override {
|
||||
void get_adapter_mac(uint8_t out[6]) {
|
||||
uint8_t mac[6];
|
||||
this->parent_->get_mac_lsb_first(mac);
|
||||
for (int i = 0; i < 6; i++)
|
||||
out[i] = mac[5 - i];
|
||||
}
|
||||
bool scan_running() override { return this->scan_running_; }
|
||||
bool scan_active() override { return this->scan_active_; }
|
||||
bool request_scan_mode(bool active) override;
|
||||
bool scan_running() { return this->scan_running_; }
|
||||
bool scan_active() { return this->scan_active_; }
|
||||
bool request_scan_mode(bool active);
|
||||
|
||||
// ---- ln882h_ble::BLEScanListener ----
|
||||
// Delivered by the controller's loop() on the ESPHome main task — the
|
||||
|
||||
@@ -55,6 +55,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# Selects the BLEHub alias arm in ble_device_base/ble_hub_impl.h.
|
||||
cg.add_define("USE_RP2_BLE_TRACKER")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
namespace esphome::rp2_ble_tracker {
|
||||
|
||||
class RP2BLETracker : public Component,
|
||||
public ble_device_base::BLEHub,
|
||||
public rp2040_ble::BLEScanListener,
|
||||
public Parented<rp2040_ble::RP2040BLE>
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
@@ -51,15 +50,15 @@ class RP2BLETracker : public Component,
|
||||
void stop_scan();
|
||||
|
||||
// ---- ble_device_base::BLEHub contract ----
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) override {
|
||||
void register_listener(ble_device_base::ESPBTDeviceListener *listener) {
|
||||
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
|
||||
this->listeners_.push_back(listener);
|
||||
#endif
|
||||
}
|
||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override {
|
||||
void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) {
|
||||
this->raw_advertisement_callback_ = callback;
|
||||
}
|
||||
ble_device_base::HubCapabilities get_capabilities() const override {
|
||||
static constexpr ble_device_base::HubCapabilities get_capabilities() {
|
||||
// BTstack delivers scan responses as separate advertisement reports rather
|
||||
// than merging them into the advertisement — consumers relying on
|
||||
// scan-response fields (device names) get them only where the receiver
|
||||
@@ -74,10 +73,10 @@ class RP2BLETracker : public Component,
|
||||
}
|
||||
// The controller stores the address in printable (MSB-first) order, which is
|
||||
// exactly what the contract wants.
|
||||
void get_adapter_mac(uint8_t out[6]) override { this->parent_->get_mac_msb_first(out); }
|
||||
bool scan_running() override { return this->scan_running_; }
|
||||
bool scan_active() override { return this->scan_active_; }
|
||||
bool request_scan_mode(bool active) override;
|
||||
void get_adapter_mac(uint8_t out[6]) { this->parent_->get_mac_msb_first(out); }
|
||||
bool scan_running() { return this->scan_running_; }
|
||||
bool scan_active() { return this->scan_active_; }
|
||||
bool request_scan_mode(bool active);
|
||||
|
||||
// ---- rp2040_ble::BLEScanListener ----
|
||||
// Delivered by the controller's loop() on the ESPHome main loop — the
|
||||
|
||||
+17
-4
@@ -252,12 +252,12 @@
|
||||
// platforms whose API/network types the proxy header cannot assume.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_RP2)
|
||||
#define USE_BLUETOOTH_PROXY
|
||||
#define USE_BLE_SCANNER_STATE_CALLBACK
|
||||
// Mirror the codegen values per platform: _to_code_esp32() emits the connection
|
||||
// count (default 3), _to_code_ble_hub() emits the slot count (1 on rp2, 0 on
|
||||
// advertisement-only hubs) — so static analysis checks the same
|
||||
// std::array<uint64_t, N> instantiation a real build produces.
|
||||
// count (default 3) and the scanner-state push slot, _to_code_ble_hub() emits
|
||||
// the slot count (1 on rp2, 0 on advertisement-only hubs) — so static analysis
|
||||
// checks the same instantiations a real build produces.
|
||||
#ifdef USE_ESP32
|
||||
#define USE_BLE_SCANNER_STATE_CALLBACK
|
||||
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3
|
||||
#elif defined(USE_RP2)
|
||||
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 1
|
||||
@@ -305,6 +305,10 @@
|
||||
#define USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE
|
||||
#define USE_ESP32_BLE_SERVER_ON_CONNECT
|
||||
#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
|
||||
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
|
||||
@@ -465,6 +469,7 @@
|
||||
#define USE_LOGGER_USB_CDC
|
||||
#define USE_SOCKET_IMPL_LWIP_TCP
|
||||
#define USE_RP2040_BLE
|
||||
#define USE_RP2_BLE_TRACKER
|
||||
#define RP2040_BLE_SCAN_LISTENER_COUNT 1
|
||||
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
|
||||
#define USE_BLE_GATT_CLIENT
|
||||
@@ -489,6 +494,14 @@
|
||||
#define BK72XX_BLE_SCAN_LISTENER_COUNT 1
|
||||
#define USE_LN882H_BLE
|
||||
#define LN882H_BLE_SCAN_LISTENER_COUNT 1
|
||||
// One tracker arm per build: ln882x gets its real hub; bk72xx also stands in
|
||||
// for hub-less LibreTiny chips (rtl87xx) so bluetooth_proxy.h has a BLEHub
|
||||
// to parse against.
|
||||
#ifdef USE_LN882X
|
||||
#define USE_LN882H_BLE_TRACKER
|
||||
#else
|
||||
#define USE_BK72XX_BLE_TRACKER
|
||||
#endif
|
||||
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
|
||||
#define USE_CAPTIVE_PORTAL
|
||||
#define USE_WIFI_SCAN_RESULTS_LOCK
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
esphome:
|
||||
name: slotcount-ln882h-tracker
|
||||
|
||||
ln882x:
|
||||
board: generic-ln882h
|
||||
|
||||
ln882h_ble_tracker:
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for the BLE hub provider registry and the missing-hub diagnostics."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Callable, Generator
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
|
||||
@@ -202,3 +202,30 @@ def test_add_service_uuid_dispatches_by_width(monkeypatch: pytest.MonkeyPatch) -
|
||||
assert "0x00,0xff,0xee,0xdd" in emitted[2]
|
||||
with pytest.raises(ValueError, match="Unsupported UUID format"):
|
||||
ble_device_base.add_service_uuid(var, "123")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_name", "define"),
|
||||
[
|
||||
("esp32_tracker_only.yaml", "USE_ESP32_BLE_TRACKER"),
|
||||
("rp2_tracker.yaml", "USE_RP2_BLE_TRACKER"),
|
||||
("bk72xx_tracker.yaml", "USE_BK72XX_BLE_TRACKER"),
|
||||
("ln882h_tracker.yaml", "USE_LN882H_BLE_TRACKER"),
|
||||
],
|
||||
)
|
||||
def test_every_tracker_emits_its_alias_define(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_name: str,
|
||||
define: str,
|
||||
) -> None:
|
||||
"""Each tracker's codegen must emit its USE_*_BLE_TRACKER define - the
|
||||
ble_hub_impl.h alias ladder selects on it. Checked through real codegen
|
||||
(the other two legs of the invariant, the ladder arm and the defines.h
|
||||
mirror, are compile-enforced: a missing arm fails any build containing a
|
||||
BLEHub consumer - today bluetooth_proxy, which CI compiles or tidy-parses
|
||||
on every tracker platform - and clang-tidy compiles each arm's
|
||||
static_assert)."""
|
||||
generate_main(component_config_path(config_name))
|
||||
|
||||
assert define in {d.name for d in CORE.defines}, f"{define} not emitted by codegen"
|
||||
|
||||
@@ -109,6 +109,8 @@ def test_esp32_bluetooth_proxy_requests_client_slots_only(
|
||||
generate_main(component_config_path("esp32_bluetooth_proxy.yaml"))
|
||||
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") is None
|
||||
assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") == "3"
|
||||
# One neutral GATT backend slot per connection (the hub-model flip).
|
||||
assert get_define_value("ESPHOME_BLE_GATT_CLIENT_COUNT") == "3"
|
||||
|
||||
|
||||
def test_counts_reset_between_compiles(
|
||||
|
||||
@@ -15,6 +15,13 @@ import voluptuous as vol
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.bluetooth_proxy import CONFIG_SCHEMA, _esp32_config_schema
|
||||
|
||||
|
||||
def _esp32_schema_keys() -> dict[str, object]:
|
||||
# The builder names its platform explicitly, so no CORE state is needed
|
||||
# (this also mirrors how the language-schema dumper calls it).
|
||||
return _keys(_schema_of(_esp32_config_schema()))
|
||||
|
||||
|
||||
# esp32-schema keys with no place in the outer schema: COMPONENT_SCHEMA
|
||||
# plumbing (derived, so a future core key does not fail this component's test),
|
||||
# generated IDs (not user-walkable options), and connections (must validate
|
||||
@@ -38,7 +45,7 @@ def _keys(schema: vol.Schema) -> dict[str, object]:
|
||||
|
||||
def test_outer_scalar_keys_exist_in_esp32_schema() -> None:
|
||||
outer = _keys(_schema_of(CONFIG_SCHEMA))
|
||||
esp32 = _keys(_schema_of(_esp32_config_schema()))
|
||||
esp32 = _esp32_schema_keys()
|
||||
missing = set(outer) - set(esp32)
|
||||
assert not missing, (
|
||||
f"outer CONFIG_SCHEMA declares {sorted(missing)} which the esp32 schema "
|
||||
@@ -51,7 +58,7 @@ def test_esp32_scalars_all_walkable() -> None:
|
||||
"""Every non-generated esp32 scalar option must appear in the outer schema
|
||||
(connections is deliberately excluded — it must validate exactly once)."""
|
||||
outer = _keys(_schema_of(CONFIG_SCHEMA))
|
||||
esp32 = _keys(_schema_of(_esp32_config_schema()))
|
||||
esp32 = _esp32_schema_keys()
|
||||
scalar = {
|
||||
name
|
||||
for name, key in esp32.items()
|
||||
|
||||
@@ -177,28 +177,35 @@ def test_rp2_rejects_esp32_only_keys_by_name(
|
||||
bluetooth_proxy.CONFIG_SCHEMA({"connections": [{}]})
|
||||
|
||||
|
||||
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).
|
||||
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 "esp32_ble_client" in bluetooth_connection.AUTO_LOAD()
|
||||
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"]
|
||||
# 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"]
|
||||
assert bluetooth_connection.AUTO_LOAD() == [
|
||||
"ble_device_base",
|
||||
"esp32_ble_tracker",
|
||||
"rp2040_ble",
|
||||
]
|
||||
|
||||
|
||||
def test_every_registered_hub_platform_has_a_schema_arm() -> None:
|
||||
# A platform added to HUB_MAX_CONNECTIONS without a schema builder,
|
||||
# codegen arm, or _HUB_PLATFORMS entry would only fail when a config for
|
||||
# it is validated (or not even then); pin all three couplings here.
|
||||
# A platform added to HUB_MAX_CONNECTIONS without a schema builder or
|
||||
# _HUB_PLATFORMS entry would only fail when a config for it is validated
|
||||
# (or not even then); pin both couplings here. Connection codegen is
|
||||
# shared (bluetooth_connection.new_gatt_backend), so it needs no arm.
|
||||
registered = set(bluetooth_connection.HUB_MAX_CONNECTIONS)
|
||||
assert registered <= set(bluetooth_proxy._GATT_HUB_SCHEMAS)
|
||||
assert registered <= set(bluetooth_proxy._GATT_HUB_TO_CODE)
|
||||
assert registered <= set(bluetooth_proxy._HUB_PLATFORMS)
|
||||
# Hub platforms must also be in the backend registry the shared consumer
|
||||
# helpers dispatch on.
|
||||
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())
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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.
|
||||
// events through the GattClientListener interface the way a real backend does.
|
||||
#define USE_BLE_GATT_CLIENT
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
@@ -11,37 +11,36 @@
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
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) {}
|
||||
// Overrides only what it records; the interface's defaults cover the rest.
|
||||
class RecordingListener : public GattClientListener {
|
||||
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_write_result(uint16_t handle, int error) override { this->write_handle_ = handle; }
|
||||
|
||||
bool connected_{false};
|
||||
int discovery_error_{0};
|
||||
uint16_t write_handle_{0};
|
||||
};
|
||||
|
||||
static_assert(GattClientEventSinkContract<RecordingSink>, "the recording sink must cover the full event-sink surface");
|
||||
|
||||
class MinimalConnection {
|
||||
public:
|
||||
void set_listener(RecordingSink *listener) { this->listener_ = listener; }
|
||||
void set_listener(GattClientListener *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);
|
||||
this->listener_->on_connection_state(true, 517, 0);
|
||||
return 0;
|
||||
}
|
||||
int disconnect() { return 0; }
|
||||
int discover_services() {
|
||||
if (this->listener_ != nullptr)
|
||||
this->listener_->on_service_discovery_done(0);
|
||||
this->listener_->on_service_discovery_done(0);
|
||||
return 0;
|
||||
}
|
||||
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 write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
|
||||
this->listener_->on_write_result(handle, 0);
|
||||
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; }
|
||||
@@ -51,23 +50,26 @@ class MinimalConnection {
|
||||
}
|
||||
GattServiceTable get_service_table() { return {}; }
|
||||
void release_services() {}
|
||||
void set_connection_type(ConnectionType ct) {}
|
||||
|
||||
protected:
|
||||
RecordingSink *listener_{nullptr};
|
||||
GattClientListener *listener_{nullptr};
|
||||
};
|
||||
|
||||
static_assert(BLEGattConnectionContract<MinimalConnection, RecordingSink>,
|
||||
static_assert(BLEGattConnectionContract<MinimalConnection>,
|
||||
"a minimal backend must satisfy the contract the alias asserts");
|
||||
|
||||
TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) {
|
||||
MinimalConnection connection;
|
||||
RecordingSink listener;
|
||||
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);
|
||||
EXPECT_EQ(connection.write_characteristic(7, nullptr, 0, true), 0);
|
||||
EXPECT_EQ(listener.write_handle_, 7);
|
||||
|
||||
// A default table is empty and safe to walk.
|
||||
GattServiceTable table = connection.get_service_table();
|
||||
@@ -76,4 +78,54 @@ TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) {
|
||||
EXPECT_EQ(table.descriptor_count, 0);
|
||||
}
|
||||
|
||||
// A radon_eye_rd200-shaped table: two services, the second holding a
|
||||
// notifying characteristic with a CCCD and a bare write characteristic.
|
||||
class ServiceTableLookup : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->services_[0] = {ESPBTUUID::from_uint16(0x1800), 0x0001, 0x0005, 0, 1};
|
||||
this->services_[1] = {ESPBTUUID::from_uint16(0x1523), 0x0010, 0x0020, 1, 2};
|
||||
this->characteristics_[0] = {ESPBTUUID::from_uint16(0x2A00), 0x0003, 0x0003, 0x02, 0, 0};
|
||||
this->characteristics_[1] = {ESPBTUUID::from_uint16(0x1525), 0x0012, 0x0014, 0x10, 0, 1};
|
||||
this->characteristics_[2] = {ESPBTUUID::from_uint16(0x1524), 0x0016, 0x0016, 0x04, 1, 0};
|
||||
this->descriptors_[0] = {ESPBTUUID::from_uint16(0x2902), 0x0013};
|
||||
this->table_ = {this->services_, this->characteristics_, this->descriptors_, 2, 3, 1};
|
||||
}
|
||||
|
||||
GattService services_[2];
|
||||
GattCharacteristic characteristics_[3];
|
||||
GattDescriptor descriptors_[1];
|
||||
GattServiceTable table_;
|
||||
};
|
||||
|
||||
TEST_F(ServiceTableLookup, FindsServicesAndCharacteristicsByUuid) {
|
||||
const GattService *service = find_service(this->table_, ESPBTUUID::from_uint16(0x1523));
|
||||
ASSERT_NE(service, nullptr);
|
||||
EXPECT_EQ(service->start_handle, 0x0010);
|
||||
EXPECT_EQ(find_service(this->table_, ESPBTUUID::from_uint16(0xFFFF)), nullptr);
|
||||
|
||||
const GattCharacteristic *characteristic =
|
||||
find_characteristic(this->table_, *service, ESPBTUUID::from_uint16(0x1525));
|
||||
ASSERT_NE(characteristic, nullptr);
|
||||
EXPECT_EQ(characteristic->value_handle, 0x0012);
|
||||
// The lookup is scoped to the service: 0x2A00 lives in the other service.
|
||||
EXPECT_EQ(find_characteristic(this->table_, *service, ESPBTUUID::from_uint16(0x2A00)), nullptr);
|
||||
}
|
||||
|
||||
TEST_F(ServiceTableLookup, FindsTheCccdAndReportsItsAbsence) {
|
||||
const GattService *service = find_service(this->table_, ESPBTUUID::from_uint16(0x1523));
|
||||
const GattCharacteristic *notify_char = find_characteristic(this->table_, *service, ESPBTUUID::from_uint16(0x1525));
|
||||
EXPECT_EQ(find_cccd(this->table_, *notify_char), 0x0013);
|
||||
const GattCharacteristic *write_char = find_characteristic(this->table_, *service, ESPBTUUID::from_uint16(0x1524));
|
||||
EXPECT_EQ(find_cccd(this->table_, *write_char), 0);
|
||||
}
|
||||
|
||||
TEST_F(ServiceTableLookup, RejectsRangesThatOverrunTheTable) {
|
||||
// A corrupt index range must fail the lookup, not walk out of bounds.
|
||||
GattService bad_service = {ESPBTUUID::from_uint16(0x1523), 0x0010, 0x0020, 2, 5};
|
||||
EXPECT_EQ(find_characteristic(this->table_, bad_service, ESPBTUUID::from_uint16(0x1524)), nullptr);
|
||||
GattCharacteristic bad_char = {ESPBTUUID::from_uint16(0x1525), 0x0012, 0x0014, 0x10, 0, 9};
|
||||
EXPECT_EQ(find_cccd(this->table_, bad_char), 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
|
||||
@@ -13,17 +13,12 @@ namespace esphome::ble_device_base::testing {
|
||||
//
|
||||
// The in-tree emit site (BK72xxBLETracker::on_scan_report) compiles against
|
||||
// the Beken SDK and cannot run host-side, so the guard-and-fire semantics are
|
||||
// pinned here through a minimal host BLEHub implementation instead.
|
||||
// pinned here through a minimal host hub carrying only the slot under test.
|
||||
namespace {
|
||||
|
||||
class FakeHub : public BLEHub {
|
||||
class FakeHub {
|
||||
public:
|
||||
void register_listener(ESPBTDeviceListener *listener) override {}
|
||||
void set_raw_advertisement_callback(RawAdvertisementCallback callback) override { this->callback_ = callback; }
|
||||
HubCapabilities get_capabilities() const override { return {false, false, false}; }
|
||||
void get_adapter_mac(uint8_t out[6]) override {}
|
||||
bool scan_running() override { return false; }
|
||||
bool scan_active() override { return false; }
|
||||
void set_raw_advertisement_callback(RawAdvertisementCallback callback) { this->callback_ = callback; }
|
||||
|
||||
/// The emit path every tracker implements: fire only when a subscriber is set.
|
||||
void emit(const RawAdvertisement &adv) {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_hub.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
// Pins the request_scan_mode() contract: the base default refuses (so hubs
|
||||
// without a mode switch — and out-of-tree trackers — keep building and
|
||||
// callers report the real state), while an overriding hub both honors the
|
||||
// request and applies it.
|
||||
namespace {
|
||||
|
||||
class DefaultHub : public BLEHub {
|
||||
public:
|
||||
void register_listener(ESPBTDeviceListener *listener) override {}
|
||||
void set_raw_advertisement_callback(RawAdvertisementCallback callback) override {}
|
||||
HubCapabilities get_capabilities() const override { return {false, false, false}; }
|
||||
void get_adapter_mac(uint8_t out[6]) override {}
|
||||
bool scan_running() override { return false; }
|
||||
// Backed by real state so "changes nothing" is observable: a base default
|
||||
// that silently mutated the hub would flip this and fail the assertion.
|
||||
bool scan_active() override { return this->active_; }
|
||||
|
||||
protected:
|
||||
bool active_{true};
|
||||
};
|
||||
|
||||
class SwitchingHub : public DefaultHub {
|
||||
public:
|
||||
HubCapabilities get_capabilities() const override { return {true, false, false, /* scan_mode_switch = */ true}; }
|
||||
bool request_scan_mode(bool active) override {
|
||||
this->active_ = active;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// The esp32 shape: the controller supports active scanning but the hub keeps
|
||||
// the refusing default (mode is driven through its own tracker API).
|
||||
class CapableRefusingHub : public DefaultHub {
|
||||
public:
|
||||
HubCapabilities get_capabilities() const override { return {true, false, false}; }
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(BLEHubScanModeRequest, DefaultRefusesAndChangesNothing) {
|
||||
DefaultHub hub;
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
EXPECT_FALSE(hub.request_scan_mode(false));
|
||||
// Refused, not applied-and-reported-false: the state is untouched.
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
EXPECT_FALSE(hub.request_scan_mode(true));
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
}
|
||||
|
||||
TEST(BLEHubScanModeRequest, OverrideHonorsAndApplies) {
|
||||
SwitchingHub hub;
|
||||
EXPECT_TRUE(hub.get_capabilities().scan_mode_switch);
|
||||
EXPECT_TRUE(hub.request_scan_mode(true));
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
EXPECT_TRUE(hub.request_scan_mode(false));
|
||||
EXPECT_FALSE(hub.scan_active());
|
||||
}
|
||||
|
||||
TEST(BLEHubScanModeRequest, CapabilityAndSwitchAreIndependent) {
|
||||
CapableRefusingHub hub;
|
||||
EXPECT_TRUE(hub.get_capabilities().active_scan);
|
||||
// The esp32 shape advertises no runtime switch, and the request refuses.
|
||||
EXPECT_FALSE(hub.get_capabilities().scan_mode_switch);
|
||||
EXPECT_FALSE(hub.request_scan_mode(false));
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -0,0 +1,10 @@
|
||||
# Advertisement-only proxy on esp32 by explicit choice: no GATT backend is
|
||||
# compiled (USE_BLE_GATT_CLIENT unset), which pins the SERVES_PROXY gating and the
|
||||
# address-scoped maintenance path that a connections build never exercises.
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
esp32_ble_tracker:
|
||||
|
||||
bluetooth_proxy:
|
||||
active: false
|
||||
Reference in New Issue
Block a user