From 28b277c1c487abb146f5e294dfe02001682029f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Jul 2025 11:20:53 -1000 Subject: [PATCH 1/4] [bluetooth_proxy] Optimize UUID transmission with efficient short_uuid field (#9995) --- esphome/components/api/api.proto | 24 +++++++++-- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_connection.h | 7 ++++ esphome/components/api/api_options.proto | 1 + esphome/components/api/api_pb2.cpp | 42 +++++++++++++------ esphome/components/api/api_pb2.h | 3 ++ esphome/components/api/api_pb2_dump.cpp | 3 ++ .../bluetooth_proxy/bluetooth_connection.cpp | 33 +++++++++++++-- .../bluetooth_proxy/bluetooth_connection.h | 1 + script/api_protobuf/api_protobuf.py | 29 +++++++++++++ 10 files changed, 126 insertions(+), 19 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 27edf4680f..4aa5cc4be0 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1482,21 +1482,39 @@ message BluetoothGATTGetServicesRequest { } message BluetoothGATTDescriptor { - repeated uint64 uuid = 1 [(fixed_array_size) = 2]; + repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; + + // New field for efficient UUID (v1.12+) + // Only one of uuid or short_uuid will be set. + // short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients. + // 128-bit UUIDs always use the uuid field for backwards compatibility. + uint32 short_uuid = 3; // 16-bit or 32-bit UUID } message BluetoothGATTCharacteristic { - repeated uint64 uuid = 1 [(fixed_array_size) = 2]; + repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; uint32 properties = 3; repeated BluetoothGATTDescriptor descriptors = 4; + + // New field for efficient UUID (v1.12+) + // Only one of uuid or short_uuid will be set. + // short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients. + // 128-bit UUIDs always use the uuid field for backwards compatibility. + uint32 short_uuid = 5; // 16-bit or 32-bit UUID } message BluetoothGATTService { - repeated uint64 uuid = 1 [(fixed_array_size) = 2]; + repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; repeated BluetoothGATTCharacteristic characteristics = 3; + + // New field for efficient UUID (v1.12+) + // Only one of uuid or short_uuid will be set. + // short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients. + // 128-bit UUIDs always use the uuid field for backwards compatibility. + uint32 short_uuid = 4; // 16-bit or 32-bit UUID } message BluetoothGATTGetServicesResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c0dbe4e198..8ac6c3b71e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1363,7 +1363,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 11; + resp.api_version_minor = 12; // Temporary string for concatenation - will be valid during send_message call std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.set_server_info(StringRef(server_info)); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 5b64adecb3..21688e601c 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -235,6 +235,13 @@ class APIConnection : public APIServerConnection { this->is_authenticated(); } uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } + + // Get client API version for feature detection + bool client_supports_api_version(uint16_t major, uint16_t minor) const { + return this->client_api_version_major_ > major || + (this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor); + } + void on_fatal_error() override; #ifdef USE_API_PASSWORD void on_unauthenticated_access() override; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 85c805260f..d4b5700024 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -28,6 +28,7 @@ extend google.protobuf.FieldOptions { optional string field_ifdef = 1042; optional uint32 fixed_array_size = 50007; optional bool no_zero_copy = 50008 [default=false]; + optional bool fixed_array_skip_zero = 50009 [default=false]; // container_pointer: Zero-copy optimization for repeated fields. // diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index ef02a5a774..29d0f2842c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1888,44 +1888,62 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI return true; } void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->uuid[0], true); - buffer.encode_uint64(1, this->uuid[1], true); + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + buffer.encode_uint64(1, this->uuid[0], true); + buffer.encode_uint64(1, this->uuid[1], true); + } buffer.encode_uint32(2, this->handle); + buffer.encode_uint32(3, this->short_uuid); } void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + size.add_uint64_force(1, this->uuid[0]); + size.add_uint64_force(1, this->uuid[1]); + } size.add_uint32(1, this->handle); + size.add_uint32(1, this->short_uuid); } void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->uuid[0], true); - buffer.encode_uint64(1, this->uuid[1], true); + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + buffer.encode_uint64(1, this->uuid[0], true); + buffer.encode_uint64(1, this->uuid[1], true); + } buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { buffer.encode_message(4, it, true); } + buffer.encode_uint32(5, this->short_uuid); } void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + size.add_uint64_force(1, this->uuid[0]); + size.add_uint64_force(1, this->uuid[1]); + } size.add_uint32(1, this->handle); size.add_uint32(1, this->properties); size.add_repeated_message(1, this->descriptors); + size.add_uint32(1, this->short_uuid); } void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { - buffer.encode_uint64(1, this->uuid[0], true); - buffer.encode_uint64(1, this->uuid[1], true); + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + buffer.encode_uint64(1, this->uuid[0], true); + buffer.encode_uint64(1, this->uuid[1], true); + } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { buffer.encode_message(3, it, true); } + buffer.encode_uint32(4, this->short_uuid); } void BluetoothGATTService::calculate_size(ProtoSize &size) const { - size.add_uint64_force(1, this->uuid[0]); - size.add_uint64_force(1, this->uuid[1]); + if (this->uuid[0] != 0 || this->uuid[1] != 0) { + size.add_uint64_force(1, this->uuid[0]); + size.add_uint64_force(1, this->uuid[1]); + } size.add_uint32(1, this->handle); size.add_repeated_message(1, this->characteristics); + size.add_uint32(1, this->short_uuid); } void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 6c2ca60e00..524674e6ef 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1857,6 +1857,7 @@ class BluetoothGATTDescriptor : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; + uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1871,6 +1872,7 @@ class BluetoothGATTCharacteristic : public ProtoMessage { uint32_t handle{0}; uint32_t properties{0}; std::vector descriptors{}; + uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1884,6 +1886,7 @@ class BluetoothGATTService : public ProtoMessage { std::array uuid{}; uint32_t handle{0}; std::vector characteristics{}; + uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index b934aead32..b212353ad8 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1561,6 +1561,7 @@ void BluetoothGATTDescriptor::dump_to(std::string &out) const { dump_field(out, "uuid", it, 4); } dump_field(out, "handle", this->handle); + dump_field(out, "short_uuid", this->short_uuid); } void BluetoothGATTCharacteristic::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTCharacteristic"); @@ -1574,6 +1575,7 @@ void BluetoothGATTCharacteristic::dump_to(std::string &out) const { it.dump_to(out); out.append("\n"); } + dump_field(out, "short_uuid", this->short_uuid); } void BluetoothGATTService::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTService"); @@ -1586,6 +1588,7 @@ void BluetoothGATTService::dump_to(std::string &out) const { it.dump_to(out); out.append("\n"); } + dump_field(out, "short_uuid", this->short_uuid); } void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse"); diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 1295c18985..4f312fce30 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -24,6 +24,24 @@ static void fill_128bit_uuid_array(std::array &out, esp_bt_uuid_t u ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]); } +// Helper to fill UUID in the appropriate format based on client support and UUID type +static void fill_gatt_uuid(std::array &uuid_128, uint32_t &short_uuid, const esp_bt_uuid_t &uuid, + bool use_efficient_uuids) { + if (!use_efficient_uuids || uuid.len == ESP_UUID_LEN_128) { + // Use 128-bit format for old clients or when UUID is already 128-bit + fill_128bit_uuid_array(uuid_128, uuid); + } else if (uuid.len == ESP_UUID_LEN_16) { + short_uuid = uuid.uuid.uuid16; + } else if (uuid.len == ESP_UUID_LEN_32) { + short_uuid = uuid.uuid.uuid32; + } +} + +bool BluetoothConnection::supports_efficient_uuids_() const { + auto *api_conn = this->proxy_->get_api_connection(); + return api_conn && api_conn->client_supports_api_version(1, 12); +} + void BluetoothConnection::dump_config() { ESP_LOGCONFIG(TAG, "BLE Connection:"); BLEClientBase::dump_config(); @@ -74,6 +92,9 @@ void BluetoothConnection::send_service_for_discovery_() { return; } + // Check if client supports efficient UUIDs + bool use_efficient_uuids = this->supports_efficient_uuids_(); + // Prepare response for up to 3 services api::BluetoothGATTGetServicesResponse resp; resp.address = this->address_; @@ -100,7 +121,9 @@ void BluetoothConnection::send_service_for_discovery_() { this->send_service_++; resp.services.emplace_back(); auto &service_resp = resp.services.back(); - fill_128bit_uuid_array(service_resp.uuid, service_result.uuid); + + 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 @@ -145,7 +168,9 @@ void BluetoothConnection::send_service_for_discovery_() { service_resp.characteristics.emplace_back(); auto &characteristic_resp = service_resp.characteristics.back(); - fill_128bit_uuid_array(characteristic_resp.uuid, char_result.uuid); + + fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids); + characteristic_resp.handle = char_result.char_handle; characteristic_resp.properties = char_result.properties; char_offset++; @@ -189,7 +214,9 @@ void BluetoothConnection::send_service_for_discovery_() { characteristic_resp.descriptors.emplace_back(); auto &descriptor_resp = characteristic_resp.descriptors.back(); - fill_128bit_uuid_array(descriptor_resp.uuid, desc_result.uuid); + + fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids); + descriptor_resp.handle = desc_result.handle; desc_offset++; } diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 3fed9d531f..622d257bf8 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -27,6 +27,7 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { protected: friend class BluetoothProxy; + bool supports_efficient_uuids_() const; void send_service_for_discovery_(); void reset_connection_(esp_err_t reason); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index fece22499a..03f8d0f8bc 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1075,6 +1075,11 @@ class FixedArrayRepeatedType(TypeInfo): def __init__(self, field: descriptor.FieldDescriptorProto, size: int) -> None: super().__init__(field) self.array_size = size + # Check if we should skip encoding when all elements are zero + # Use getattr to handle older versions of api_options_pb2 + self.skip_zero = get_field_opt( + field, getattr(pb, "fixed_array_skip_zero", None), False + ) # Create the element type info validate_field_type(field.type, field.name) self._ti: TypeInfo = TYPE_INFO[field.type](field) @@ -1113,6 +1118,18 @@ class FixedArrayRepeatedType(TypeInfo): else: return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + # If skip_zero is enabled, wrap encoding in a zero check + if self.skip_zero: + # Build the condition to check if at least one element is non-zero + non_zero_checks = " || ".join( + [f"this->{self.field_name}[{i}] != 0" for i in range(self.array_size)] + ) + encode_lines = [ + f" {encode_element(f'this->{self.field_name}[{i}]')}" + for i in range(self.array_size) + ] + return f"if ({non_zero_checks}) {{\n" + "\n".join(encode_lines) + "\n}" + # Unroll small arrays for efficiency if self.array_size == 1: return encode_element(f"this->{self.field_name}[0]") @@ -1141,6 +1158,18 @@ class FixedArrayRepeatedType(TypeInfo): return "" def get_size_calculation(self, name: str, force: bool = False) -> str: + # If skip_zero is enabled, wrap size calculation in a zero check + if self.skip_zero: + # Build the condition to check if at least one element is non-zero + non_zero_checks = " || ".join( + [f"{name}[{i}] != 0" for i in range(self.array_size)] + ) + size_lines = [ + f" {self._ti.get_size_calculation(f'{name}[{i}]', True)}" + for i in range(self.array_size) + ] + return f"if ({non_zero_checks}) {{\n" + "\n".join(size_lines) + "\n}" + # For fixed arrays, we always encode all elements # Special case for single-element arrays - no loop needed From 7205b1edf0820a3d25c857f7ff637c6e0c09f29c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Jul 2025 07:59:49 -1000 Subject: [PATCH 2/4] [bluetooth_proxy] Implement dynamic service batching based on MTU constraints --- .../bluetooth_proxy/bluetooth_connection.cpp | 244 ++++++++++++------ .../bluetooth_proxy/bluetooth_proxy.h | 1 - 2 files changed, 163 insertions(+), 82 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 4f312fce30..1f4bfd9806 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -37,6 +37,35 @@ static void fill_gatt_uuid(std::array &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 +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; +} + +// Helper to calculate actual service size +static size_t get_service_size(api::BluetoothGATTService &service) { + api::ProtoSize service_size; + service.calculate_size(service_size); + size_t size = service_size.get_size(); + ESP_LOGV(TAG, "Service size calculation: uuid[0]=%llx uuid[1]=%llx short_uuid=%u handle=%u -> size=%d", + service.uuid[0], service.uuid[1], service.short_uuid, service.handle, size); + return size; +} + bool BluetoothConnection::supports_efficient_uuids_() const { auto *api_conn = this->proxy_->get_api_connection(); return api_conn && api_conn->client_supports_api_version(1, 12); @@ -95,16 +124,23 @@ void BluetoothConnection::send_service_for_discovery_() { // Check if client supports efficient UUIDs bool use_efficient_uuids = this->supports_efficient_uuids_(); - // Prepare response for up to 3 services + // Prepare response api::BluetoothGATTGetServicesResponse resp; resp.address = this->address_; - // Process up to 3 services in this iteration - uint8_t services_to_process = - std::min(MAX_SERVICES_PER_BATCH, static_cast(this->service_count_ - this->send_service_)); - resp.services.reserve(services_to_process); + // Dynamic batching based on actual size + static constexpr size_t MAX_PACKET_SIZE = + 1360; // Conservative MTU limit for API messages (accounts for WPA3 overhead) - 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(); + ESP_LOGV(TAG, "[%d] [%s] Starting batch with base size: %d, send_service_: %d", this->connection_index_, + this->address_str().c_str(), current_size, 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, @@ -118,15 +154,7 @@ void BluetoothConnection::send_service_for_discovery_() { return; } - this->send_service_++; - 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 + // 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, @@ -139,91 +167,145 @@ void BluetoothConnection::send_service_for_discovery_() { return; } - if (total_char_count == 0) { - // No characteristics, continue to next service - continue; + // 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; } - // Reserve space and process characteristics - service_resp.characteristics.reserve(total_char_count); - uint16_t char_offset = 0; - esp_gattc_char_elem_t char_result; - while (true) { // 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) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, - this->address_str().c_str(), char_status); - this->send_service_ = DONE_SENDING_SERVICES; - return; - } - if (char_count == 0) { - break; - } + // Now add the service since we know it will likely fit + resp.services.emplace_back(); + auto &service_resp = resp.services.back(); - service_resp.characteristics.emplace_back(); - auto &characteristic_resp = service_resp.characteristics.back(); + fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service_result.uuid, use_efficient_uuids); - fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids); + service_resp.handle = service_result.start_handle; - characteristic_resp.handle = char_result.char_handle; - characteristic_resp.properties = char_result.properties; - char_offset++; + ESP_LOGV(TAG, "[%d] [%s] Service UUID: %llx,%llx short:%u handle:%u", this->connection_index_, + this->address_str().c_str(), service_resp.uuid[0], service_resp.uuid[1], service_resp.short_uuid, + service_resp.handle); - // 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) { - ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_, - this->address_str().c_str(), char_result.char_handle, desc_count_status); - this->send_service_ = DONE_SENDING_SERVICES; - return; - } - if (total_desc_count == 0) { - // No descriptors, continue to next characteristic - continue; - } - - // Reserve space and process descriptors - characteristic_resp.descriptors.reserve(total_desc_count); - uint16_t desc_offset = 0; - esp_gattc_descr_elem_t desc_result; - while (true) { // 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) { + if (total_char_count > 0) { + // Reserve space and process characteristics + service_resp.characteristics.reserve(total_char_count); + uint16_t char_offset = 0; + esp_gattc_char_elem_t char_result; + while (true) { // 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 (desc_status != ESP_GATT_OK) { - ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_, - this->address_str().c_str(), desc_status); + if (char_status != ESP_GATT_OK) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, + this->address_str().c_str(), char_status); this->send_service_ = DONE_SENDING_SERVICES; return; } - if (desc_count == 0) { - break; // No more descriptors + if (char_count == 0) { + break; } - characteristic_resp.descriptors.emplace_back(); - auto &descriptor_resp = characteristic_resp.descriptors.back(); + service_resp.characteristics.emplace_back(); + auto &characteristic_resp = service_resp.characteristics.back(); - fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids); + fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids); - descriptor_resp.handle = desc_result.handle; - desc_offset++; + 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) { + ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", + this->connection_index_, this->address_str().c_str(), char_result.char_handle, desc_count_status); + this->send_service_ = DONE_SENDING_SERVICES; + return; + } + if (total_desc_count == 0) { + // No descriptors, continue to next characteristic + continue; + } + + // Reserve space and process descriptors + characteristic_resp.descriptors.reserve(total_desc_count); + uint16_t desc_offset = 0; + esp_gattc_descr_elem_t desc_result; + while (true) { // 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) { + ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_, + this->address_str().c_str(), 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, desc_result.uuid, use_efficient_uuids); + + descriptor_resp.handle = desc_result.handle; + desc_offset++; + } } + } // end if (total_char_count > 0) + + // Calculate the actual size of just this service + size_t service_size = get_service_size(service_resp) + 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 + current_size += service_size; + 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; } + + // Now we know we're keeping this service, add its size + current_size += service_size; + + // Log the difference between estimate and actual size + int size_diff = (int) service_size - (int) estimated_size; + ESP_LOGV(TAG, "[%d] [%s] Service %d actual: %d, estimated: %d, diff: %+d", this->connection_index_, + this->address_str().c_str(), this->send_service_, service_size, estimated_size, size_diff); + ESP_LOGV(TAG, "[%d] [%s] Total size now: %d", this->connection_index_, this->address_str().c_str(), current_size); + + // Successfully added this service, increment counter + this->send_service_++; } - // Send the message with 1-3 services + // Send the message with dynamically batched services + ESP_LOGD(TAG, "[%d] [%s] Sending batch with %d services, total size %d", this->connection_index_, + this->address_str().c_str(), resp.services.size(), current_size); api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index b33460339b..d249515fdf 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -22,7 +22,6 @@ namespace esphome::bluetooth_proxy { static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; static const int DONE_SENDING_SERVICES = -2; -static const uint8_t MAX_SERVICES_PER_BATCH = 3; using namespace esp32_ble_client; From 0f19e234863093abba14f0dac5d150033e7474c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Jul 2025 11:25:33 -1000 Subject: [PATCH 3/4] Update esphome/components/bluetooth_proxy/bluetooth_connection.cpp --- esphome/components/bluetooth_proxy/bluetooth_connection.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 1f4bfd9806..c1932e79e6 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -297,7 +297,6 @@ void BluetoothConnection::send_service_for_discovery_() { int size_diff = (int) service_size - (int) estimated_size; ESP_LOGV(TAG, "[%d] [%s] Service %d actual: %d, estimated: %d, diff: %+d", this->connection_index_, this->address_str().c_str(), this->send_service_, service_size, estimated_size, size_diff); - ESP_LOGV(TAG, "[%d] [%s] Total size now: %d", this->connection_index_, this->address_str().c_str(), current_size); // Successfully added this service, increment counter this->send_service_++; From dd7441e104b8b24ba47bb67e8efa4805d64cf0b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Jul 2025 11:25:48 -1000 Subject: [PATCH 4/4] Update esphome/components/bluetooth_proxy/bluetooth_connection.cpp --- esphome/components/bluetooth_proxy/bluetooth_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index c1932e79e6..2f350d74fc 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -303,7 +303,7 @@ void BluetoothConnection::send_service_for_discovery_() { } // Send the message with dynamically batched services - ESP_LOGD(TAG, "[%d] [%s] Sending batch with %d services, total size %d", this->connection_index_, + ESP_LOGV(TAG, "[%d] [%s] Sending batch with %d services, total size %d", this->connection_index_, this->address_str().c_str(), resp.services.size(), current_size); api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); }