From 901fd35a7eb66933d69941a5180dc02f1992a4fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Aug 2026 15:53:14 -0500 Subject: [PATCH] Extract the Bluedroid service-table materializer into a shared class BluedroidServiceTable carries the two-pass build verbatim in its own TU; the backend embeds one instance. ble_client's esp32 engine becomes the second consumer next. --- .../bluetooth_connection/__init__.py | 1 + .../bluetooth_connection_bluedroid.cpp | 202 +----------------- .../bluetooth_connection_bluedroid.h | 17 +- .../gatt_service_table_bluedroid.cpp | 180 ++++++++++++++++ .../gatt_service_table_bluedroid.h | 71 ++++++ 5 files changed, 264 insertions(+), 207 deletions(-) create mode 100644 esphome/components/bluetooth_connection/gatt_service_table_bluedroid.cpp create mode 100644 esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h diff --git a/esphome/components/bluetooth_connection/__init__.py b/esphome/components/bluetooth_connection/__init__.py index e6c8de0697..6f965989ab 100644 --- a/esphome/components/bluetooth_connection/__init__.py +++ b/esphome/components/bluetooth_connection/__init__.py @@ -259,6 +259,7 @@ async def new_gatt_backend( # list (this module cannot import bluetooth_proxy to derive it). SOURCE_FILE_FRAMEWORKS: dict[str, set[PlatformFramework]] = { "bluetooth_connection_bluedroid.cpp": frameworks_for_platforms([PLATFORM_ESP32]), + "gatt_service_table_bluedroid.cpp": frameworks_for_platforms([PLATFORM_ESP32]), # Every hub platform the proxy admits (the file compiles empty where # USE_BLE_GATT_CLIENT is not defined), so a platform gaining a backend # cannot hit a missing-symbol trap here. diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index 35ce8164cf..c7ff99b242 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -32,10 +32,6 @@ using ble_device_base::MEDIUM_MIN_CONN_INTERVAL; using esp32_ble_tracker::ClientState; using esp32_ble_tracker::ConnectionType; -// Bounds one characteristic's descriptor walk against a stack that never -// reports end-of-range. -static constexpr uint16_t MAX_DESCRIPTORS_PER_CHARACTERISTIC = 64; - // ---- tracker surface ---- void BluedroidGattClient::connect() { this->tracker_connect_(); } @@ -306,7 +302,7 @@ int BluedroidGattClient::update_connection_params(uint16_t min_interval, uint16_ void BluedroidGattClient::release_services() { this->service_total_ = 0; #ifdef USE_BLE_GATT_SERVICE_TABLE - this->free_service_table_(); + this->table_.free(); #endif // Always set: terminates any in-flight stream on every cache config. this->services_released_ = true; @@ -322,197 +318,19 @@ void BluedroidGattClient::release_services() { #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_())) { + // Lifetime: every teardown path (CLOSE_EVT, the safety timeout, stack-down, + // passive DISCONNECT) routes through release_services(), so a materialized + // table cannot outlive its link. + if (this->table_.empty() && + (this->services_released_ || this->service_total_ == 0 || + !this->table_.build(this->gattc_if_, this->conn_id_, this->service_total_, this->connection_index_))) { // Released / no services / failed build all collapse to empty; the // build failures warned above, log the quiet two. ESP_LOGD(TAG, "[%d] No service table (released=%d, services=%u)", this->connection_index_, this->services_released_, this->service_total_); 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(this->table_storage_), - reinterpret_cast(this->table_storage_ + svc_bytes), - reinterpret_cast(this->table_storage_ + svc_bytes + char_bytes), - this->service_total_, - this->table_char_total_, - this->table_desc_total_}; -} - -// Lifetime: every teardown path (CLOSE_EVT, the safety timeout, stack-down, -// passive DISCONNECT) routes through release_services(), so a materialized -// table cannot outlive its link. -void BluedroidGattClient::free_service_table_() { - if (this->table_storage_ == nullptr) { - return; - } - RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); - allocator.deallocate(this->table_storage_, 0); - this->table_storage_ = nullptr; - this->table_char_total_ = 0; - this->table_desc_total_ = 0; -} - -template -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; - auto svc_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &svc, &svc_count, s); - if (svc_status != ESP_GATT_OK || svc_count == 0) { - this->log_gattc_warning_("esp_ble_gattc_get_service", svc_status); - return false; - } - if (!on_service(s, svc)) { - return false; - } - uint16_t svc_chars = 0; - auto count_status = 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); - if (count_status != ESP_GATT_OK) { - this->log_gattc_warning_("esp_ble_gattc_get_attr_count", count_status); - 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_OK || char_count == 0) { - // An early terminator contradicts svc_chars from the same cache; - // never build a silently truncated table. - this->log_gattc_warning_("esp_ble_gattc_get_all_char", status); - return false; - } - if (!on_char(svc, chr)) { - return false; - } - for (uint16_t d = 0;; d++) { - if (d == MAX_DESCRIPTORS_PER_CHARACTERISTIC) { - // A stack that never reports end-of-range; fail like every other - // inconsistency instead of truncating the table silently. - ESP_LOGW(TAG, "[%d] Descriptor walk exceeded %u entries", this->connection_index_, - MAX_DESCRIPTORS_PER_CHARACTERISTIC); - return false; - } - 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) { - this->log_gattc_warning_("esp_ble_gattc_get_all_descr", desc_status); - 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) { - ESP_LOGW(TAG, "[%d] Service table walk failed during count", this->connection_index_); - 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 allocator(RAMAllocator::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(total_bytes)); - return false; - } - auto *services = reinterpret_cast(this->table_storage_); - auto *characteristics = reinterpret_cast(this->table_storage_ + svc_bytes); - auto *descriptors = - reinterpret_cast(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) { - // Walk error or the database changed between passes; better an empty - // table than a corrupt one. - ESP_LOGW(TAG, "[%d] Service table walk mismatch, discarding", this->connection_index_); - this->free_service_table_(); - return false; - } - this->table_char_total_ = char_total; - this->table_desc_total_ = desc_total; - return true; + return this->table_.view(); } #endif // USE_BLE_GATT_SERVICE_TABLE @@ -563,9 +381,9 @@ void BluedroidGattClient::log_gattc_warning_(const char *operation, int code) { int BluedroidGattClient::handle_search_cmpl_(esp_gatt_status_t status) { #ifdef USE_BLE_GATT_SERVICE_TABLE - // Re-discovery moves the counts table_view_() derives offsets from; free + // Re-discovery moves the counts the table view derives offsets from; free // the stale table. - this->free_service_table_(); + this->table_.free(); #endif // Step down from the fast discovery params. this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium"); diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h index 7595574611..5503b6026c 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h @@ -12,6 +12,7 @@ #if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT) #include "bluetooth_connection.h" +#include "gatt_service_table_bluedroid.h" #include "esphome/components/ble_device_base/ble_gatt_client.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -109,20 +110,11 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public 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 - 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 ble_device_base::GattClientListener *listener_{nullptr}; #ifdef USE_BLE_GATT_SERVICE_TABLE - // One exact-size block carved into the three arrays; the view is rebuilt - // per (cold) call instead of cached. - uint8_t *table_storage_{nullptr}; + BluedroidServiceTable table_; #endif // Group 2: 4-byte types uint32_t disconnecting_started_{0}; @@ -133,11 +125,6 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public // Group 4: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; 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 esp_gatt_if_t gattc_if_{ESP_GATT_IF_NONE}; // uint8_t width keeps the object at 48 bytes diff --git a/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.cpp b/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.cpp new file mode 100644 index 0000000000..cf774d1931 --- /dev/null +++ b/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.cpp @@ -0,0 +1,180 @@ +#include "gatt_service_table_bluedroid.h" + +#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT) && defined(USE_BLE_GATT_SERVICE_TABLE) + +#include "esphome/core/log.h" + +namespace esphome::bluetooth_connection { + +static const char *const TAG = "gatt_service_table"; + +// A stack that never reports end-of-range would otherwise walk forever. +static constexpr uint16_t MAX_DESCRIPTORS_PER_CHARACTERISTIC = 64; + +// Shared enumeration for both 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. +template +bool BluedroidServiceTable::walk_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc) { + for (uint16_t s = 0; s < this->service_total_; s++) { + esp_gattc_service_elem_t svc; + uint16_t svc_count = 1; + auto svc_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &svc, &svc_count, s); + if (svc_status != ESP_GATT_OK || svc_count == 0) { + this->log_walk_warning_("esp_ble_gattc_get_service", svc_status); + return false; + } + if (!on_service(s, svc)) { + return false; + } + uint16_t svc_chars = 0; + auto count_status = 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); + if (count_status != ESP_GATT_OK) { + this->log_walk_warning_("esp_ble_gattc_get_attr_count", count_status); + 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_OK || char_count == 0) { + // An early terminator contradicts svc_chars from the same cache; + // never build a silently truncated table. + this->log_walk_warning_("esp_ble_gattc_get_all_char", status); + return false; + } + if (!on_char(svc, chr)) { + return false; + } + for (uint16_t d = 0;; d++) { + if (d == MAX_DESCRIPTORS_PER_CHARACTERISTIC) { + // A stack that never reports end-of-range; fail like every other + // inconsistency instead of truncating the table silently. + ESP_LOGW(TAG, "[%d] Descriptor walk exceeded %u entries", this->log_index_, + MAX_DESCRIPTORS_PER_CHARACTERISTIC); + return false; + } + 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) { + this->log_walk_warning_("esp_ble_gattc_get_all_descr", desc_status); + return false; + } + if (!on_desc(chr, desc)) { + return false; + } + } + } + } + return true; +} + +bool BluedroidServiceTable::build(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t service_total, uint8_t log_index) { + this->free(); + this->gattc_if_ = gattc_if; + this->conn_id_ = conn_id; + this->service_total_ = service_total; + this->log_index_ = log_index; + + // 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_([](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) { + ESP_LOGW(TAG, "[%d] Service table walk failed during count", this->log_index_); + 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 allocator(RAMAllocator::ALLOC_INTERNAL); + this->storage_ = allocator.allocate(total_bytes); + if (this->storage_ == nullptr) { + ESP_LOGW(TAG, "[%d] Service table allocation failed (%u bytes)", this->log_index_, + static_cast(total_bytes)); + return false; + } + auto *services = reinterpret_cast(this->storage_); + auto *characteristics = reinterpret_cast(this->storage_ + svc_bytes); + auto *descriptors = reinterpret_cast(this->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_( + [&](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) { + // Walk error or the database changed between passes; better an empty + // table than a corrupt one. + ESP_LOGW(TAG, "[%d] Service table walk mismatch, discarding", this->log_index_); + this->free(); + return false; + } + this->char_total_ = char_total; + this->desc_total_ = desc_total; + return true; +} + +void BluedroidServiceTable::log_walk_warning_(const char *operation, int code) { + ESP_LOGW(TAG, "[%d] %s failed, status=%d", this->log_index_, operation, code); +} + +} // namespace esphome::bluetooth_connection + +#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT && USE_BLE_GATT_SERVICE_TABLE diff --git a/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h b/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h new file mode 100644 index 0000000000..b9992f6d35 --- /dev/null +++ b/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h @@ -0,0 +1,71 @@ +// Owning two-pass materializer of one Bluedroid GATT database snapshot into +// the neutral GattServiceTable layout, shared by the BluedroidGattClient +// backend and ble_client's esp32 engine. + +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT) && defined(USE_BLE_GATT_SERVICE_TABLE) + +#include "esphome/components/ble_device_base/ble_gatt_client.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome::bluetooth_connection { + +class BluedroidServiceTable { + public: + ~BluedroidServiceTable() { this->free(); } + + /// Two-pass build from the stack's cached database. service_total MUST be + /// the esp_ble_gattc_get_attr_count(PRIMARY)+(SECONDARY) total, never the + /// SEARCH_RES event count. log_index labels warnings. Frees any previous + /// table first; on failure the table is left empty. + bool build(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t service_total, uint8_t log_index); + + // 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 view() const { + size_t svc_bytes = this->service_total_ * sizeof(ble_device_base::GattService); + size_t char_bytes = this->char_total_ * sizeof(ble_device_base::GattCharacteristic); + return {reinterpret_cast(this->storage_), + reinterpret_cast(this->storage_ + svc_bytes), + reinterpret_cast(this->storage_ + svc_bytes + char_bytes), + this->service_total_, + this->char_total_, + this->desc_total_}; + } + + void free() { + if (this->storage_ == nullptr) { + return; + } + RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); + allocator.deallocate(this->storage_, 0); + this->storage_ = nullptr; + this->char_total_ = 0; + this->desc_total_ = 0; + } + + bool empty() const { return this->storage_ == nullptr; } + + private: + template + bool walk_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc); + void log_walk_warning_(const char *operation, int code); + + uint8_t *storage_{nullptr}; + uint16_t service_total_{0}; + uint16_t char_total_{0}; + uint16_t desc_total_{0}; + // Walk context, set by build(). + esp_gatt_if_t gattc_if_{}; + uint16_t conn_id_{0}; + uint8_t log_index_{0}; +}; + +} // namespace esphome::bluetooth_connection + +#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT && USE_BLE_GATT_SERVICE_TABLE