From 4344ffd68210430d22a379cf67bf3980cf19ed31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Aug 2026 15:47:43 -0500 Subject: [PATCH] Share one BLEClientNode between the engines and round out the neutral surface The neutral callbacks (plus a new on_pairing_result hook) live unconditionally in ble_client_node.h; the raw esp32 surface stays beneath them for unmigrated nodes. Adds find_descriptor beside find_cccd and pair()/unpair() on the neutral client. Drops the node 'client' pointer and address, which nothing reads. --- esphome/components/ble_client/ble_client.h | 30 +-------- .../components/ble_client/ble_client_gatt.cpp | 3 + .../components/ble_client/ble_client_gatt.h | 25 ++----- .../components/ble_client/ble_client_node.h | 65 +++++++++++++++++++ .../ble_device_base/ble_gatt_client.cpp | 19 ++++-- .../ble_device_base/ble_gatt_client.h | 3 + 6 files changed, 88 insertions(+), 57 deletions(-) create mode 100644 esphome/components/ble_client/ble_client_node.h diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index c8494cc82c..24851b6656 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -4,6 +4,7 @@ #ifdef USE_ESP32 +#include "ble_client_node.h" #include "esphome/components/esp32_ble_client/ble_client_base.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/core/component.h" @@ -23,34 +24,6 @@ namespace espbt = esphome::esp32_ble_tracker; using namespace esp32_ble_client; -class BLEClient; - -class BLEClientNode { - public: - virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param){}; - virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {} - virtual void loop() {} - void set_address(uint64_t address) { address_ = address; } - espbt::ESPBTClient *client; - // This should be transitioned to Established once the node no longer needs - // the services/descriptors/characteristics of the parent client. This will - // allow some memory to be freed. - // The parent frees the peer's GATT cache once every node reports Established. - // Never report Established while an operation that reads that cache is outstanding. - // - esp_ble_gattc_register_for_notify() completes asynchronously. - // - Register from ESP_GATTC_SEARCH_CMPL_EVT, then set this from ESP_GATTC_REG_FOR_NOTIFY_EVT. - // - BLEClientBase::register_for_notify() holds the release until the registration completes. - espbt::ClientState node_state; - - BLEClient *parent() { return this->parent_; } - void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; } - - protected: - BLEClient *parent_; - uint64_t address_; -}; - class BLEClient final : public BLEClientBase { public: void setup() override; @@ -66,7 +39,6 @@ class BLEClient final : public BLEClientBase { void set_enabled(bool enabled); void register_ble_node(BLEClientNode *node) { - node->client = this; node->set_ble_client_parent(this); this->nodes_.push_back(node); } diff --git a/esphome/components/ble_client/ble_client_gatt.cpp b/esphome/components/ble_client/ble_client_gatt.cpp index 8ca8170fc3..2365190819 100644 --- a/esphome/components/ble_client/ble_client_gatt.cpp +++ b/esphome/components/ble_client/ble_client_gatt.cpp @@ -243,6 +243,9 @@ void BLEClient::on_pairing_result(int status) { } else { ESP_LOGI(TAG, "[%s] Paired", this->address_str_); } + for (auto *node : this->nodes_) { + node->on_pairing_result(status); + } } void BLEClient::dump_config() { diff --git a/esphome/components/ble_client/ble_client_gatt.h b/esphome/components/ble_client/ble_client_gatt.h index 6bde97dcf1..0560d85ba8 100644 --- a/esphome/components/ble_client/ble_client_gatt.h +++ b/esphome/components/ble_client/ble_client_gatt.h @@ -15,8 +15,10 @@ #if defined(USE_BLE_GATT_CLIENT) && !defined(USE_ESP32) +#include "ble_client_node.h" #include "esphome/components/ble_device_base/ble_device.h" #include "esphome/components/ble_device_base/ble_gatt_client.h" +#include "esphome/components/bluetooth_connection/bluetooth_connection.h" #include "esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -26,27 +28,6 @@ namespace esphome::ble_client { -class BLEClient; - -/// The neutral node interface. Nodes resolve their handles from the service -/// table during on_connected() and MUST copy what they need: the table is -/// borrowed backend storage, valid only for the duration of that call. -class BLEClientNode { - public: - virtual void on_connected(const ble_device_base::GattServiceTable &table) {} - virtual void on_disconnected() {} - virtual void on_notify(uint16_t handle, const uint8_t *data, uint16_t len) {} - virtual void on_notify_state(uint16_t handle, bool enabled, 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) {} - - BLEClient *parent() const { return this->parent_; } - void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; } - - protected: - BLEClient *parent_{nullptr}; -}; - class BLEClient : public Component, public ble_device_base::ESPBTDeviceListener, public ble_device_base::GattClientListener { @@ -95,6 +76,8 @@ class BLEClient : public Component, int notify_characteristic(uint16_t handle, bool enable) { return this->backend_->notify_characteristic(handle, enable); } + int pair() { return this->backend_->pair(); } + int unpair() { return bluetooth_connection::unpair_device(this->address_); } // Automation callback registration. template void add_on_connect_callback(F &&callback) { diff --git a/esphome/components/ble_client/ble_client_node.h b/esphome/components/ble_client/ble_client_node.h new file mode 100644 index 0000000000..71f2ae59c8 --- /dev/null +++ b/esphome/components/ble_client/ble_client_node.h @@ -0,0 +1,65 @@ +// The single BLEClientNode both ble_client engines share. The neutral +// callback surface is the one interface node components build on; the raw +// esp32 surface below it remains for components that have not migrated yet. + +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_BLE_GATT_CLIENT +#include "esphome/components/ble_device_base/ble_gatt_client.h" +#endif +#ifdef USE_ESP32 +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#include +#include +#endif + +#include + +namespace esphome::ble_client { + +class BLEClient; + +class BLEClientNode { + public: +#if defined(USE_BLE_CLIENT_GATT_NODES) || (defined(USE_BLE_GATT_CLIENT) && !defined(USE_ESP32)) + // Neutral surface, delivered by both engines. The table is borrowed + // backend storage, valid only for the duration of on_connected(): copy the + // handles you need. All nodes see all completions/notifications; filter by + // handle. + virtual void on_connected(const ble_device_base::GattServiceTable &table) {} + virtual void on_disconnected() {} + virtual void on_notify(uint16_t handle, const uint8_t *data, uint16_t len) {} + virtual void on_notify_state(uint16_t handle, bool enabled, 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_pairing_result(int status) {} +#endif +#ifdef USE_ESP32 + // Legacy raw surface (esp32 engine only); components overriding these are + // esp32-only until migrated to the neutral surface above. + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) {} + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {} + virtual void loop() {} + // This should be transitioned to Established once the node no longer needs + // the services/descriptors/characteristics of the parent client. This will + // allow some memory to be freed. + // The parent frees the peer's GATT cache once every node reports Established. + // Never report Established while an operation that reads that cache is outstanding. + // - esp_ble_gattc_register_for_notify() completes asynchronously. + // - Register from ESP_GATTC_SEARCH_CMPL_EVT, then set this from ESP_GATTC_REG_FOR_NOTIFY_EVT. + // - BLEClientBase::register_for_notify() holds the release until the registration completes. + esp32_ble_tracker::ClientState node_state; +#endif + + BLEClient *parent() const { return this->parent_; } + void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; } + + protected: + BLEClient *parent_{nullptr}; +}; + +} // namespace esphome::ble_client diff --git a/esphome/components/ble_device_base/ble_gatt_client.cpp b/esphome/components/ble_device_base/ble_gatt_client.cpp index 66c9f7d0f2..b076d89b1a 100644 --- a/esphome/components/ble_device_base/ble_gatt_client.cpp +++ b/esphome/components/ble_device_base/ble_gatt_client.cpp @@ -23,19 +23,24 @@ const GattCharacteristic *find_characteristic(const GattServiceTable &table, con return nullptr; } -uint16_t find_cccd(const GattServiceTable &table, const GattCharacteristic &characteristic) { +const GattDescriptor *find_descriptor(const GattServiceTable &table, const GattCharacteristic &characteristic, + const ESPBTUUID &uuid) { uint32_t end = uint32_t(characteristic.first_descriptor) + characteristic.descriptor_count; if (end > table.descriptor_count) { - // Corrupt range, not a missing CCCD. + // Corrupt range, not a missing descriptor. ESP_LOGW(TAG, "descriptor range out of bounds"); - return 0; + return nullptr; } - const ESPBTUUID cccd_uuid = ESPBTUUID::from_uint16(CCCD_UUID); for (uint32_t i = characteristic.first_descriptor; i < end; i++) { - if (table.descriptors[i].uuid == cccd_uuid) - return table.descriptors[i].handle; + if (table.descriptors[i].uuid == uuid) + return &table.descriptors[i]; } - return 0; + return nullptr; +} + +uint16_t find_cccd(const GattServiceTable &table, const GattCharacteristic &characteristic) { + const GattDescriptor *desc = find_descriptor(table, characteristic, ESPBTUUID::from_uint16(CCCD_UUID)); + return desc != nullptr ? desc->handle : 0; } } // namespace esphome::ble_device_base diff --git a/esphome/components/ble_device_base/ble_gatt_client.h b/esphome/components/ble_device_base/ble_gatt_client.h index 70e043b7a1..7fc9aab10d 100644 --- a/esphome/components/ble_device_base/ble_gatt_client.h +++ b/esphome/components/ble_device_base/ble_gatt_client.h @@ -171,6 +171,9 @@ inline const GattService *find_service(const GattServiceTable &table, const ESPB const GattCharacteristic *find_characteristic(const GattServiceTable &table, const GattService &service, const ESPBTUUID &uuid); +const GattDescriptor *find_descriptor(const GattServiceTable &table, const GattCharacteristic &characteristic, + const ESPBTUUID &uuid); + /// Handle of the characteristic's Client Characteristic Configuration /// descriptor (0x2902), or 0 when it has none. uint16_t find_cccd(const GattServiceTable &table, const GattCharacteristic &characteristic);