From 28b277c1c487abb146f5e294dfe02001682029f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 31 Jul 2025 11:20:53 -1000 Subject: [PATCH] [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