mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 00:40:21 +00:00
Merge branch 'esp32-tracker-retire-scanner-listener' into esp32-hub-devirtualize
# Conflicts: # esphome/components/ble_device_base/ble_hub.h # esphome/components/bluetooth_proxy/__init__.py # esphome/components/bluetooth_proxy/bluetooth_proxy.cpp # esphome/components/bluetooth_proxy/bluetooth_proxy.h # esphome/components/esp32_ble_tracker/esp32_ble_tracker.h
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
//
|
||||
// Platform-neutral GATT client connection contract.
|
||||
//
|
||||
// A platform's GATT client backend (bluetooth_connection/esp32,
|
||||
// bluetooth_connection/rp2) implements BLEGattConnection; consumers
|
||||
// (bluetooth_proxy) drive it through this interface and receive
|
||||
// completions through GattClientEventListener. All listener callbacks are
|
||||
// delivered on the ESPHome main loop; borrowed data pointers are valid only
|
||||
// for the duration of the call.
|
||||
// Exactly one GATT backend exists per build, so BLEGattConnection is a
|
||||
// compile-time alias (bluetooth_connection_gatt_backend.h), not an abstract
|
||||
// interface.
|
||||
// The hub BluetoothConnection wrapper drives it and receives completions
|
||||
// through its event-sink methods, which the backend calls directly. All sink
|
||||
// calls are delivered on the ESPHome main loop; borrowed data pointers are
|
||||
// valid only for the duration of the call.
|
||||
//
|
||||
// Error domain (plain int, forwarded to the API without translation):
|
||||
// 0 success
|
||||
@@ -29,6 +30,7 @@
|
||||
#include "ble_client_state.h"
|
||||
#include "ble_device.h"
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::ble_device_base {
|
||||
@@ -76,64 +78,53 @@ struct GattServiceTable {
|
||||
uint16_t descriptor_count{0};
|
||||
};
|
||||
|
||||
/// Completion/event sink for a GATT connection. Implemented by the consumer
|
||||
/// (bluetooth_proxy's connection wrapper). Every callback runs on the main loop.
|
||||
class GattClientEventListener {
|
||||
public:
|
||||
virtual ~GattClientEventListener() = default;
|
||||
|
||||
/// Connected (with negotiated MTU) or disconnected/connect-failed
|
||||
/// (error = HCI status or disconnect reason).
|
||||
virtual void on_connection_state(bool connected, uint16_t mtu, int error) = 0;
|
||||
/// Service discovery finished; on success the service table is populated.
|
||||
virtual void on_service_discovery_done(int error) = 0;
|
||||
/// Characteristic or descriptor read finished. data/len valid during the call.
|
||||
virtual void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) = 0;
|
||||
/// Characteristic write-with-response or descriptor write finished.
|
||||
virtual void on_write_result(uint16_t handle, int error) = 0;
|
||||
/// Notification/indication registration state changed.
|
||||
virtual void on_notify_state(uint16_t handle, bool enabled, int error) = 0;
|
||||
/// Notification/indication data from the peer. data/len valid during the call.
|
||||
virtual void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) = 0;
|
||||
// The BLEGattConnection op surface, asserted where the alias binds
|
||||
// (bluetooth_connection_gatt_backend.h). Operations return 0 when accepted (completion arrives
|
||||
// through the sink) or a synchronous error (busy, not connected, stack
|
||||
// rejection); one operation may be outstanding at a time. Semantics beyond
|
||||
// the signatures:
|
||||
// - connect: addr_type is a BLE_ADDR_TYPE_* constant (ble_device.h).
|
||||
// - disconnect: also cancels a connect in progress.
|
||||
// - notify_characteristic: local registration only; the CCCD write is the
|
||||
// API client's responsibility (a plain write_descriptor).
|
||||
// - get_service_table/release_services: backend-owned transient storage,
|
||||
// released after streaming (release is idempotent).
|
||||
// - completions: connect and disconnect land in on_connection_state,
|
||||
// discover_services in on_service_discovery_done, pair in
|
||||
// on_pairing_result, reads in on_read_result, notify_characteristic in
|
||||
// on_notify_state, characteristic writes with response and descriptor
|
||||
// writes in on_write_result.
|
||||
template<typename T, typename Sink>
|
||||
concept BLEGattConnectionContract = requires(T conn, Sink *sink, const uint8_t *data) {
|
||||
conn.set_listener(sink);
|
||||
{ conn.connect(uint64_t{}, uint8_t{}) } -> std::same_as<int>;
|
||||
{ conn.disconnect() } -> std::same_as<int>;
|
||||
{ conn.discover_services() } -> std::same_as<int>;
|
||||
{ conn.read_characteristic(uint16_t{}) } -> std::same_as<int>;
|
||||
{ conn.write_characteristic(uint16_t{}, data, uint16_t{}, true) } -> std::same_as<int>;
|
||||
{ conn.read_descriptor(uint16_t{}) } -> std::same_as<int>;
|
||||
{ conn.write_descriptor(uint16_t{}, data, uint16_t{}) } -> std::same_as<int>;
|
||||
{ conn.notify_characteristic(uint16_t{}, true) } -> std::same_as<int>;
|
||||
{ conn.pair() } -> std::same_as<int>;
|
||||
{ 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>;
|
||||
};
|
||||
|
||||
/// One GATT client connection slot. Operations return 0 when accepted
|
||||
/// (completion arrives via the listener) or a synchronous error code
|
||||
/// (busy, not connected, stack rejection). One operation may be outstanding
|
||||
/// at a time; callers see a synchronous error otherwise.
|
||||
class BLEGattConnection {
|
||||
public:
|
||||
virtual ~BLEGattConnection() = default;
|
||||
|
||||
void set_listener(GattClientEventListener *listener) { this->listener_ = listener; }
|
||||
|
||||
/// Start connecting to a peer. addr_type is a BLE_ADDR_TYPE_* constant
|
||||
/// (ble_device.h). Completion: on_connection_state().
|
||||
virtual int connect(uint64_t address, uint8_t addr_type) = 0;
|
||||
/// Disconnect (or cancel a connect in progress). Completion: on_connection_state().
|
||||
virtual int disconnect() = 0;
|
||||
/// Discover the peer's services/characteristics/descriptors into the
|
||||
/// service table. Completion: on_service_discovery_done().
|
||||
virtual int discover_services() = 0;
|
||||
virtual int read_characteristic(uint16_t handle) = 0;
|
||||
virtual int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) = 0;
|
||||
virtual int read_descriptor(uint16_t handle) = 0;
|
||||
virtual int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) = 0;
|
||||
/// Enable/disable delivery of on_notify_data() for a characteristic value
|
||||
/// handle. Local registration only — the CCCD write is the API client's
|
||||
/// responsibility (it arrives as a plain write_descriptor).
|
||||
virtual int notify_characteristic(uint16_t handle, bool enable) = 0;
|
||||
virtual int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
|
||||
uint16_t timeout) = 0;
|
||||
|
||||
/// Backend-owned service table (see GattServiceTable lifetime).
|
||||
virtual GattServiceTable get_service_table() = 0;
|
||||
/// Free the transient service table storage. Call after streaming;
|
||||
/// idempotent (a call with no table held is a no-op).
|
||||
virtual void release_services() = 0;
|
||||
|
||||
protected:
|
||||
GattClientEventListener *listener_{nullptr};
|
||||
// The event sink the backend calls directly (the hub BluetoothConnection
|
||||
// wrapper), asserted where the wrapper is defined: on_connection_state
|
||||
// carries the negotiated MTU and an HCI status/disconnect reason. The
|
||||
// requirements check call validity, not exact parameter types; keep sink
|
||||
// parameters at the documented widths (uint16_t handles and lengths).
|
||||
template<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>;
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_device_base
|
||||
|
||||
@@ -48,20 +48,29 @@ static constexpr conn_err_t GATT_NOT_CONNECTED = ble_device_base::GATT_ERR_NOT_C
|
||||
|
||||
// What the platform's connection backend supports beyond GATT operations;
|
||||
// the proxy derives its feature flags and legacy version from these.
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32)
|
||||
static constexpr bool SUPPORTS_PAIRING = true;
|
||||
static constexpr bool SUPPORTS_CACHE_CLEARING = true;
|
||||
#elif defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
// The rp2 BTstack backend pairs (just works + bonding); it has no service
|
||||
// cache to clear. Keyed on the backend, not the generic client define, so a
|
||||
// future backend without pairing keeps the stub arm below.
|
||||
static constexpr bool SUPPORTS_PAIRING = true;
|
||||
static constexpr bool SUPPORTS_CACHE_CLEARING = false;
|
||||
#else
|
||||
static constexpr bool SUPPORTS_PAIRING = false;
|
||||
static constexpr bool SUPPORTS_CACHE_CLEARING = false;
|
||||
#endif
|
||||
|
||||
// Address-scoped (not connection-scoped) maintenance requests.
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32) || (defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT))
|
||||
conn_err_t unpair_device(uint64_t address);
|
||||
conn_err_t clear_gatt_cache(uint64_t address);
|
||||
#else
|
||||
inline conn_err_t unpair_device(uint64_t) { return GATT_NOT_CONNECTED; }
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
conn_err_t clear_gatt_cache(uint64_t address);
|
||||
#else
|
||||
inline conn_err_t clear_gatt_cache(uint64_t) { return GATT_NOT_CONNECTED; }
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// bluetooth_connection_gatt_backend.h
|
||||
//
|
||||
// Binds ble_device_base::BLEGattConnection to the build's one GATT backend.
|
||||
// Backend and consumer both live in this component, so the ladder does too;
|
||||
// backends implement ble_gatt_client.h (the neutral contract).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
|
||||
#if defined(USE_RP2040_BLE)
|
||||
#include "bluetooth_connection_rp2.h"
|
||||
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::RP2GattClient
|
||||
#elif defined(USE_BLE_GATT_CLIENT_STUB_BACKEND)
|
||||
// Emitted only by the host unit-test manifest: the tests compile the hub
|
||||
// wrapper standalone, so bind a do-nothing backend. Every other backend-less
|
||||
// build hits the #error below.
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
class BluetoothConnection;
|
||||
|
||||
class StubGattBackend {
|
||||
public:
|
||||
void set_listener(BluetoothConnection *listener) {}
|
||||
int connect(uint64_t address, uint8_t addr_type) { return ble_device_base::GATT_ERR_NOT_CONNECTED; }
|
||||
int disconnect() { return ble_device_base::GATT_ERR_NOT_CONNECTED; }
|
||||
int discover_services() { return ble_device_base::GATT_ERR_NOT_CONNECTED; }
|
||||
int read_characteristic(uint16_t handle) { return ble_device_base::GATT_ERR_NOT_CONNECTED; }
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
}
|
||||
int read_descriptor(uint16_t handle) { return ble_device_base::GATT_ERR_NOT_CONNECTED; }
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
}
|
||||
int notify_characteristic(uint16_t handle, bool enable) { return ble_device_base::GATT_ERR_NOT_CONNECTED; }
|
||||
int pair() { return ble_device_base::GATT_ERR_NOT_CONNECTED; }
|
||||
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) {
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
}
|
||||
ble_device_base::GattServiceTable get_service_table() { return {}; }
|
||||
void release_services() {}
|
||||
};
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::StubGattBackend
|
||||
#else
|
||||
#error "USE_BLE_GATT_CLIENT is set but this build has no GATT backend; add an alias arm here"
|
||||
#endif
|
||||
|
||||
namespace esphome::ble_device_base {
|
||||
|
||||
using BLEGattConnection = ESPHOME_BLE_GATT_CONNECTION_TYPE;
|
||||
static_assert(BLEGattConnectionContract<BLEGattConnection, bluetooth_connection::BluetoothConnection>,
|
||||
"The build's GATT backend is missing part of the BLEGattConnection surface (ble_gatt_client.h)");
|
||||
#undef ESPHOME_BLE_GATT_CONNECTION_TYPE
|
||||
|
||||
} // namespace esphome::ble_device_base
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT
|
||||
@@ -74,6 +74,16 @@ void BluetoothConnection::check_disconnect_timeout_() {
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::on_pairing_result(int status) {
|
||||
if (this->address_ == 0) {
|
||||
// A drop before completion already answered: reset_connection_slot_ sends
|
||||
// the connection response, which the client's pair watcher raises on.
|
||||
return;
|
||||
}
|
||||
this->paired_ = status == 0;
|
||||
this->proxy_->send_device_pairing(this->address_, status == 0, status);
|
||||
}
|
||||
|
||||
void BluetoothConnection::reset_connection_(conn_err_t reason) {
|
||||
if (this->pending_error_ != 0) {
|
||||
reason = this->pending_error_;
|
||||
@@ -81,11 +91,12 @@ void BluetoothConnection::reset_connection_(conn_err_t reason) {
|
||||
}
|
||||
this->state_ = ClientState::IDLE;
|
||||
this->services_discovered_ = false;
|
||||
this->paired_ = false;
|
||||
this->backend_->release_services();
|
||||
this->proxy_->reset_connection_slot_(this, reason);
|
||||
}
|
||||
|
||||
// ---- GattClientEventListener ----
|
||||
// ---- backend event sink ----
|
||||
|
||||
void BluetoothConnection::on_connection_state(bool connected, uint16_t mtu, int error) {
|
||||
if (connected && this->address_ == 0) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Hub-platform BluetoothConnection: drives a platform GATT client backend
|
||||
// through the neutral ble_device_base::BLEGattConnection interface and
|
||||
// translates its events into the same API messages the esp32 class emits.
|
||||
// Hub-platform BluetoothConnection: drives the build's GATT backend (the
|
||||
// ble_device_base::BLEGattConnection alias) and translates its events into
|
||||
// the same API messages the esp32 class emits.
|
||||
// Presents the identical method surface, so the proxy's GATT dispatch
|
||||
// compiles against either class unchanged.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_client_state.h"
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
#include "bluetooth_connection_gatt_backend.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::bluetooth_proxy {
|
||||
@@ -25,7 +25,7 @@ namespace esphome::bluetooth_connection {
|
||||
using ClientState = ble_device_base::ClientState;
|
||||
using ConnectionType = ble_device_base::ConnectionType;
|
||||
|
||||
class BluetoothConnection final : public ble_device_base::GattClientEventListener {
|
||||
class BluetoothConnection final {
|
||||
public:
|
||||
/// Wire the platform backend. Called from codegen before setup.
|
||||
void set_backend(ble_device_base::BLEGattConnection *backend) {
|
||||
@@ -49,6 +49,9 @@ class BluetoothConnection final : public ble_device_base::GattClientEventListene
|
||||
this->start_connect_();
|
||||
}
|
||||
void disconnect();
|
||||
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; }
|
||||
@@ -80,13 +83,14 @@ class BluetoothConnection final : public ble_device_base::GattClientEventListene
|
||||
this->check_disconnect_timeout_();
|
||||
}
|
||||
|
||||
// ---- ble_device_base::GattClientEventListener ----
|
||||
void on_connection_state(bool connected, uint16_t mtu, int error) override;
|
||||
void on_service_discovery_done(int error) override;
|
||||
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) override;
|
||||
void on_write_result(uint16_t handle, int error) override;
|
||||
void on_notify_state(uint16_t handle, bool enabled, int error) override;
|
||||
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) override;
|
||||
// ---- backend event sink (called directly by the backend, main loop) ----
|
||||
void on_connection_state(bool connected, uint16_t mtu, int error);
|
||||
void on_service_discovery_done(int error);
|
||||
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error);
|
||||
void on_write_result(uint16_t handle, int error);
|
||||
void on_notify_state(uint16_t handle, bool enabled, int error);
|
||||
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len);
|
||||
void on_pairing_result(int status);
|
||||
|
||||
protected:
|
||||
friend class bluetooth_proxy::BluetoothProxy;
|
||||
@@ -98,8 +102,7 @@ class BluetoothConnection final : public ble_device_base::GattClientEventListene
|
||||
conn_err_t check_connected_op_(const char *action, const char *type) const;
|
||||
void log_gatt_operation_error_(const char *operation, uint16_t handle, int status);
|
||||
|
||||
// Memory optimized layout for 32-bit systems (a vptr precedes: pointers and
|
||||
// 2-byte members first fill to an 8-byte boundary before address_)
|
||||
// Memory optimized layout for 32-bit systems
|
||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||
bluetooth_proxy::BluetoothProxy *proxy_{nullptr};
|
||||
ble_device_base::BLEGattConnection *backend_{nullptr};
|
||||
@@ -118,12 +121,16 @@ class BluetoothConnection final : public ble_device_base::GattClientEventListene
|
||||
|
||||
// Group 5: 1-byte types
|
||||
ClientState state_{ClientState::IDLE};
|
||||
bool paired_{false};
|
||||
ConnectionType connection_type_{ConnectionType::V1};
|
||||
uint8_t remote_addr_type_{0};
|
||||
uint8_t connection_index_{0};
|
||||
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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "bluetooth_connection_rp2.h"
|
||||
|
||||
#include "bluetooth_connection_hub.h"
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
#if defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -51,6 +54,7 @@ using ble_device_base::MEDIUM_MIN_CONN_INTERVAL;
|
||||
RP2GattClient *RP2GattClient::instances[ESPHOME_BLE_GATT_CLIENT_COUNT] = {};
|
||||
uint8_t RP2GattClient::instance_count = 0;
|
||||
btstack_packet_callback_registration_t RP2GattClient::hci_event_registration = {};
|
||||
btstack_packet_callback_registration_t RP2GattClient::sm_event_registration = {};
|
||||
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static ESPBTUUID uuid_from_btstack(uint16_t uuid16, const uint8_t uuid128[16]) {
|
||||
@@ -88,6 +92,8 @@ void RP2GattClient::setup() {
|
||||
if (hci_event_registration.callback == nullptr) {
|
||||
hci_event_registration.callback = &RP2GattClient::hci_packet_handler;
|
||||
hci_add_event_handler(&hci_event_registration);
|
||||
sm_event_registration.callback = &RP2GattClient::sm_packet_handler;
|
||||
sm_add_event_handler(&sm_event_registration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +163,38 @@ void RP2GattClient::hci_packet_handler(uint8_t type, uint16_t channel, uint8_t *
|
||||
}
|
||||
}
|
||||
|
||||
void RP2GattClient::sm_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
|
||||
if (type != HCI_EVENT_PACKET) {
|
||||
return;
|
||||
}
|
||||
switch (hci_event_packet_get_type(packet)) {
|
||||
case SM_EVENT_JUST_WORKS_REQUEST:
|
||||
// Confirming from the SM callback is the intended BTstack pattern.
|
||||
// Unscoped on purpose: no peripheral role exists in-tree, and scoping
|
||||
// would drop a request racing the queued CONNECTED event.
|
||||
sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));
|
||||
break;
|
||||
case SM_EVENT_PAIRING_COMPLETE: {
|
||||
RP2GattClient *inst = instance_for_con_handle(sm_event_pairing_complete_get_handle(packet));
|
||||
if (inst != nullptr) {
|
||||
inst->enqueue_event_irq_(RP2GattEvent::PAIRING_RESULT, sm_event_pairing_complete_get_status(packet), 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SM_EVENT_REENCRYPTION_COMPLETE: {
|
||||
// A bonded peer re-encrypts instead of pairing; BTstack emits only this
|
||||
// event on that path, so it answers the PAIR request too.
|
||||
RP2GattClient *inst = instance_for_con_handle(sm_event_reencryption_complete_get_handle(packet));
|
||||
if (inst != nullptr) {
|
||||
inst->enqueue_event_irq_(RP2GattEvent::PAIRING_RESULT, sm_event_reencryption_complete_get_status(packet), 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RP2GattClient::gatt_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
|
||||
if (type != HCI_EVENT_PACKET) {
|
||||
return;
|
||||
@@ -447,6 +485,11 @@ void RP2GattClient::handle_event_(const RP2GattEvent &event) {
|
||||
case RP2GattEvent::WRITE_NO_RSP_DONE:
|
||||
this->finish_write_no_rsp_(event.status);
|
||||
break;
|
||||
case RP2GattEvent::PAIRING_RESULT:
|
||||
if (this->listener_ != nullptr) {
|
||||
this->listener_->on_pairing_result(event.status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1019,6 +1062,15 @@ int RP2GattClient::write_descriptor(uint16_t handle, const uint8_t *data, uint16
|
||||
return 0;
|
||||
}
|
||||
|
||||
int RP2GattClient::pair() {
|
||||
if (this->state_ != EngineState::READY) {
|
||||
return GATT_ERR_NOT_CONNECTED;
|
||||
}
|
||||
BluetoothLock lock;
|
||||
sm_request_pairing(this->con_handle_); // void API; completion via SM events
|
||||
return 0;
|
||||
}
|
||||
|
||||
int RP2GattClient::notify_characteristic(uint16_t handle, bool enable) {
|
||||
if (this->state_ != EngineState::READY) {
|
||||
return GATT_ERR_NOT_CONNECTED;
|
||||
@@ -1064,6 +1116,30 @@ int RP2GattClient::update_connection_params(uint16_t min_interval, uint16_t max_
|
||||
return gap_update_connection_parameters(this->con_handle_, min_interval, max_interval, latency, timeout);
|
||||
}
|
||||
|
||||
conn_err_t unpair_device(uint64_t address) {
|
||||
uint8_t mac[6];
|
||||
ble_device_base::uint64_to_mac_msb_first(address, mac);
|
||||
bool found = false;
|
||||
BluetoothLock lock;
|
||||
// Exhaustive: the db keys on (type, address), so stale entries can share
|
||||
// the same address bytes under different types.
|
||||
for (int i = 0; i < le_device_db_max_count(); i++) {
|
||||
int addr_type = 0;
|
||||
bd_addr_t addr;
|
||||
le_device_db_info(i, &addr_type, addr, nullptr);
|
||||
if (addr_type != BD_ADDR_TYPE_UNKNOWN && memcmp(addr, mac, sizeof(bd_addr_t)) == 0) {
|
||||
le_device_db_remove(i);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
return CONN_OK;
|
||||
}
|
||||
// No bond for this address; the shared error domain has no closer code
|
||||
// (esp32 parity: its remove-bond call also errors for an unknown address).
|
||||
return GATT_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_RP2040_BLE && USE_BLE_GATT_CLIENT
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// RP2 (Pico W / Pico 2 W) GATT client backend over BTstack.
|
||||
//
|
||||
// Implements ble_device_base::BLEGattConnection for the hub BluetoothConnection
|
||||
// wrapper. BTstack packet handlers run in the CYW43 async-context low-priority
|
||||
// IRQ (or on the main-loop stack during BluetoothLock release), so handlers
|
||||
// only copy into per-instance lock-free queues/storage; loop() drains them and
|
||||
// drives the state machine. Every BTstack call issued from the main loop is
|
||||
// wrapped in BluetoothLock.
|
||||
// The build's ble_device_base::BLEGattConnection backend (bound by alias in
|
||||
// bluetooth_connection_gatt_backend.h) for the hub BluetoothConnection wrapper. BTstack packet handlers run in the
|
||||
// CYW43 async-context low-priority IRQ (or on the main-loop stack during BluetoothLock release), so handlers only copy
|
||||
// into per-instance lock-free queues/storage; loop() drains them and drives the state machine. Every BTstack call
|
||||
// issued from the main loop is wrapped in BluetoothLock.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -27,6 +26,8 @@
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
class BluetoothConnection;
|
||||
|
||||
// Caps for the transient service table. Sized generously for real devices
|
||||
// (typical peripherals expose < 8 services / < 30 characteristics); a peer
|
||||
// exceeding a cap fails discovery with INSUFFICIENT_RESOURCES rather than
|
||||
@@ -51,6 +52,7 @@ struct RP2GattEvent {
|
||||
MTU_EXCHANGED, // value = negotiated MTU
|
||||
QUERY_COMPLETE, // status = ATT status of the finished query
|
||||
WRITE_NO_RSP_DONE, // status = result of the deferred write
|
||||
PAIRING_RESULT, // status = SM pairing status (0 = bonded)
|
||||
};
|
||||
Type type;
|
||||
uint8_t status;
|
||||
@@ -71,28 +73,28 @@ static constexpr uint8_t RP2_GATT_EVENT_QUEUE_SIZE = 8;
|
||||
// full 512 B ATT payload, so depth buys burst tolerance at ~516 B per slot.
|
||||
static constexpr uint8_t RP2_GATT_NOTIFY_QUEUE_SIZE = 4;
|
||||
|
||||
class RP2GattClient final : public Component,
|
||||
public ble_device_base::BLEGattConnection,
|
||||
public Parented<rp2040_ble::RP2040BLE> {
|
||||
class RP2GattClient final : public Component, public Parented<rp2040_ble::RP2040BLE> {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
// ---- ble_device_base::BLEGattConnection ----
|
||||
int connect(uint64_t address, uint8_t addr_type) override;
|
||||
int disconnect() override;
|
||||
int discover_services() override;
|
||||
int read_characteristic(uint16_t handle) override;
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) override;
|
||||
int read_descriptor(uint16_t handle) override;
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) override;
|
||||
int notify_characteristic(uint16_t handle, bool enable) override;
|
||||
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
|
||||
uint16_t timeout) override;
|
||||
ble_device_base::GattServiceTable get_service_table() override;
|
||||
void release_services() override;
|
||||
void set_listener(BluetoothConnection *listener) { this->listener_ = listener; }
|
||||
|
||||
// ---- ble_device_base::BLEGattConnection contract ----
|
||||
int connect(uint64_t address, uint8_t addr_type);
|
||||
int disconnect();
|
||||
int discover_services();
|
||||
int read_characteristic(uint16_t handle);
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response);
|
||||
int read_descriptor(uint16_t handle);
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len);
|
||||
int notify_characteristic(uint16_t handle, bool enable);
|
||||
int pair();
|
||||
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout);
|
||||
ble_device_base::GattServiceTable get_service_table();
|
||||
void release_services();
|
||||
|
||||
protected:
|
||||
// Link/engine state. Discovery and GATT ops have their own cursors below —
|
||||
@@ -120,6 +122,7 @@ class RP2GattClient final : public Component,
|
||||
// BTstack packet handlers (IRQ context: copy-and-enqueue only).
|
||||
static void hci_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size);
|
||||
static void gatt_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size);
|
||||
static void sm_packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size);
|
||||
static RP2GattClient *instance_for_con_handle(hci_con_handle_t con_handle);
|
||||
|
||||
void handle_gatt_event_irq_(uint8_t event_type, const uint8_t *packet);
|
||||
@@ -147,6 +150,7 @@ class RP2GattClient final : public Component,
|
||||
}
|
||||
|
||||
// Group 1: containers / large storage
|
||||
BluetoothConnection *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_;
|
||||
@@ -205,6 +209,8 @@ class RP2GattClient final : public Component,
|
||||
static uint8_t instance_count;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static btstack_packet_callback_registration_t hci_event_registration;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static btstack_packet_callback_registration_t sm_event_registration;
|
||||
};
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
@@ -92,6 +92,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
uint16_t get_conn_id() const { return this->conn_id_; }
|
||||
uint64_t get_address() const { return this->address_; }
|
||||
bool is_paired() const { return this->paired_; }
|
||||
// The proxy clears this when a bond is removed while the link is up.
|
||||
void set_unpaired() { this->paired_ = false; }
|
||||
|
||||
uint8_t get_connection_index() const { return this->connection_index_; }
|
||||
|
||||
|
||||
@@ -252,6 +252,7 @@
|
||||
// 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) 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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// The GATT client contract compiles in no real build until a hub backend is
|
||||
// configured; this TU pins it on the host so the header cannot rot unseen.
|
||||
// The contract is a concept (BLEGattConnection is a per-platform alias), so
|
||||
// the minimal backend here proves the concept stays satisfiable and routes
|
||||
// events through the duck-typed sink the way a real backend does.
|
||||
#define USE_BLE_GATT_CLIENT
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
@@ -8,47 +11,57 @@
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
class RecordingListener : public GattClientEventListener {
|
||||
public:
|
||||
void on_connection_state(bool connected, uint16_t mtu, int error) override { this->connected_ = connected; }
|
||||
void on_service_discovery_done(int error) override { this->discovery_error_ = error; }
|
||||
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) override {}
|
||||
void on_write_result(uint16_t handle, int error) override {}
|
||||
void on_notify_state(uint16_t handle, bool enabled, int error) override {}
|
||||
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) override {}
|
||||
struct RecordingSink {
|
||||
void on_connection_state(bool connected, uint16_t mtu, int error) { this->connected_ = connected; }
|
||||
void on_service_discovery_done(int error) { this->discovery_error_ = error; }
|
||||
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {}
|
||||
void on_write_result(uint16_t handle, int error) {}
|
||||
void on_notify_state(uint16_t handle, bool enabled, int error) {}
|
||||
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) {}
|
||||
void on_pairing_result(int status) {}
|
||||
bool connected_{false};
|
||||
int discovery_error_{0};
|
||||
};
|
||||
|
||||
class MinimalConnection : public BLEGattConnection {
|
||||
static_assert(GattClientEventSinkContract<RecordingSink>, "the recording sink must cover the full event-sink surface");
|
||||
|
||||
class MinimalConnection {
|
||||
public:
|
||||
int connect(uint64_t address, uint8_t addr_type) override {
|
||||
void set_listener(RecordingSink *listener) { this->listener_ = listener; }
|
||||
|
||||
int connect(uint64_t address, uint8_t addr_type) {
|
||||
if (this->listener_ != nullptr)
|
||||
this->listener_->on_connection_state(true, 517, 0);
|
||||
return 0;
|
||||
}
|
||||
int disconnect() override { return 0; }
|
||||
int discover_services() override {
|
||||
int disconnect() { return 0; }
|
||||
int discover_services() {
|
||||
if (this->listener_ != nullptr)
|
||||
this->listener_->on_service_discovery_done(0);
|
||||
return 0;
|
||||
}
|
||||
int read_characteristic(uint16_t handle) override { return GATT_ERR_NOT_CONNECTED; }
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) override { return 0; }
|
||||
int read_descriptor(uint16_t handle) override { return 0; }
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) override { return 0; }
|
||||
int notify_characteristic(uint16_t handle, bool enable) override { return 0; }
|
||||
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
|
||||
uint16_t timeout) override {
|
||||
int read_characteristic(uint16_t handle) { return GATT_ERR_NOT_CONNECTED; }
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) { return 0; }
|
||||
int read_descriptor(uint16_t handle) { return 0; }
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) { return 0; }
|
||||
int notify_characteristic(uint16_t handle, bool enable) { return 0; }
|
||||
int pair() { return GATT_ERR_NOT_CONNECTED; }
|
||||
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) {
|
||||
return 0;
|
||||
}
|
||||
GattServiceTable get_service_table() override { return {}; }
|
||||
void release_services() override {}
|
||||
GattServiceTable get_service_table() { return {}; }
|
||||
void release_services() {}
|
||||
|
||||
protected:
|
||||
RecordingSink *listener_{nullptr};
|
||||
};
|
||||
|
||||
static_assert(BLEGattConnectionContract<MinimalConnection, RecordingSink>,
|
||||
"a minimal backend must satisfy the contract the alias asserts");
|
||||
|
||||
TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) {
|
||||
MinimalConnection connection;
|
||||
RecordingListener listener;
|
||||
RecordingSink listener;
|
||||
connection.set_listener(&listener);
|
||||
EXPECT_EQ(connection.connect(0xAABBCCDDEEFFULL, 0), 0);
|
||||
EXPECT_TRUE(listener.connected_);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# close_service_batch compiles only under BLUETOOTH_CONNECTION_HAS_GATT;
|
||||
# emit the backend define so the host build exercises it.
|
||||
async def to_code_testing(config):
|
||||
# These defines are global to the merged host test binary; safe
|
||||
# because no co-compiled test observes them.
|
||||
cg.add_define("USE_BLE_GATT_CLIENT")
|
||||
cg.add_define("USE_BLE_GATT_CLIENT_STUB_BACKEND")
|
||||
cg.add_define("USE_BLUETOOTH_PROXY")
|
||||
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
|
||||
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 1)
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
# The batcher sizes api protobuf messages.
|
||||
manifest.dependencies = manifest.dependencies + ["api"]
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
// The three cursor behaviors: a fitting service advances and continues, an
|
||||
// overflowing batch with >1 service pops and retries it, and a single
|
||||
// oversized service is force-advanced so the stream cannot wedge.
|
||||
|
||||
static void add_service(api::BluetoothGATTGetServicesResponse &resp, uint16_t characteristics) {
|
||||
resp.services.emplace_back();
|
||||
auto &svc = resp.services.back();
|
||||
svc.handle = resp.services.size();
|
||||
svc.uuid = {0x1234567890ABCDEFULL, 0xFEDCBA0987654321ULL};
|
||||
svc.characteristics.init(characteristics);
|
||||
for (uint16_t i = 0; i < characteristics; i++) {
|
||||
auto &chr = svc.characteristics.emplace_back();
|
||||
chr.handle = 100 + i;
|
||||
chr.properties = 0x12;
|
||||
chr.uuid = {0x1234567890ABCDEFULL, 0xFEDCBA0987654321ULL};
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CloseServiceBatch, FittingServiceAdvancesAndContinues) {
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
add_service(resp, 1);
|
||||
size_t current_size = 0;
|
||||
int16_t cursor = 0;
|
||||
EXPECT_EQ(close_service_batch(resp, current_size, cursor, 0, "AA:BB"), BatchClose::CONTINUE);
|
||||
EXPECT_EQ(cursor, 1);
|
||||
EXPECT_GT(current_size, 0u);
|
||||
}
|
||||
|
||||
TEST(CloseServiceBatch, OverflowPopsAndRetriesWithoutAdvancing) {
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
add_service(resp, 1);
|
||||
add_service(resp, 1);
|
||||
size_t current_size = MAX_PACKET_SIZE - 10; // any service is bigger than 10 bytes
|
||||
int16_t cursor = 5;
|
||||
EXPECT_EQ(close_service_batch(resp, current_size, cursor, 0, "AA:BB"), BatchClose::SEND);
|
||||
EXPECT_EQ(resp.services.size(), 1u); // popped for the next batch
|
||||
EXPECT_EQ(cursor, 5); // not advanced: retried next batch
|
||||
}
|
||||
|
||||
TEST(CloseServiceBatch, SingleOversizedServiceForceAdvances) {
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
add_service(resp, 60); // ~30 bytes per characteristic, far past the budget
|
||||
ASSERT_GT(resp.services.back().calculate_size(), MAX_PACKET_SIZE);
|
||||
size_t current_size = 0;
|
||||
int16_t cursor = 7;
|
||||
EXPECT_EQ(close_service_batch(resp, current_size, cursor, 0, "AA:BB"), BatchClose::SEND);
|
||||
EXPECT_EQ(cursor, 8); // advanced despite not fitting, so the stream moves on
|
||||
}
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
Reference in New Issue
Block a user