diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 4ccf860995..98f2ceccda 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -338,13 +338,13 @@ async def register_ble_node(var, config): def _request_gatt_node_build() -> None: - """Node storage plus, on esp32, the bridge and shared-materializer - defines one neutral node needs compiled in.""" + """Node storage and the one define meaning "the neutral node surface is + compiled in", plus the esp32 bridge/materializer defines.""" _request_node_slot() + cg.add_define("USE_BLE_CLIENT_GATT_NODES") if CORE.is_esp32: # Deliberately not ble_device_base.request_gatt_client(): that would # claim a phantom backend slot on combined proxy builds. - cg.add_define("USE_BLE_CLIENT_GATT_NODES") cg.add_define("USE_BLE_GATT_CLIENT") cg.add_define("USE_BLE_GATT_SERVICE_TABLE") @@ -538,7 +538,9 @@ _request_node_slot = cg.slot_counter("ESPHOME_BLE_CLIENT_MAX_NODES") async def _to_code_gatt(config: ConfigType) -> cg.MockObj: - _request_node_slot() + # The engine always carries the node surface (the client itself owns the + # baseline slot). + _request_gatt_node_build() backend = await bluetooth_connection.new_gatt_backend(config) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 4a68a5e3a6..f4585d01e2 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -2,13 +2,8 @@ #ifdef USE_ESP32 -#include -#include - #include "esphome/core/automation.h" #include "esphome/components/ble_client/ble_client.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" namespace esphome::ble_client { diff --git a/esphome/components/ble_client/automation_gatt.h b/esphome/components/ble_client/automation_gatt.h index d4666c59f0..9ea2518ccf 100644 --- a/esphome/components/ble_client/automation_gatt.h +++ b/esphome/components/ble_client/automation_gatt.h @@ -12,12 +12,9 @@ #if defined(USE_BLE_GATT_CLIENT) && !defined(USE_ESP32) #include -#include #include "ble_client_gatt.h" #include "esphome/core/automation.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" namespace esphome::ble_client { diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index c248fd1148..5ad53ccc09 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -9,6 +9,7 @@ #ifdef USE_BLE_CLIENT_GATT_NODES #include "esphome/components/bluetooth_connection/bluetooth_connection.h" +#include "esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h" #endif namespace esphome::ble_client { @@ -60,7 +61,8 @@ bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es int err = param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status; for (auto *node : this->gatt_nodes_) node->on_notify_state(param->reg_for_notify.handle, true, err); - // A retiring last registration must still release the cache. + // A retiring last registration must still release the cache (the counter + // was cleared above, before the node dispatch). this->maybe_release_services_(); return true; } @@ -68,18 +70,20 @@ bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param)) return false; - for (auto *node : this->nodes_) - node->gattc_event_handler(event, esp_gattc_if, param); #ifdef USE_BLE_CLIENT_GATT_NODES + // Before the legacy fan-out so gatt nodes resolve their handles before any + // legacy trigger (e.g. on_connect at SEARCH_CMPL) fires - the neutral + // engine's order. this->dispatch_gatt_event_(event, param); #endif + for (auto *node : this->nodes_) + node->gattc_event_handler(event, esp_gattc_if, param); this->maybe_release_services_(); return true; } void BLEClient::maybe_release_services_() { // The release frees the GATT cache that BLEClientBase's CCCD lookup still needs. - // The last REG_FOR_NOTIFY event clears the counter before node dispatch, so the release still runs here. if (!this->services_.empty() && !this->notify_registration_pending() && this->all_nodes_established_()) { this->release_services(); ESP_LOGD(TAG, "All clients established, services released"); @@ -102,14 +106,10 @@ void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_p void BLEClient::set_state(espbt::ClientState state) { BLEClientBase::set_state(state); + // ESTABLISHED never flows through here (the base sets it internally); gatt + // nodes (members of nodes_ too) are promoted after the on_connected fan-out. for (auto &node : nodes_) node->node_state = state; -#ifdef USE_BLE_CLIENT_GATT_NODES - // ESTABLISHED never flows through here (the base sets it internally); - // gatt nodes are promoted explicitly after the on_connected fan-out. - for (auto *node : this->gatt_nodes_) - node->node_state = state; -#endif } bool BLEClient::all_nodes_established_() { @@ -119,19 +119,12 @@ bool BLEClient::all_nodes_established_() { if (node->node_state != espbt::ClientState::ESTABLISHED) return false; } -#ifdef USE_BLE_CLIENT_GATT_NODES - for (auto *node : this->gatt_nodes_) { - if (node->node_state != espbt::ClientState::ESTABLISHED) - return false; - } -#endif return true; } #ifdef USE_BLE_CLIENT_GATT_NODES void BLEClient::register_gatt_node(BLEClientNode *node) { - node->set_ble_client_parent(this); if (this->gatt_nodes_.size() == ESPHOME_BLE_CLIENT_MAX_NODES) { // push_back past capacity is a silent no-op; an undersized slot count // must be loud at boot, not an unresolvable node at runtime. @@ -139,22 +132,21 @@ void BLEClient::register_gatt_node(BLEClientNode *node) { return; } this->gatt_nodes_.push_back(node); + // Membership in nodes_ covers the shared state bookkeeping (node_state, + // release condition); gatt_nodes_ is the neutral fan-out subset. + this->register_ble_node(node); } bool BLEClient::take_pending_gatt_reg_(uint16_t handle) { - bool found = false; - // Erase every match: a stale duplicate must never swallow a later legacy - // registration of the same handle. - auto ®s = this->pending_gatt_regs_; - for (auto it = regs.begin(); it != regs.end();) { - if (*it == handle) { - it = regs.erase(it); - found = true; - } else { - ++it; + // No duplicates possible: notify_characteristic() refuses a re-push while + // one is pending, so a single swap-with-last removal suffices. + for (uint8_t i = 0; i < this->pending_gatt_reg_count_; i++) { + if (this->pending_gatt_regs_[i] == handle) { + this->pending_gatt_regs_[i] = this->pending_gatt_regs_[--this->pending_gatt_reg_count_]; + return true; } } - return found; + return false; } void BLEClient::dispatch_gatt_event_(esp_gattc_cb_event_t event, esp_ble_gattc_cb_param_t *param) { @@ -199,18 +191,14 @@ void BLEClient::dispatch_gatt_event_(esp_gattc_cb_event_t event, esp_ble_gattc_c void BLEClient::handle_gatt_search_cmpl_(esp_gatt_status_t status) { // The base establishes without reading the search status; the neutral // contract treats a failed or empty discovery as a failed connection. - uint16_t primary = 0; - uint16_t secondary = 0; - bool counted = status == ESP_GATT_OK && - esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_PRIMARY_SERVICE, 0x0001, - 0xFFFF, 0, &primary) == ESP_GATT_OK && - esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_SECONDARY_SERVICE, 0x0001, - 0xFFFF, 0, &secondary) == ESP_GATT_OK; + uint16_t service_total = 0; + bool counted = status == ESP_GATT_OK && bluetooth_connection::BluedroidServiceTable::count_services( + this->gattc_if_, this->conn_id_, &service_total); // Stack-owned: freed on scope exit; nodes copy their handles during // on_connected() per the borrowed-table contract. bluetooth_connection::BluedroidServiceTable table; - if (!counted || primary + secondary == 0 || - !table.build(this->gattc_if_, this->conn_id_, primary + secondary, this->connection_index_)) { + if (!counted || service_total == 0 || + !table.build(this->gattc_if_, this->conn_id_, service_total, this->connection_index_)) { ESP_LOGW(TAG, "[%s] Service table is empty; treating as failed discovery", this->address_str()); this->disconnect(); return; @@ -233,7 +221,7 @@ void BLEClient::handle_gatt_search_cmpl_(esp_gatt_status_t status) { } void BLEClient::on_disconnect_complete(esp_err_t reason) { - this->pending_gatt_regs_.clear(); + this->pending_gatt_reg_count_ = 0; if (!this->gatt_connected_) return; // Never-established links report nothing (neutral parity). this->gatt_connected_ = false; @@ -241,45 +229,68 @@ void BLEClient::on_disconnect_complete(esp_err_t reason) { node->on_disconnected(); } +int BLEClient::check_gatt_op_(const char *operation, esp_err_t err) { + if (err != ESP_OK) + this->log_gattc_warning_(operation, err); + return err; +} + int BLEClient::write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) { if (this->conn_id_ == UNSET_CONN_ID) return ble_device_base::GATT_ERR_NOT_CONNECTED; - return esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, len, const_cast(data), - response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, - ESP_GATT_AUTH_REQ_NONE); + return this->check_gatt_op_( + "esp_ble_gattc_write_char", + esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, len, const_cast(data), + response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, + ESP_GATT_AUTH_REQ_NONE)); } int BLEClient::read_characteristic(uint16_t handle) { if (this->conn_id_ == UNSET_CONN_ID) return ble_device_base::GATT_ERR_NOT_CONNECTED; - return esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); + return this->check_gatt_op_("esp_ble_gattc_read_char", + esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE)); } int BLEClient::read_descriptor(uint16_t handle) { if (this->conn_id_ == UNSET_CONN_ID) return ble_device_base::GATT_ERR_NOT_CONNECTED; - return esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); + return this->check_gatt_op_( + "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 BLEClient::write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) { if (this->conn_id_ == UNSET_CONN_ID) return ble_device_base::GATT_ERR_NOT_CONNECTED; - return esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, handle, len, const_cast(data), - ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + return this->check_gatt_op_( + "esp_ble_gattc_write_char_descr", + esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, handle, len, const_cast(data), + ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE)); } int BLEClient::notify_characteristic(uint16_t handle, bool enable) { if (this->conn_id_ == UNSET_CONN_ID) return ble_device_base::GATT_ERR_NOT_CONNECTED; if (enable) { + for (uint8_t i = 0; i < this->pending_gatt_reg_count_; i++) { + if (this->pending_gatt_regs_[i] == handle) { + ESP_LOGW(TAG, "[%s] Notify registration already pending for handle 0x%04x", this->address_str(), handle); + return ESP_OK; + } + } + if (this->pending_gatt_reg_count_ == ESPHOME_BLE_CLIENT_MAX_NODES) { + // Registering without a pending entry would let the base's automatic + // CCCD write through; refuse loudly instead. + ESP_LOGE(TAG, "[%s] Too many pending notify registrations", this->address_str()); + return ble_device_base::GATT_ERR_NO_MEMORY; + } // Reuses the base helper so its pending count holds the service-release // until the registration completes; the completion is intercepted before // the base's automatic CCCD write. - if (this->take_pending_gatt_reg_(handle)) - ESP_LOGW(TAG, "[%s] Duplicate notify registration for handle 0x%04x", this->address_str(), handle); esp_err_t err = this->register_for_notify(handle); if (err == ESP_OK) - this->pending_gatt_regs_.push_back(handle); + this->pending_gatt_regs_[this->pending_gatt_reg_count_++] = handle; return err; } return esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle); diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index de25d912be..5a5ef540ba 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -10,10 +10,6 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#ifdef USE_BLE_CLIENT_GATT_NODES -#include "esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h" -#endif - #include #include #include @@ -65,6 +61,7 @@ class BLEClient final : public BLEClientBase { /// Local registration only; per the neutral contract the CCCD write is the /// node's job (the legacy auto-CCCD is suppressed for these handles). int notify_characteristic(uint16_t handle, bool enable); + // pair() comes from BLEClientBase, matching the neutral engine's. int unpair(); #endif @@ -72,6 +69,7 @@ class BLEClient final : public BLEClientBase { bool all_nodes_established_(); void maybe_release_services_(); #ifdef USE_BLE_CLIENT_GATT_NODES + int check_gatt_op_(const char *operation, esp_err_t err); void dispatch_gatt_event_(esp_gattc_cb_event_t event, esp_ble_gattc_cb_param_t *param); void handle_gatt_search_cmpl_(esp_gatt_status_t status); bool take_pending_gatt_reg_(uint16_t handle); @@ -83,8 +81,10 @@ class BLEClient final : public BLEClientBase { // Nodes on the neutral surface; fed the translated callbacks and // auto-established after the on_connected fan-out. StaticVector gatt_nodes_; - // Bridge-initiated notify registrations awaiting REG_FOR_NOTIFY_EVT. - std::vector pending_gatt_regs_; + // Bridge-initiated notify registrations awaiting REG_FOR_NOTIFY_EVT; + // bounded by the node count (one in-flight registration per node). + uint16_t pending_gatt_regs_[ESPHOME_BLE_CLIENT_MAX_NODES]; + uint8_t pending_gatt_reg_count_{0}; // on_connected fan-out started; on_disconnected is owed at teardown. bool gatt_connected_{false}; #endif diff --git a/esphome/components/ble_client/ble_client_node.h b/esphome/components/ble_client/ble_client_node.h index 71f2ae59c8..40786cce73 100644 --- a/esphome/components/ble_client/ble_client_node.h +++ b/esphome/components/ble_client/ble_client_node.h @@ -24,7 +24,7 @@ class BLEClient; class BLEClientNode { public: -#if defined(USE_BLE_CLIENT_GATT_NODES) || (defined(USE_BLE_GATT_CLIENT) && !defined(USE_ESP32)) +#ifdef USE_BLE_CLIENT_GATT_NODES // 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 diff --git a/esphome/components/ble_client/ble_write_action.h b/esphome/components/ble_client/ble_write_action.h index 8789846678..5a5d030e29 100644 --- a/esphome/components/ble_client/ble_write_action.h +++ b/esphome/components/ble_client/ble_write_action.h @@ -6,14 +6,14 @@ #include "esphome/core/defines.h" -#if defined(USE_BLE_CLIENT_GATT_NODES) || (defined(USE_BLE_GATT_CLIENT) && !defined(USE_ESP32)) +#ifdef USE_BLE_CLIENT_GATT_NODES #include #include -// One of the two resolves per build; both define class Automation's TAG box. -#include "automation.h" -#include "automation_gatt.h" +// One of the two engine headers resolves per build. +#include "ble_client.h" +#include "ble_client_gatt.h" #include "ble_client_node.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" @@ -21,6 +21,8 @@ namespace esphome::ble_client { +static const char *const BLE_WRITE_TAG = "ble_client.automation"; + // Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars) static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64; @@ -75,20 +77,20 @@ template class BLEClientWriteAction final : public Action // handle is armed before the backend is touched. bool write(const uint8_t *data, size_t len) { if (!this->ble_client_->connected()) { - esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); + esph_log_w(BLE_WRITE_TAG, "Cannot write to BLE characteristic - not connected"); return false; } if (!this->resolved_) { - esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - characteristic was not resolved"); + esph_log_w(BLE_WRITE_TAG, "Cannot write to BLE characteristic - characteristic was not resolved"); return false; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)]; - esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len)); + esph_log_vv(BLE_WRITE_TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len)); #endif int err = this->ble_client_->write_characteristic(this->char_handle_, data, len, this->write_response_); if (err != 0) { - esph_log_e(Automation::TAG, "Error writing to characteristic: %d!", err); + esph_log_e(BLE_WRITE_TAG, "Error writing to characteristic: %d!", err); return false; } return true; @@ -101,7 +103,7 @@ template class BLEClientWriteAction final : public Action if (chr == nullptr) { char char_buf[ble_device_base::UUID_STR_LEN]; char service_buf[ble_device_base::UUID_STR_LEN]; - esph_log_w("ble_write_action", "Characteristic %s was not found in service %s", this->char_uuid_.to_str(char_buf), + esph_log_w(BLE_WRITE_TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_str(char_buf), this->service_uuid_.to_str(service_buf)); return; } @@ -111,13 +113,13 @@ template class BLEClientWriteAction final : public Action this->write_response_ = false; } else { char char_buf[ble_device_base::UUID_STR_LEN]; - esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf)); + esph_log_e(BLE_WRITE_TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf)); return; } this->char_handle_ = chr->value_handle; this->resolved_ = true; char char_buf[ble_device_base::UUID_STR_LEN]; - esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf), + esph_log_d(BLE_WRITE_TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf), this->ble_client_->address_str()); } @@ -135,13 +137,13 @@ template class BLEClientWriteAction final : public Action if (!this->resolved_ || handle != this->char_handle_) { // A parked chain waiting on a completion that never matches would // otherwise stall silently until disconnect. - esph_log_d(Automation::TAG, "Write result for handle 0x%04x ignored, waiting on 0x%04x", handle, + esph_log_d(BLE_WRITE_TAG, "Write result for handle 0x%04x ignored, waiting on 0x%04x", handle, this->char_handle_); return; } if (error != 0) { // Continue the chain (legacy parity) but leave a breadcrumb. - esph_log_w(Automation::TAG, "Write completed with status %d", error); + esph_log_w(BLE_WRITE_TAG, "Write completed with status %d", error); } this->ble_client_->run_later([this]() { this->play_next_tuple_(this->var_); }); } @@ -163,4 +165,4 @@ template class BLEClientWriteAction final : public Action } // namespace esphome::ble_client -#endif // USE_BLE_CLIENT_GATT_NODES || (USE_BLE_GATT_CLIENT && !USE_ESP32) +#endif // USE_BLE_CLIENT_GATT_NODES diff --git a/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.cpp b/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.cpp index cf774d1931..e80f060dbf 100644 --- a/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.cpp @@ -76,6 +76,20 @@ bool BluedroidServiceTable::walk_(ServiceFn &&on_service, CharFn &&on_char, Desc return true; } +bool BluedroidServiceTable::count_services(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t *total) { + uint16_t primary = 0; + uint16_t secondary = 0; + if (esp_ble_gattc_get_attr_count(gattc_if, conn_id, ESP_GATT_DB_PRIMARY_SERVICE, 0x0001, 0xFFFF, 0, &primary) != + ESP_GATT_OK || + esp_ble_gattc_get_attr_count(gattc_if, conn_id, ESP_GATT_DB_SECONDARY_SERVICE, 0x0001, 0xFFFF, 0, &secondary) != + ESP_GATT_OK) { + // A failed count must not read as an authoritative empty database. + return false; + } + *total = primary + secondary; + 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; diff --git a/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h b/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h index b9992f6d35..49d9f7f6e9 100644 --- a/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h +++ b/esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h @@ -19,10 +19,13 @@ 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. + /// The service count build() requires: the stack's PRIMARY+SECONDARY + /// attribute totals, never the SEARCH_RES event count. + static bool count_services(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t *total); + + /// Two-pass build from the stack's cached database (service_total from + /// count_services()). 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 @@ -45,6 +48,7 @@ class BluedroidServiceTable { RAMAllocator allocator(RAMAllocator::ALLOC_INTERNAL); allocator.deallocate(this->storage_, 0); this->storage_ = nullptr; + this->service_total_ = 0; this->char_total_ = 0; this->desc_total_ = 0; } diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bcaddbca34..f33f30257d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -488,6 +488,7 @@ #define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 #define USE_BLE_SCAN_RESPONSE_MERGER #define USE_BLE_GATT_CLIENT +#define USE_BLE_CLIENT_GATT_NODES #define ESPHOME_BLE_GATT_CLIENT_COUNT 3 #define ESPHOME_BLE_CLIENT_MAX_NODES 1 #define USE_RP2040_VARIANT_RP2040