From 9dafff6a615503c67b38d463d9b085e98d6fa45e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Aug 2026 22:32:36 -0500 Subject: [PATCH] Apply the simplify findings and fix the passive esp32 build --- .../bluetooth_connection.h | 9 ++-- .../bluetooth_connection_bluedroid.cpp | 47 ++++++------------- .../bluetooth_connection_bluedroid.h | 1 + .../bluetooth_connection_esp32.cpp | 12 +---- .../bluetooth_connection_gatt_backend.h | 3 ++ .../bluetooth_connection_hub.h | 46 ++++++------------ .../bluetooth_connection_rp2.h | 5 ++ .../components/bluetooth_proxy/__init__.py | 33 ++++++++----- .../bluetooth_proxy/bluetooth_proxy.cpp | 19 +++++--- .../bluetooth_proxy/bluetooth_proxy.h | 3 -- .../test-passive.esp32-c6-idf.yaml | 10 ++++ 11 files changed, 87 insertions(+), 101 deletions(-) create mode 100644 tests/components/bluetooth_proxy/test-passive.esp32-c6-idf.yaml diff --git a/esphome/components/bluetooth_connection/bluetooth_connection.h b/esphome/components/bluetooth_connection/bluetooth_connection.h index 2125d5b34f..c7bda55047 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection.h @@ -16,10 +16,11 @@ #include #endif -// A GATT connection backend exists in this build: esp32 (Bluedroid) or a hub -// platform with the neutral GATT client compiled in. Single-sourced here so -// the proxy and this component cannot drift. -#if defined(USE_ESP32) || defined(USE_BLE_GATT_CLIENT) +// The connection-aware API request handlers are compiled: a GATT backend is +// wired by codegen (one slot per connection). Advertisement-only builds get +// the clean-error handlers; address-scoped maintenance (unpair, cache clear) +// still works there through the per-platform free functions below. +#ifdef USE_BLE_GATT_CLIENT #define BLUETOOTH_CONNECTION_HAS_GATT #endif diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index 8c10eb3f25..0bcaae7717 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -246,9 +246,7 @@ void BluedroidGattClient::set_disconnecting_() { } void BluedroidGattClient::report_connection_state_(bool connected, int error) { - if (this->listener_ != nullptr) { - this->listener_->on_connection_state(connected, this->mtu_, error); - } + this->listener_->on_connection_state(connected, this->mtu_, error); } esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, @@ -286,9 +284,7 @@ void BluedroidGattClient::handle_search_cmpl_() { esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_SECONDARY_SERVICE, 0x0001, 0xFFFF, 0, &secondary); this->service_total_ = primary + secondary; - if (this->listener_ != nullptr) { - this->listener_->on_service_discovery_done(0); - } + this->listener_->on_service_discovery_done(0); } void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) { @@ -554,46 +550,35 @@ bool BluedroidGattClient::handle_gattc_event_(esp_gattc_cb_event_t event, esp_ga case ESP_GATTC_READ_DESCR_EVT: { if (this->conn_id_ != param->read.conn_id) return false; - if (this->listener_ != nullptr) { - bool ok = param->read.status == ESP_GATT_OK; - this->listener_->on_read_result(param->read.handle, ok ? param->read.value : nullptr, - ok ? param->read.value_len : 0, ok ? 0 : param->read.status); - } + bool ok = param->read.status == ESP_GATT_OK; + this->listener_->on_read_result(param->read.handle, ok ? param->read.value : nullptr, + ok ? param->read.value_len : 0, ok ? 0 : param->read.status); break; } case ESP_GATTC_WRITE_CHAR_EVT: case ESP_GATTC_WRITE_DESCR_EVT: { if (this->conn_id_ != param->write.conn_id) return false; - if (this->listener_ != nullptr) { - this->listener_->on_write_result(param->write.handle, - param->write.status == ESP_GATT_OK ? 0 : param->write.status); - } + this->listener_->on_write_result(param->write.handle, + param->write.status == ESP_GATT_OK ? 0 : param->write.status); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - if (this->listener_ != nullptr) { - this->listener_->on_notify_state( - param->reg_for_notify.handle, true, - param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status); - } + this->listener_->on_notify_state(param->reg_for_notify.handle, true, + param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { - if (this->listener_ != nullptr) { - this->listener_->on_notify_state( - param->unreg_for_notify.handle, false, - param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status); - } + this->listener_->on_notify_state( + param->unreg_for_notify.handle, false, + param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status); break; } case ESP_GATTC_NOTIFY_EVT: { if (this->conn_id_ != param->notify.conn_id) return false; ESP_LOGV(TAG, "[%d] NOTIFY_EVT handle=0x%2X", this->connection_index_, param->notify.handle); - if (this->listener_ != nullptr) { - this->listener_->on_notify_data(param->notify.handle, param->notify.value, param->notify.value_len); - } + this->listener_->on_notify_data(param->notify.handle, param->notify.value, param->notify.value_len); break; } default: @@ -614,10 +599,8 @@ void BluedroidGattClient::handle_gap_event_(esp_gap_ble_cb_event_t event, esp_bl case ESP_GAP_BLE_AUTH_CMPL_EVT: { if (!this->check_addr_(param->ble_security.auth_cmpl.bd_addr)) break; - if (this->listener_ != nullptr) { - this->listener_->on_pairing_result( - param->ble_security.auth_cmpl.success ? 0 : param->ble_security.auth_cmpl.fail_reason); - } + this->listener_->on_pairing_result( + param->ble_security.auth_cmpl.success ? 0 : param->ble_security.auth_cmpl.fail_reason); break; } default: diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h index 2c50123b8f..3e0494f371 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h @@ -52,6 +52,7 @@ class BluedroidGattClient final : public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_BLUETOOTH; } + // Wired by codegen before setup and invariant for the device lifetime. void set_listener(BluetoothConnection *listener) { this->listener_ = listener; } esp32_ble_tracker::ESPBTClient *tracker_client() { return &this->shim_; } diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp index ca5ad42982..142b1b65d9 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp @@ -12,23 +12,15 @@ namespace esphome::bluetooth_connection { -namespace { -void address_to_bda(uint64_t address, esp_bd_addr_t &bda) { - for (uint8_t i = 0; i < 6; i++) { - bda[i] = (address >> ((5 - i) * 8)) & 0xFF; - } -} -} // namespace - conn_err_t unpair_device(uint64_t address) { esp_bd_addr_t bda; - address_to_bda(address, bda); + ble_device_base::uint64_to_mac_msb_first(address, bda); return esp_ble_remove_bond_device(bda); } conn_err_t clear_gatt_cache(uint64_t address) { esp_bd_addr_t bda; - address_to_bda(address, bda); + ble_device_base::uint64_to_mac_msb_first(address, bda); esp_ble_gattc_cache_clean(bda); return CONN_OK; } diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h b/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h index 32ddf5e577..0a54622d8e 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h @@ -46,6 +46,9 @@ class StubGattBackend { return ble_device_base::GATT_ERR_NOT_CONNECTED; } ble_device_base::GattServiceTable get_service_table() { return {}; } + bool disconnect_pending() const { return false; } + void cancel_pending_disconnect() {} + void set_connection_type(ble_device_base::ConnectionType ct) {} void release_services() {} }; diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h index 985326f13f..fa903685f4 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h @@ -1,8 +1,7 @@ -// Hub-platform BluetoothConnection: drives the build's GATT backend (the +// BluetoothConnection: drives the build's GATT backend (the // ble_device_base::BLEGattConnection alias) and translates its events into -// the same API messages the esp32 class emits. -// Presents the identical method surface, so the proxy's GATT dispatch -// compiles against either class unchanged. +// the proxy's API messages. One wrapper for every platform; per-backend +// differences live behind the alias and the streamer cut-through. #pragma once @@ -52,11 +51,8 @@ class BluetoothConnection final { bool is_paired() const { return this->paired_; } void set_unpaired() { this->paired_ = false; } conn_err_t pair() { return this->backend_->pair(); } - // Backends with deferred-disconnect state (bluedroid) answer through the - // detected forwards; the rest have nothing to track. Templates so the - // discarded branch is not odr-checked against backends without the methods. - bool disconnect_pending() const { return disconnect_pending_(this->backend_); } - void cancel_pending_disconnect() { cancel_pending_(this->backend_); } + bool disconnect_pending() const { return this->backend_->disconnect_pending(); } + void cancel_pending_disconnect() { this->backend_->cancel_pending_disconnect(); } void set_address(uint64_t address); uint64_t get_address() const { return this->address_; } @@ -69,8 +65,8 @@ class BluetoothConnection final { void set_connection_type(ConnectionType ct) { this->connection_type_ = ct; // The bluedroid backend branches on the type itself (prefer-params and - // the with-cache report at OPEN_EVT). - forward_connection_type_(this->backend_, ct); + // the with-cache report at OPEN_EVT); the others ignore it. + this->backend_->set_connection_type(ct); } // Latched at discovery completion rather than read from the backend table: // streaming frees the table, and this must stay true for the connection's @@ -79,14 +75,17 @@ class BluetoothConnection final { bool has_gatt_services() const { return this->services_discovered_; } /// Stream any pending service-discovery batch and police the disconnect - /// safety timeout. Called from the proxy's loop — hub connections have no - /// Component loop of their own (the esp32 class streams from its own - /// loop() and has the same 10 s safety net in its base class). + /// safety timeout. Called from the proxy's loop — the wrapper has no + /// Component loop of its own. void process_pending_services() { if (this->send_service_ >= 0) { this->stream_pending_(this->backend_); } - this->check_disconnect_timeout_(); + // Inline state gate: this runs per loop iteration for every slot, and the + // 10 s safety net only matters while DISCONNECTING. + if (this->state_ == ClientState::DISCONNECTING) { + this->check_disconnect_timeout_(); + } } // ---- backend event sink (called directly by the backend, main loop) ---- @@ -108,23 +107,6 @@ class BluetoothConnection final { // response in place from its stack cache; the rest use the table streamer. // Template so the discarded branch is not odr-checked against backends // that lack the method. - template static bool disconnect_pending_(Backend *backend) { - if constexpr (requires { backend->disconnect_pending(); }) { - return backend->disconnect_pending(); - } else { - return false; - } - } - template static void cancel_pending_(Backend *backend) { - if constexpr (requires { backend->cancel_pending_disconnect(); }) { - backend->cancel_pending_disconnect(); - } - } - template static void forward_connection_type_(Backend *backend, ConnectionType ct) { - if constexpr (requires { backend->set_connection_type(ct); }) { - backend->set_connection_type(ct); - } - } template void stream_pending_(Backend *backend) { if constexpr (requires { backend->stream_service_batch(*this); }) { backend->stream_service_batch(*this); diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h index d5bf76e6ee..f3d9ecc11e 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.h @@ -94,6 +94,11 @@ class RP2GattClient final : public Component, public Parented cv.All: return cv.All(schema, populate_connections) -async def _rp2_connections_to_code(var: cg.MockObj, config: ConfigType) -> None: - from esphome.components import rp2040_ble - - # One wrapper + backend pair per slot (the esp32 arm's pattern). - for connection_conf in config[CONF_CONNECTIONS]: +async def _connections_to_code( + var: cg.MockObj, config: ConfigType, register_backend +) -> None: + """One wrapper + backend pair per slot; register_backend supplies the + platform's backend registration (tracker client on esp32, controller + parent on rp2).""" + for connection_conf in config.get(CONF_CONNECTIONS, []): ble_device_base.request_gatt_client() backend = cg.new_Pvariable(connection_conf[CONF_BACKEND_ID]) await cg.register_component(backend, connection_conf) - await cg.register_parented(backend, config[rp2040_ble.CONF_RP2040_BLE_ID]) + await register_backend(backend, connection_conf, config) connection = cg.new_Pvariable(connection_conf[CONF_ID]) cg.add(connection.set_backend(backend)) cg.add(var.register_connection(connection)) +async def _rp2_connections_to_code(var: cg.MockObj, config: ConfigType) -> None: + from esphome.components import rp2040_ble + + async def register_backend(backend, connection_conf, config): + await cg.register_parented(backend, config[rp2040_ble.CONF_RP2040_BLE_ID]) + + await _connections_to_code(var, config, register_backend) + + # Per-platform schema builders and connection codegen; every key of # bluetooth_connection.HUB_MAX_CONNECTIONS needs an entry in both (pinned by # tests/component_tests/bluetooth_proxy/). @@ -386,17 +397,13 @@ async def _to_code_esp32(config: ConfigType) -> None: connection_count = len(config.get(CONF_CONNECTIONS, [])) cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count) - for connection_conf in config.get(CONF_CONNECTIONS, []): - ble_device_base.request_gatt_client() - backend = cg.new_Pvariable(connection_conf[CONF_BACKEND_ID]) - await cg.register_component(backend, connection_conf) + async def register_backend(backend, connection_conf, _config): # The tracker promote loop drives connect timing through the shim. await esp32_ble_tracker.register_raw_client( backend.tracker_client(), connection_conf ) - connection = cg.new_Pvariable(connection_conf[CONF_ID]) - cg.add(connection.set_backend(backend)) - cg.add(var.register_connection(connection)) + + await _connections_to_code(var, config, register_backend) if config.get(CONF_CACHE_SERVICES): add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 23e60c91ba..3c4e11ff26 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -477,9 +477,8 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { void BluetoothProxy::loop() { #ifdef BLUETOOTH_CONNECTION_HAS_GATT - // Stream pending service-discovery batches every iteration (esp32 parity: - // its connections stream from their own per-iteration Component loop). - // send_service_for_discovery_() handles a vanished API connection itself. + // Stream pending service-discovery batches every iteration; the streamer + // handles a vanished API connection itself. for (uint8_t i = 0; i < this->connection_count_; i++) { this->connections_[i]->process_pending_services(); } @@ -539,12 +538,18 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: this->send_device_pairing(msg.address, false, GATT_NOT_CONNECTED); break; - case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: - this->send_device_unpairing(msg.address, false, GATT_NOT_CONNECTED); + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: { + // Address-scoped maintenance needs no connection slot: real on esp32 + // (Bluedroid bond table), the stub elsewhere keeps the old error reply. + conn_err_t ret = bluetooth_connection::unpair_device(msg.address); + this->send_device_unpairing(msg.address, ret == CONN_OK, ret); break; - case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: - this->send_device_clear_cache(msg.address, false, GATT_NOT_CONNECTED); + } + case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: { + conn_err_t ret = bluetooth_connection::clear_gatt_cache(msg.address); + this->send_device_clear_cache(msg.address, ret == CONN_OK, ret); break; + } } } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 7a2b9ca695..267e29d12e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -5,8 +5,6 @@ #ifdef USE_BLUETOOTH_PROXY #include -#include -#include #include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" @@ -27,7 +25,6 @@ namespace esphome::bluetooth_proxy { // re-exported here so the proxy code reads unqualified. using bluetooth_connection::CONN_OK; using bluetooth_connection::conn_err_t; -using bluetooth_connection::DONE_SENDING_SERVICES; using bluetooth_connection::GATT_NOT_CONNECTED; using bluetooth_connection::INIT_SENDING_SERVICES; diff --git a/tests/components/bluetooth_proxy/test-passive.esp32-c6-idf.yaml b/tests/components/bluetooth_proxy/test-passive.esp32-c6-idf.yaml new file mode 100644 index 0000000000..06ce74dbad --- /dev/null +++ b/tests/components/bluetooth_proxy/test-passive.esp32-c6-idf.yaml @@ -0,0 +1,10 @@ +# Advertisement-only proxy on esp32 by explicit choice: no GATT backend is +# compiled (USE_BLE_GATT_CLIENT unset), which pins the HAS_GATT gating and the +# address-scoped maintenance path that a connections build never exercises. +packages: + common: !include common.yaml + +esp32_ble_tracker: + +bluetooth_proxy: + active: false