diff --git a/esphome/components/bluetooth_connection/__init__.py b/esphome/components/bluetooth_connection/__init__.py index b4d1a7a762..a9800ab81c 100644 --- a/esphome/components/bluetooth_connection/__init__.py +++ b/esphome/components/bluetooth_connection/__init__.py @@ -5,19 +5,12 @@ user-facing configuration; the proxy's codegen declares and registers the connection instances. """ -import functools - import esphome.codegen as cg from esphome.config_helpers import filter_source_files_from_platform from esphome.const import PLATFORM_RP2, PlatformFramework -from esphome.core import CORE def AUTO_LOAD() -> list[str]: - """The esp32 connection header includes esp32_ble_client, so the closure - must be self-satisfying; no target platform (tooling) gets the union.""" - if CORE.is_esp32 or CORE.target_platform is None: - return ["ble_device_base", "esp32_ble_client"] return ["ble_device_base"] @@ -41,17 +34,6 @@ BluedroidGattClient = bluetooth_connection_ns.class_( ) -@functools.cache -def esp32_connection_class() -> cg.MockObjClass: - """Lazy: importing esp32_ble_client registers esp32-only automations as - an import side effect, which must not leak into other platforms.""" - from esphome.components import esp32_ble_client - - return bluetooth_connection_ns.class_( - "BluetoothConnection", esp32_ble_client.BLEClientBase - ) - - FILTER_SOURCE_FILES = filter_source_files_from_platform( { "bluetooth_connection_bluedroid.cpp": { @@ -68,6 +50,8 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( "bluetooth_connection_hub.cpp": { PlatformFramework.RP2_ARDUINO, PlatformFramework.LN882X_ARDUINO, + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, }, "bluetooth_connection_rp2.cpp": {PlatformFramework.RP2_ARDUINO}, } diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index e5bdb7af56..8c10eb3f25 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -12,7 +12,9 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #include +#endif #include #include @@ -342,7 +344,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) { resp.services.emplace_back(); auto &service_resp = resp.services.back(); fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, - esp32_ble_tracker::ESPBTUUID::from_uuid(service_result.uuid), use_efficient_uuids); + ble_device_base::ESPBTUUID::from_uuid(service_result.uuid), use_efficient_uuids); service_resp.handle = service_result.start_handle; if (total_char_count > 0) { @@ -368,7 +370,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) { service_resp.characteristics.emplace_back(); auto &characteristic_resp = service_resp.characteristics.back(); fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, - esp32_ble_tracker::ESPBTUUID::from_uuid(char_result.uuid), use_efficient_uuids); + ble_device_base::ESPBTUUID::from_uuid(char_result.uuid), use_efficient_uuids); characteristic_resp.handle = char_result.char_handle; characteristic_resp.properties = char_result.properties; @@ -396,7 +398,7 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) { characteristic_resp.descriptors.emplace_back(); auto &descriptor_resp = characteristic_resp.descriptors.back(); fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, - esp32_ble_tracker::ESPBTUUID::from_uuid(desc_result.uuid), use_efficient_uuids); + ble_device_base::ESPBTUUID::from_uuid(desc_result.uuid), use_efficient_uuids); descriptor_resp.handle = desc_result.handle; desc_offset++; } @@ -439,7 +441,7 @@ void BluedroidGattClient::handle_open_evt_(esp_ble_gattc_cb_param_t *param) { this->report_connection_state_(false, param->open.status); return; } - if (this->shim_.disconnect_scheduled()) { + if (this->shim_.disconnect_pending()) { // Earliest point conn_id_ exists; keep it set so CLOSE_EVT still matches. this->unconditional_disconnect_(); return; diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp index f5c59ca43a..ca5ad42982 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.cpp @@ -1,482 +1,36 @@ -#include "bluetooth_connection_esp32.h" +// Address-scoped Bluedroid maintenance shared by every esp32 proxy build, +// including advertisement-only ones where no GATT backend is compiled. -#include "esphome/components/api/api_pb2.h" -#include "esphome/core/helpers.h" -#include "esphome/core/log.h" +#include "esphome/core/defines.h" #ifdef USE_ESP32 -#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" +#include "bluetooth_connection.h" + +#include +#include namespace esphome::bluetooth_connection { -namespace espbt = esphome::esp32_ble_tracker; - -using ble_device_base::ESPBTUUID; - -static const char *const TAG = "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 bd_addr; - ble_device_base::uint64_to_mac_msb_first(address, bd_addr); - return esp_ble_remove_bond_device(bd_addr); + esp_bd_addr_t bda; + address_to_bda(address, bda); + return esp_ble_remove_bond_device(bda); } conn_err_t clear_gatt_cache(uint64_t address) { - esp_bd_addr_t bd_addr; - ble_device_base::uint64_to_mac_msb_first(address, bd_addr); - return esp_ble_gattc_cache_clean(bd_addr); -} - -void BluetoothConnection::dump_config() { - ESP_LOGCONFIG(TAG, "BLE Connection:"); - BLEClientBase::dump_config(); -} - -void BluetoothConnection::set_address(uint64_t address) { - // Keep the proxy's pre-allocated connections-free message in step - this->proxy_->update_address_slot_(this->address_, address); - // Call parent implementation to actually set the address - BLEClientBase::set_address(address); -} - -void BluetoothConnection::loop() { - BLEClientBase::loop(); - - // Early return if no active connection - if (this->address_ == 0) { - return; - } - - // Handle service discovery if in valid range - if (this->send_service_ >= 0 && this->send_service_ <= this->service_count_) { - this->send_service_for_discovery_(); - } - - // Check if we should disable the loop - // - For V3_WITH_CACHE: Services are never sent, disable after INIT state - // - For V3_WITHOUT_CACHE: Disable only after service discovery is complete - // (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent) - // Never disable while DISCONNECTING — BLEClientBase::loop() needs to keep running so the - // 10s safety timeout can force IDLE if CLOSE_EVT is never delivered. - if (this->state() != espbt::ClientState::INIT && this->state() != espbt::ClientState::DISCONNECTING && - (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || - this->send_service_ == DONE_SENDING_SERVICES)) { - this->disable_loop(); - } -} - -void BluetoothConnection::on_disconnect_complete(esp_err_t reason) { - // Called from both the CLOSE_EVT handler and the DISCONNECTING safety timeout in the - // base class. Free the proxy slot, notify the API client, and reset send_service_. - // address_ may already be 0 if reset_connection_ ran earlier on this teardown. - if (this->address_ == 0) { - return; - } - ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_, reason); - this->reset_connection_(reason); -} - -void BluetoothConnection::reset_connection_(esp_err_t reason) { this->proxy_->reset_connection_slot_(this, reason); } - -void BluetoothConnection::send_service_for_discovery_() { - if (this->send_service_ >= this->service_count_) { - this->send_service_ = DONE_SENDING_SERVICES; - this->proxy_->send_gatt_services_done(this->address_); - this->release_services(); - return; - } - - // Early return if no API connection - auto *api_conn = this->proxy_->get_api_connection(); - if (api_conn == nullptr) { - this->send_service_ = DONE_SENDING_SERVICES; - return; - } - - // Check if client supports efficient UUIDs - bool use_efficient_uuids = this->proxy_->client_supports_efficient_uuids(); - - // Prepare response - api::BluetoothGATTGetServicesResponse resp; - resp.address = this->address_; - - // Dynamic batching based on actual size - // Keep running total of actual message size - size_t current_size = resp.calculate_size(); - int16_t batch_start = this->send_service_; - - while (this->send_service_ < this->service_count_) { - esp_gattc_service_elem_t service_result; - uint16_t service_count = 1; - esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, - &service_result, &service_count, this->send_service_); - - if (service_status != ESP_GATT_OK || service_count == 0) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d", - this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing", - service_status, service_count, this->send_service_); - this->send_service_ = DONE_SENDING_SERVICES; - return; - } - - // Get the number of characteristics BEFORE adding to response - uint16_t total_char_count = 0; - esp_gatt_status_t char_count_status = - esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC, - service_result.start_handle, service_result.end_handle, 0, &total_char_count); - - if (char_count_status != ESP_GATT_OK) { - this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status); - this->send_service_ = DONE_SENDING_SERVICES; - return; - } - - // If this service likely won't fit, send current batch (unless it's the first) - size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids); - if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) { - // This service likely won't fit, send current batch - break; - } - - // Now add the service since we know it will likely fit - resp.services.emplace_back(); - auto &service_resp = resp.services.back(); - - fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, ESPBTUUID::from_uuid(service_result.uuid), - use_efficient_uuids); - - service_resp.handle = service_result.start_handle; - - if (total_char_count > 0) { - // Initialize FixedVector with exact count and process characteristics - service_resp.characteristics.init(total_char_count); - uint16_t char_offset = 0; - esp_gattc_char_elem_t char_result; - // Bound by total_char_count: the vector is sized for it, and a malicious peripheral - // can make enumeration return more entries than the count query reported - while (char_offset < total_char_count) { // characteristics - uint16_t char_count = 1; - esp_gatt_status_t char_status = - esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle, - service_result.end_handle, &char_result, &char_count, char_offset); - if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) { - break; - } - if (char_status != ESP_GATT_OK) { - this->log_connection_error_("esp_ble_gattc_get_all_char", char_status); - this->send_service_ = DONE_SENDING_SERVICES; - return; - } - if (char_count == 0) { - break; - } - - service_resp.characteristics.emplace_back(); - auto &characteristic_resp = service_resp.characteristics.back(); - fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, ESPBTUUID::from_uuid(char_result.uuid), - use_efficient_uuids); - characteristic_resp.handle = char_result.char_handle; - characteristic_resp.properties = char_result.properties; - char_offset++; - - // Get the number of descriptors directly with one call - uint16_t total_desc_count = 0; - esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count( - this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count); - - if (desc_count_status != ESP_GATT_OK) { - this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status); - this->send_service_ = DONE_SENDING_SERVICES; - return; - } - if (total_desc_count == 0) { - continue; - } - - // Initialize FixedVector with exact count and process descriptors - characteristic_resp.descriptors.init(total_desc_count); - uint16_t desc_offset = 0; - esp_gattc_descr_elem_t desc_result; - while (desc_offset < total_desc_count) { // descriptors - uint16_t desc_count = 1; - esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr( - this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset); - if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) { - break; - } - if (desc_status != ESP_GATT_OK) { - this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status); - this->send_service_ = DONE_SENDING_SERVICES; - return; - } - if (desc_count == 0) { - break; // No more descriptors - } - - characteristic_resp.descriptors.emplace_back(); - auto &descriptor_resp = characteristic_resp.descriptors.back(); - fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, ESPBTUUID::from_uuid(desc_result.uuid), - use_efficient_uuids); - descriptor_resp.handle = desc_result.handle; - desc_offset++; - } - } - } // end if (total_char_count > 0) - - if (close_service_batch(resp, current_size, this->send_service_, this->connection_index_, this->address_str()) != - BatchClose::CONTINUE) { - break; - } - } - - // Send the message with dynamically batched services; on a failed send, - // rewind the cursor so the batch is retried instead of silently skipped. - if (!api_conn->send_message(resp)) { - ESP_LOGW(TAG, "[%d] [%s] Failed to send service batch, retrying", this->connection_index_, this->address_str_); - this->send_service_ = batch_start; - } -} - -void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { - ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status); -} - -void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) { - ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err); -} - -void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) { - ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action, - type); -} - -void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) { - ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(), - operation, handle, status); -} - -esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) { - if (err != ESP_OK) { - this->log_connection_warning_(operation, err); - return err; - } - return ESP_OK; -} - -bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) { - if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) - return false; - - switch (event) { - case ESP_GATTC_DISCONNECT_EVT: { - // Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources - // This prevents race condition where we mark slot as free before controller cleanup is complete - ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_, - param->disconnect.reason); - // Send disconnection notification but don't free the slot yet - this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); - break; - } - case ESP_GATTC_OPEN_EVT: { - if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { - this->reset_connection_(param->open.status); - } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - this->proxy_->send_device_connection(this->address_, true, this->mtu_); - this->proxy_->send_connections_free(); - } - this->seen_mtu_or_services_ = false; - break; - } - case ESP_GATTC_CFG_MTU_EVT: - case ESP_GATTC_SEARCH_CMPL_EVT: { - if (!this->seen_mtu_or_services_) { - // We don't know if we will get the MTU or the services first, so - // only send the device connection true if we have already received - // the services. - this->seen_mtu_or_services_ = true; - break; - } - this->proxy_->send_device_connection(this->address_, true, this->mtu_); - this->proxy_->send_connections_free(); - break; - } - case ESP_GATTC_READ_DESCR_EVT: - case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.status != ESP_GATT_OK) { - this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status); - this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); - break; - } - auto *api_connection = this->proxy_->get_api_connection(); - if (api_connection == nullptr) - break; - api::BluetoothGATTReadResponse resp; - resp.address = this->address_; - resp.handle = param->read.handle; - resp.set_data(param->read.value, param->read.value_len); - api_connection->send_message(resp); - break; - } - case ESP_GATTC_WRITE_CHAR_EVT: - case ESP_GATTC_WRITE_DESCR_EVT: { - if (param->write.status != ESP_GATT_OK) { - this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status); - this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); - break; - } - auto *api_connection = this->proxy_->get_api_connection(); - if (api_connection == nullptr) - break; - api::BluetoothGATTWriteResponse resp; - resp.address = this->address_; - resp.handle = param->write.handle; - api_connection->send_message(resp); - break; - } - case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { - if (param->unreg_for_notify.status != ESP_GATT_OK) { - this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle, - param->unreg_for_notify.status); - this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); - break; - } - auto *api_connection = this->proxy_->get_api_connection(); - if (api_connection == nullptr) - break; - api::BluetoothGATTNotifyResponse resp; - resp.address = this->address_; - resp.handle = param->unreg_for_notify.handle; - api_connection->send_message(resp); - break; - } - case ESP_GATTC_REG_FOR_NOTIFY_EVT: { - if (param->reg_for_notify.status != ESP_GATT_OK) { - this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle, - param->reg_for_notify.status); - this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); - break; - } - auto *api_connection = this->proxy_->get_api_connection(); - if (api_connection == nullptr) - break; - api::BluetoothGATTNotifyResponse resp; - resp.address = this->address_; - resp.handle = param->reg_for_notify.handle; - api_connection->send_message(resp); - break; - } - case ESP_GATTC_NOTIFY_EVT: { - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, - param->notify.handle); - auto *api_connection = this->proxy_->get_api_connection(); - if (api_connection == nullptr) - break; - api::BluetoothGATTNotifyDataResponse resp; - resp.address = this->address_; - resp.handle = param->notify.handle; - resp.set_data(param->notify.value, param->notify.value_len); - api_connection->send_message(resp); - break; - } - default: - break; - } - return true; -} - -void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - BLEClientBase::gap_event_handler(event, param); - - switch (event) { - case ESP_GAP_BLE_AUTH_CMPL_EVT: - if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) - break; - if (param->ble_security.auth_cmpl.success) { - this->proxy_->send_device_pairing(this->address_, true); - } else { - this->proxy_->send_device_pairing(this->address_, false, param->ble_security.auth_cmpl.fail_reason); - } - break; - default: - break; - } -} - -esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { - if (!this->connected()) { - this->log_gatt_not_connected_("read", "characteristic"); - return GATT_NOT_CONNECTED; - } - - ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); - - esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); - return this->check_and_log_error_("esp_ble_gattc_read_char", err); -} - -esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8_t *data, size_t length, - bool response) { - if (!this->connected()) { - this->log_gatt_not_connected_("write", "characteristic"); - return GATT_NOT_CONNECTED; - } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); - - // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data - // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) - // const_cast is safe here and was previously hidden by a C-style cast - esp_err_t err = - esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, length, const_cast(data), - response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); - return this->check_and_log_error_("esp_ble_gattc_write_char", err); -} - -esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) { - if (!this->connected()) { - this->log_gatt_not_connected_("read", "descriptor"); - return GATT_NOT_CONNECTED; - } - ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); - - esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); - return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err); -} - -esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response) { - if (!this->connected()) { - this->log_gatt_not_connected_("write", "descriptor"); - return GATT_NOT_CONNECTED; - } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); - - // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data - // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) - // const_cast is safe here and was previously hidden by a C-style cast - esp_err_t err = esp_ble_gattc_write_char_descr( - this->gattc_if_, this->conn_id_, handle, length, const_cast(data), - response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); - return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err); -} - -esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) { - if (!this->connected()) { - this->log_gatt_not_connected_("notify", "characteristic"); - return GATT_NOT_CONNECTED; - } - - if (enable) { - ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_, handle); - esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle); - return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err); - } - - ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_, handle); - esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle); - return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err); + esp_bd_addr_t bda; + address_to_bda(address, bda); + esp_ble_gattc_cache_clean(bda); + return CONN_OK; } } // namespace esphome::bluetooth_connection diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_esp32.h b/esphome/components/bluetooth_connection/bluetooth_connection_esp32.h deleted file mode 100644 index fb60d93e9c..0000000000 --- a/esphome/components/bluetooth_connection/bluetooth_connection_esp32.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include "esphome/core/defines.h" - -#ifdef USE_ESP32 - -#include "esphome/components/esp32_ble_client/ble_client_base.h" - -#include "bluetooth_connection.h" - -namespace esphome::bluetooth_proxy { -class BluetoothProxy; -} // namespace esphome::bluetooth_proxy - -namespace esphome::bluetooth_connection { - -class BluetoothConnection final : public esp32_ble_client::BLEClientBase { - public: - void dump_config() override; - void loop() override; - bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override; - void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; - // The proxy's connections never consume parsed ESPBTDevice objects. - bool wants_parsed_advertisements() override { return false; } - - esp_err_t read_characteristic(uint16_t handle); - esp_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response); - esp_err_t read_descriptor(uint16_t handle); - esp_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response); - - esp_err_t notify_characteristic(uint16_t handle, bool enable); - - esp_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) { - return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom"); - } - - bool has_gatt_services() const { return this->service_count_ != 0; } - - /// Start connecting: record the API address type and hand the client to the - /// tracker's promote loop (it pauses the scan and opens the connection). - void initiate_connection(uint8_t address_type) { - this->set_remote_addr_type(static_cast(address_type)); - this->set_state(esp32_ble_tracker::ClientState::DISCOVERED); - } - - void set_address(uint64_t address) override; - - protected: - friend class bluetooth_proxy::BluetoothProxy; - - void on_disconnect_complete(esp_err_t reason) override; - - void send_service_for_discovery_(); - void reset_connection_(esp_err_t reason); - void log_connection_error_(const char *operation, esp_gatt_status_t status); - void log_connection_warning_(const char *operation, esp_err_t err); - void log_gatt_not_connected_(const char *action, const char *type); - void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status); - esp_err_t check_and_log_error_(const char *operation, esp_err_t err); - - // Memory optimized layout for 32-bit systems - // Group 1: Pointers (4 bytes each, naturally aligned) - bluetooth_proxy::BluetoothProxy *proxy_; - - // Group 2: 2-byte types - int16_t send_service_{INIT_SENDING_SERVICES}; // see bluetooth_connection.h cursor states - - // Group 3: 1-byte types - bool seen_mtu_or_services_{false}; - // 1 byte used, 1 byte padding -}; - -} // namespace esphome::bluetooth_connection - -#endif // USE_ESP32 diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp index ec03f18e1d..db110706fa 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.cpp @@ -1,7 +1,7 @@ // Hub-platform connection wrapper (USE_RP2 hub builds today). #include "bluetooth_connection_hub.h" -#if !defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT) +#ifdef USE_BLE_GATT_CLIENT #include "esphome/components/api/api_pb2.h" #include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" @@ -433,4 +433,4 @@ void BluetoothConnection::send_service_for_discovery_() { } // namespace esphome::bluetooth_connection -#endif // !USE_ESP32 && USE_BLE_GATT_CLIENT +#endif // USE_BLE_GATT_CLIENT diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h index 95aac31931..985326f13f 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h @@ -8,7 +8,7 @@ #include "esphome/core/defines.h" -#if !defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT) +#ifdef USE_BLE_GATT_CLIENT #include "bluetooth_connection.h" @@ -52,10 +52,11 @@ class BluetoothConnection final { bool is_paired() const { return this->paired_; } void set_unpaired() { this->paired_ = false; } conn_err_t pair() { return this->backend_->pair(); } - // A backend disconnect() is a single call that also cancels an in-progress - // connect; there is no deferred-disconnect state to track. - bool disconnect_pending() const { return false; } - void cancel_pending_disconnect() {} + // 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_); } void set_address(uint64_t address); uint64_t get_address() const { return this->address_; } @@ -65,7 +66,12 @@ class BluetoothConnection final { ClientState state() const { return this->state_; } void set_state(ClientState st) { this->state_ = st; } bool connected() const { return this->state_ == ClientState::ESTABLISHED; } - void set_connection_type(ConnectionType ct) { this->connection_type_ = ct; } + 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); + } // Latched at discovery completion rather than read from the backend table: // streaming frees the table, and this must stay true for the connection's // lifetime (esp32 parity — a repeat GetServices is silently ignored there, @@ -102,6 +108,23 @@ 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); @@ -146,4 +169,4 @@ static_assert(ble_device_base::GattClientEventSinkContract, } // namespace esphome::bluetooth_connection -#endif // !USE_ESP32 && USE_BLE_GATT_CLIENT +#endif // USE_BLE_GATT_CLIENT diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index a28c8abc71..b844d66469 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -27,7 +27,7 @@ def AUTO_LOAD(config: ConfigType | None = None) -> list[str]: target platform set, so it takes one of the concrete branches. """ if CORE.is_esp32: - return ["bluetooth_connection", "esp32_ble_client", "esp32_ble_tracker"] + return ["bluetooth_connection", "esp32_ble_tracker"] if CORE.target_platform in _HUB_PLATFORMS: return ["ble_device_base", "bluetooth_connection"] # No target platform, or one this component does not support: tooling @@ -36,7 +36,6 @@ def AUTO_LOAD(config: ConfigType | None = None) -> list[str]: return [ "ble_device_base", "bluetooth_connection", - "esp32_ble_client", "esp32_ble_tracker", ] @@ -86,10 +85,12 @@ def _esp32_config_schema() -> cv.All: f"update _IDF_MAX_CONNECTIONS in bluetooth_proxy/__init__.py" ) - BluetoothConnection = bluetooth_connection.esp32_connection_class() CONNECTION_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(BluetoothConnection), + cv.GenerateID(): cv.declare_id(bluetooth_connection.HubBluetoothConnection), + cv.GenerateID(CONF_BACKEND_ID): cv.declare_id( + bluetooth_connection.BluedroidGattClient + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -386,10 +387,16 @@ async def _to_code_esp32(config: ConfigType) -> None: cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count) for connection_conf in config.get(CONF_CONNECTIONS, []): - connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) - await cg.register_component(connection_var, connection_conf) - cg.add(var.register_connection(connection_var)) - await esp32_ble_tracker.register_raw_client(connection_var, connection_conf) + ble_device_base.request_gatt_client() + backend = cg.new_Pvariable(connection_conf[CONF_BACKEND_ID]) + await cg.register_component(backend, connection_conf) + # 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)) 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 88d8cc1885..23e60c91ba 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -132,13 +132,6 @@ void BluetoothProxy::log_advertisement_flush_() { } void BluetoothProxy::dump_config() { -#ifdef USE_ESP32 - ESP_LOGCONFIG(TAG, - "Bluetooth Proxy:\n" - " Active: %s\n" - " Connections: %d", - YESNO(this->active_), this->connection_count_); -#else // Print configured facts. dump_config runs right after setup, before the // radio is up, so live scan state would always read "stopped" here — the // loop's BluetoothScannerStateResponse carries the changing value instead. @@ -162,32 +155,8 @@ void BluetoothProxy::dump_config() { " Adapter MAC: %s", scan_mode, mac_out); #endif -#endif } -#ifdef USE_ESP32 - -void BluetoothProxy::loop() { - // Run advertisement flush / connection cleanup every 100ms - uint32_t now = App.get_loop_component_start_time(); - if (now - this->last_advertisement_flush_time_ < 100) - return; - this->last_advertisement_flush_time_ = now; - - if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) { - this->flush_pending_advertisements_(); - return; - } - for (uint8_t i = 0; i < this->connection_count_; i++) { - auto *connection = this->connections_[i]; - if (connection->get_address() != 0 && !connection->disconnect_pending()) { - connection->disconnect(); - } - } -} - -#endif // USE_ESP32 - #ifdef BLUETOOTH_CONNECTION_HAS_GATT // maybe_unused: in a passive proxy (active: false) MAX is 0, the body is removed, and connection is unused. @@ -200,11 +169,8 @@ void BluetoothProxy::register_connection([[maybe_unused]] BluetoothConnection *c ESP_LOGE(TAG, "Connection registry full, dropping registration"); return; } -#ifndef USE_ESP32 - // esp32 assigns connection_index_ in BLEClientBase::setup(); the hub - // class has no Component lifecycle, so the index is assigned here. + // The hub wrapper has no Component lifecycle, so the index is assigned here. connection->connection_index_ = this->connection_count_; -#endif this->connections_[this->connection_count_++] = connection; connection->proxy_ = this; #endif @@ -486,6 +452,29 @@ void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { #else // !USE_ESP32 +void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { + if (this->hub_->scan_active() != active) { + ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive"); + if (!this->hub_->request_scan_mode(active)) { + // Passive-only controller asked for active scanning; the state report + // below carries the real, unchanged mode so the subscriber does not + // assume the change happened. + ESP_LOGW(TAG, "Scanner mode %s not supported by this tracker", active ? "active" : "passive"); + } + } +#ifndef USE_BLE_SCANNER_STATE_CALLBACK + if (this->api_connection_ != nullptr) { + // Reports the mode change; the sender also refreshes last_scan_running_, so + // a failed restart (scan_running_ dropped by the tracker) is not reported + // again by loop() on the next tick. A push hub reports the restart's + // transitions (mode rides along) instead. + this->send_polled_scanner_state_(); + } +#endif +} + +#endif // USE_ESP32 + void BluetoothProxy::loop() { #ifdef BLUETOOTH_CONNECTION_HAS_GATT // Stream pending service-discovery batches every iteration (esp32 parity: @@ -508,7 +497,7 @@ void BluetoothProxy::loop() { // (disconnect() on an already-disconnecting backend is a no-op). for (uint8_t i = 0; i < this->connection_count_; i++) { auto *connection = this->connections_[i]; - if (connection->get_address() != 0) { + if (connection->get_address() != 0 && !connection->disconnect_pending()) { connection->disconnect(); } } @@ -595,29 +584,6 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn #endif // !BLUETOOTH_CONNECTION_HAS_GATT -void BluetoothProxy::bluetooth_scanner_set_mode(bool active) { - if (this->hub_->scan_active() != active) { - ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive"); - if (!this->hub_->request_scan_mode(active)) { - // Passive-only controller asked for active scanning; the state report - // below carries the real, unchanged mode so the subscriber does not - // assume the change happened. - ESP_LOGW(TAG, "Scanner mode %s not supported by this tracker", active ? "active" : "passive"); - } - } -#ifndef USE_BLE_SCANNER_STATE_CALLBACK - if (this->api_connection_ != nullptr) { - // Reports the mode change; the sender also refreshes last_scan_running_, so - // a failed restart (scan_running_ dropped by the tracker) is not reported - // again by loop() on the next tick. A push hub reports the restart's - // transitions (mode rides along) instead. - this->send_polled_scanner_state_(); - } -#endif -} - -#endif // USE_ESP32 - void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { if (this->api_connection_ != nullptr && this->api_connection_ != api_connection) { // A previous subscriber still holds the slot. This is almost always a stale diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index d7150617d3..7a2b9ca695 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -17,9 +17,7 @@ #include "esphome/components/ble_device_base/ble_hub_impl.h" -#ifdef USE_ESP32 -#include "esphome/components/bluetooth_connection/bluetooth_connection_esp32.h" -#elif defined(USE_BLE_GATT_CLIENT) +#ifdef USE_BLE_GATT_CLIENT #include "esphome/components/bluetooth_connection/bluetooth_connection_hub.h" #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ad24d27369..e3c4dd87ce 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -306,6 +306,8 @@ #define USE_ESP32_BLE_SERVER_ON_CONNECT #define USE_ESP32_BLE_SERVER_ON_DISCONNECT #define USE_ESP32_BLE_TRACKER +#define USE_BLE_GATT_CLIENT +#define ESPHOME_BLE_GATT_CLIENT_COUNT 1 #define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1 #define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1 #define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 diff --git a/tests/component_tests/ble_device_base/test_slot_counter.py b/tests/component_tests/ble_device_base/test_slot_counter.py index e784c9871e..86ee53fe8a 100644 --- a/tests/component_tests/ble_device_base/test_slot_counter.py +++ b/tests/component_tests/ble_device_base/test_slot_counter.py @@ -109,6 +109,8 @@ def test_esp32_bluetooth_proxy_requests_client_slots_only( generate_main(component_config_path("esp32_bluetooth_proxy.yaml")) assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT") is None assert get_define_value("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT") == "3" + # One neutral GATT backend slot per connection (the hub-model flip). + assert get_define_value("ESPHOME_BLE_GATT_CLIENT_COUNT") == "3" def test_counts_reset_between_compiles( diff --git a/tests/component_tests/bluetooth_proxy/test_platform_gates.py b/tests/component_tests/bluetooth_proxy/test_platform_gates.py index 55e6fe2ca7..ec3cf8bd4c 100644 --- a/tests/component_tests/bluetooth_proxy/test_platform_gates.py +++ b/tests/component_tests/bluetooth_proxy/test_platform_gates.py @@ -178,17 +178,11 @@ def test_rp2_rejects_esp32_only_keys_by_name( def test_bluetooth_connection_auto_load_covers_its_includes() -> None: - # The esp32 connection header includes esp32_ble_client; the auto load - # must satisfy that closure itself (regression: it once relied on the - # consumer's auto loads). - _set_platform("esp32") - assert "esp32_ble_client" in bluetooth_connection.AUTO_LOAD() - _set_platform("rp2") - assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base"] - # No target platform (tooling resolving the manifest): the union, so - # dependency closures stay complete for build_codeowners and friends. - _set_platform(None) - assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base", "esp32_ble_client"] + # Every backend builds on ble_device_base alone; the Bluedroid backend + # talks to IDF directly, so esp32_ble_client is no longer in the closure. + for platform in ("esp32", "rp2", None): + _set_platform(platform) + assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base"] def test_every_registered_hub_platform_has_a_schema_arm() -> None: