[bluetooth_proxy] Implement dynamic service batching based on MTU constraints (#10001)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
J. Nick Koston 2025-07-31 14:15:12 -10:00 committed by GitHub
parent 161f51e1f4
commit d4ff1bcf5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 152 additions and 83 deletions

View File

@ -37,6 +37,37 @@ static void fill_gatt_uuid(std::array<uint64_t, 2> &uuid_128, uint32_t &short_uu
} }
} }
// Constants for size estimation
static constexpr uint8_t SERVICE_OVERHEAD_LEGACY = 25; // UUID(20) + handle(4) + overhead(1)
static constexpr uint8_t SERVICE_OVERHEAD_EFFICIENT = 10; // UUID(6) + handle(4)
static constexpr uint8_t CHAR_SIZE_128BIT = 35; // UUID(20) + handle(4) + props(4) + overhead(7)
static constexpr uint8_t DESC_SIZE_128BIT = 25; // UUID(20) + handle(4) + overhead(1)
static constexpr uint8_t DESC_SIZE_16BIT = 10; // UUID(6) + handle(4)
static constexpr uint8_t DESC_PER_CHAR = 1; // Assume 1 descriptor per characteristic
// Helper to estimate service size before fetching all data
/**
* Estimate the size of a Bluetooth service based on the number of characteristics and UUID format.
*
* @param char_count The number of characteristics in the service.
* @param use_efficient_uuids Whether to use efficient UUIDs (16-bit or 32-bit) for newer APIVersions.
* @return The estimated size of the service in bytes.
*
* This function calculates the size of a Bluetooth service by considering:
* - A service overhead, which depends on whether efficient UUIDs are used.
* - The size of each characteristic, assuming 128-bit UUIDs for safety.
* - The size of descriptors, assuming one 128-bit descriptor per characteristic.
*/
static size_t estimate_service_size(uint16_t char_count, bool use_efficient_uuids) {
size_t service_overhead = use_efficient_uuids ? SERVICE_OVERHEAD_EFFICIENT : SERVICE_OVERHEAD_LEGACY;
// Always assume 128-bit UUIDs for characteristics to be safe
size_t char_size = CHAR_SIZE_128BIT;
// Assume one 128-bit descriptor per characteristic
size_t desc_size = DESC_SIZE_128BIT * DESC_PER_CHAR;
return service_overhead + (char_size + desc_size) * char_count;
}
bool BluetoothConnection::supports_efficient_uuids_() const { bool BluetoothConnection::supports_efficient_uuids_() const {
auto *api_conn = this->proxy_->get_api_connection(); auto *api_conn = this->proxy_->get_api_connection();
return api_conn && api_conn->client_supports_api_version(1, 12); return api_conn && api_conn->client_supports_api_version(1, 12);
@ -95,16 +126,21 @@ void BluetoothConnection::send_service_for_discovery_() {
// Check if client supports efficient UUIDs // Check if client supports efficient UUIDs
bool use_efficient_uuids = this->supports_efficient_uuids_(); bool use_efficient_uuids = this->supports_efficient_uuids_();
// Prepare response for up to 3 services // Prepare response
api::BluetoothGATTGetServicesResponse resp; api::BluetoothGATTGetServicesResponse resp;
resp.address = this->address_; resp.address = this->address_;
// Process up to 3 services in this iteration // Dynamic batching based on actual size
uint8_t services_to_process = // Conservative MTU limit for API messages (accounts for WPA3 overhead)
std::min(MAX_SERVICES_PER_BATCH, static_cast<uint8_t>(this->service_count_ - this->send_service_)); static constexpr size_t MAX_PACKET_SIZE = 1360;
resp.services.reserve(services_to_process);
for (int service_idx = 0; service_idx < services_to_process; service_idx++) { // Keep running total of actual message size
size_t current_size = 0;
api::ProtoSize size;
resp.calculate_size(size);
current_size = size.get_size();
while (this->send_service_ < this->service_count_) {
esp_gattc_service_elem_t service_result; esp_gattc_service_elem_t service_result;
uint16_t service_count = 1; uint16_t service_count = 1;
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
@ -118,15 +154,7 @@ void BluetoothConnection::send_service_for_discovery_() {
return; return;
} }
this->send_service_++; // Get the number of characteristics BEFORE adding to response
resp.services.emplace_back();
auto &service_resp = resp.services.back();
fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service_result.uuid, use_efficient_uuids);
service_resp.handle = service_result.start_handle;
// Get the number of characteristics directly with one call
uint16_t total_char_count = 0; uint16_t total_char_count = 0;
esp_gatt_status_t char_count_status = esp_gatt_status_t char_count_status =
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC, esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
@ -139,11 +167,22 @@ void BluetoothConnection::send_service_for_discovery_() {
return; return;
} }
if (total_char_count == 0) { // If this service likely won't fit, send current batch (unless it's the first)
// No characteristics, continue to next service size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids);
continue; 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, service_result.uuid, use_efficient_uuids);
service_resp.handle = service_result.start_handle;
if (total_char_count > 0) {
// Reserve space and process characteristics // Reserve space and process characteristics
service_resp.characteristics.reserve(total_char_count); service_resp.characteristics.reserve(total_char_count);
uint16_t char_offset = 0; uint16_t char_offset = 0;
@ -181,8 +220,8 @@ void BluetoothConnection::send_service_for_discovery_() {
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_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) { if (desc_count_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_, ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
this->address_str().c_str(), char_result.char_handle, desc_count_status); this->connection_index_, this->address_str().c_str(), char_result.char_handle, desc_count_status);
this->send_service_ = DONE_SENDING_SERVICES; this->send_service_ = DONE_SENDING_SERVICES;
return; return;
} }
@ -221,9 +260,40 @@ void BluetoothConnection::send_service_for_discovery_() {
desc_offset++; desc_offset++;
} }
} }
} // end if (total_char_count > 0)
// Calculate the actual size of just this service
api::ProtoSize service_sizer;
service_resp.calculate_size(service_sizer);
size_t service_size = service_sizer.get_size() + 1; // +1 for field tag
// Check if adding this service would exceed the limit
if (current_size + service_size > MAX_PACKET_SIZE) {
// We would go over - pop the last service if we have more than one
if (resp.services.size() > 1) {
resp.services.pop_back();
ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch",
this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size,
MAX_PACKET_SIZE);
// Don't increment send_service_ - we'll retry this service in next batch
} else {
// This single service is too large, but we have to send it anyway
ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_,
this->address_str().c_str(), this->send_service_, service_size);
// Increment so we don't get stuck
this->send_service_++;
}
// Send what we have
break;
} }
// Send the message with 1-3 services // Now we know we're keeping this service, add its size
current_size += service_size;
// Successfully added this service, increment counter
this->send_service_++;
}
// Send the message with dynamically batched services
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
} }

View File

@ -22,7 +22,6 @@ namespace esphome::bluetooth_proxy {
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
static const int DONE_SENDING_SERVICES = -2; static const int DONE_SENDING_SERVICES = -2;
static const uint8_t MAX_SERVICES_PER_BATCH = 3;
using namespace esp32_ble_client; using namespace esp32_ble_client;