Address review: empty-table failure semantics, async abort guard, StaticVector nodes

- A zero-service table on a client with nodes is treated as a failed
  discovery (warn, release, charge the backoff, disconnect): a real GATT
  peer always exposes at least GAP/GATT, so an empty table means the
  materialization failed and the connection must retry instead of
  sitting inert behind a successful on_connect
- The fan-out abort guard also tests cancel_requested_, covering the
  normal async teardown a node starts from on_connected (the state-only
  check caught just the synchronous refusal)
- nodes_ becomes a StaticVector sized by ESPHOME_BLE_CLIENT_MAX_NODES:
  the client requests a baseline slot, each neutral write action (which
  registers itself in its constructor) requests one, and the define
  rides defines.h for analysis - no realloc machinery on the rp2 target
- The Bluedroid empty-table return logs its quiet cases; the
  service_table=False stub notes the misconfiguration it implies
This commit is contained in:
J. Nick Koston
2026-08-09 18:34:13 -05:00
parent 45d921f77a
commit 5ec7047151
6 changed files with 28 additions and 6 deletions
@@ -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)
@@ -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_);
@@ -23,7 +23,6 @@
#include <cstdint>
#include <functional>
#include <vector>
namespace esphome::ble_client {
@@ -133,7 +132,8 @@ class BLEClient : public Component,
// Group 1: pointers / containers
ble_device_base::BLEGattConnection *backend_{nullptr};
std::vector<BLEClientNode *> nodes_; // filled during setup, never after
// Codegen-sized (ESPHOME_BLE_CLIENT_MAX_NODES); filled during setup.
StaticVector<BLEClientNode *, ESPHOME_BLE_CLIENT_MAX_NODES> nodes_;
// Group 2: 8-byte types
uint64_t address_{0};
@@ -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_();
@@ -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();
+1
View File
@@ -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