diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 9e90d3c236..828bd0b7b2 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -350,6 +350,9 @@ async def ble_connect_to_code(config, action_id, template_arg, args): ) async def ble_write_to_code(config, action_id, template_arg, args): parent = await cg.get_variable(config[CONF_ID]) + if not CORE.is_esp32: + # The neutral action registers itself as a node in its constructor. + _request_node_slot() var = cg.new_Pvariable(action_id, template_arg, parent) value = config[CONF_VALUE] @@ -448,7 +451,13 @@ async def _to_code_esp32(config: ConfigType) -> cg.MockObj: return var +# Sizes the neutral client's node storage; the client itself requests a +# baseline slot so the define exists on every build that compiles the engine. +_request_node_slot = cg.slot_counter("ESPHOME_BLE_CLIENT_MAX_NODES") + + async def _to_code_gatt(config: ConfigType) -> cg.MockObj: + _request_node_slot() 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/ble_client_gatt.cpp b/esphome/components/ble_client/ble_client_gatt.cpp index 1874b17daa..095899f78a 100644 --- a/esphome/components/ble_client/ble_client_gatt.cpp +++ b/esphome/components/ble_client/ble_client_gatt.cpp @@ -160,13 +160,19 @@ void BLEClient::on_service_discovery_done(int error) { // would pay the build/free cycle on every (re)connect for nothing. auto table = this->backend_->get_service_table(); if (table.service_count == 0) { - // A service-less device or a failed materialization: either way the - // nodes cannot resolve, so say so instead of sitting inert. - ESP_LOGW(TAG, "[%s] Service table is empty; nodes will not resolve", this->address_str_); + // A failed materialization is indistinguishable from a service-less + // peer, and a real GATT peer always exposes at least GAP/GATT: treat + // it as a discovery failure so the connection retries instead of + // sitting inert behind a successful-looking on_connect. + ESP_LOGW(TAG, "[%s] Service table is empty; treating as failed discovery", this->address_str_); + this->backend_->release_services(); + this->register_failure_(); + this->disconnect(); + return; } for (auto *node : this->nodes_) { node->on_connected(table); - if (this->state_ != State::CONNECTED) { + if (this->state_ != State::CONNECTED || this->cancel_requested_) { // A node tore the link down mid-fan-out: on_disconnect fires with no // preceding on_connect, so leave a trace of why. ESP_LOGW(TAG, "[%s] A node aborted the connection during setup", this->address_str_); diff --git a/esphome/components/ble_client/ble_client_gatt.h b/esphome/components/ble_client/ble_client_gatt.h index a72a0cd1e6..4f8a6f08a6 100644 --- a/esphome/components/ble_client/ble_client_gatt.h +++ b/esphome/components/ble_client/ble_client_gatt.h @@ -23,7 +23,6 @@ #include #include -#include namespace esphome::ble_client { @@ -133,7 +132,8 @@ class BLEClient : public Component, // Group 1: pointers / containers ble_device_base::BLEGattConnection *backend_{nullptr}; - std::vector nodes_; // filled during setup, never after + // Codegen-sized (ESPHOME_BLE_CLIENT_MAX_NODES); filled during setup. + StaticVector nodes_; // Group 2: 8-byte types uint64_t address_{0}; diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index a27aab1f95..396f969938 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -324,6 +324,10 @@ void BluedroidGattClient::release_services() { ble_device_base::GattServiceTable BluedroidGattClient::get_service_table() { if (this->table_storage_ == nullptr && (this->services_released_ || this->service_total_ == 0 || !this->build_service_table_())) { + // 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_(); diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h index 0f92854690..3fd4dc67e7 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h @@ -80,6 +80,8 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public #ifdef USE_BLE_GATT_SERVICE_TABLE ble_device_base::GattServiceTable get_service_table(); #else + // A direct consumer reaching this stub misconfigured its codegen + // (service_table=False): the empty table reads as a service-less peer. ble_device_base::GattServiceTable get_service_table() { return {}; } #endif void release_services(); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index e87c8044f4..84af39a01d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -474,6 +474,7 @@ #define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 #define USE_BLE_GATT_CLIENT #define ESPHOME_BLE_GATT_CLIENT_COUNT 1 +#define ESPHOME_BLE_CLIENT_MAX_NODES 1 #define USE_RP2040_VARIANT_RP2040 #define USE_SPI #ifndef USE_ETHERNET