[bluetooth_proxy] Optimize UUID transmission with efficient short_uuid field (#9995)

This commit is contained in:
J. Nick Koston 2025-07-31 11:20:53 -10:00 committed by GitHub
parent 936a090aaa
commit 28b277c1c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 126 additions and 19 deletions

View File

@ -1482,21 +1482,39 @@ message BluetoothGATTGetServicesRequest {
} }
message BluetoothGATTDescriptor { 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; 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 { 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 handle = 2;
uint32 properties = 3; uint32 properties = 3;
repeated BluetoothGATTDescriptor descriptors = 4; 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 { 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; uint32 handle = 2;
repeated BluetoothGATTCharacteristic characteristics = 3; 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 { message BluetoothGATTGetServicesResponse {

View File

@ -1363,7 +1363,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; 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 // Temporary string for concatenation - will be valid during send_message call
std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
resp.set_server_info(StringRef(server_info)); resp.set_server_info(StringRef(server_info));

View File

@ -235,6 +235,13 @@ class APIConnection : public APIServerConnection {
this->is_authenticated(); this->is_authenticated();
} }
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } 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; void on_fatal_error() override;
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
void on_unauthenticated_access() override; void on_unauthenticated_access() override;

View File

@ -28,6 +28,7 @@ extend google.protobuf.FieldOptions {
optional string field_ifdef = 1042; optional string field_ifdef = 1042;
optional uint32 fixed_array_size = 50007; optional uint32 fixed_array_size = 50007;
optional bool no_zero_copy = 50008 [default=false]; 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. // container_pointer: Zero-copy optimization for repeated fields.
// //

View File

@ -1888,44 +1888,62 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI
return true; return true;
} }
void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->uuid[0], true); if (this->uuid[0] != 0 || this->uuid[1] != 0) {
buffer.encode_uint64(1, this->uuid[1], true); 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(2, this->handle);
buffer.encode_uint32(3, this->short_uuid);
} }
void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const { void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const {
size.add_uint64_force(1, this->uuid[0]); if (this->uuid[0] != 0 || this->uuid[1] != 0) {
size.add_uint64_force(1, this->uuid[1]); 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->handle);
size.add_uint32(1, this->short_uuid);
} }
void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->uuid[0], true); if (this->uuid[0] != 0 || this->uuid[1] != 0) {
buffer.encode_uint64(1, this->uuid[1], true); 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(2, this->handle);
buffer.encode_uint32(3, this->properties); buffer.encode_uint32(3, this->properties);
for (auto &it : this->descriptors) { for (auto &it : this->descriptors) {
buffer.encode_message(4, it, true); buffer.encode_message(4, it, true);
} }
buffer.encode_uint32(5, this->short_uuid);
} }
void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const { void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const {
size.add_uint64_force(1, this->uuid[0]); if (this->uuid[0] != 0 || this->uuid[1] != 0) {
size.add_uint64_force(1, this->uuid[1]); 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->handle);
size.add_uint32(1, this->properties); size.add_uint32(1, this->properties);
size.add_repeated_message(1, this->descriptors); size.add_repeated_message(1, this->descriptors);
size.add_uint32(1, this->short_uuid);
} }
void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->uuid[0], true); if (this->uuid[0] != 0 || this->uuid[1] != 0) {
buffer.encode_uint64(1, this->uuid[1], true); 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(2, this->handle);
for (auto &it : this->characteristics) { for (auto &it : this->characteristics) {
buffer.encode_message(3, it, true); buffer.encode_message(3, it, true);
} }
buffer.encode_uint32(4, this->short_uuid);
} }
void BluetoothGATTService::calculate_size(ProtoSize &size) const { void BluetoothGATTService::calculate_size(ProtoSize &size) const {
size.add_uint64_force(1, this->uuid[0]); if (this->uuid[0] != 0 || this->uuid[1] != 0) {
size.add_uint64_force(1, this->uuid[1]); 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->handle);
size.add_repeated_message(1, this->characteristics); size.add_repeated_message(1, this->characteristics);
size.add_uint32(1, this->short_uuid);
} }
void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->address); buffer.encode_uint64(1, this->address);

View File

@ -1857,6 +1857,7 @@ class BluetoothGATTDescriptor : public ProtoMessage {
public: public:
std::array<uint64_t, 2> uuid{}; std::array<uint64_t, 2> uuid{};
uint32_t handle{0}; uint32_t handle{0};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1871,6 +1872,7 @@ class BluetoothGATTCharacteristic : public ProtoMessage {
uint32_t handle{0}; uint32_t handle{0};
uint32_t properties{0}; uint32_t properties{0};
std::vector<BluetoothGATTDescriptor> descriptors{}; std::vector<BluetoothGATTDescriptor> descriptors{};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1884,6 +1886,7 @@ class BluetoothGATTService : public ProtoMessage {
std::array<uint64_t, 2> uuid{}; std::array<uint64_t, 2> uuid{};
uint32_t handle{0}; uint32_t handle{0};
std::vector<BluetoothGATTCharacteristic> characteristics{}; std::vector<BluetoothGATTCharacteristic> characteristics{};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP

View File

@ -1561,6 +1561,7 @@ void BluetoothGATTDescriptor::dump_to(std::string &out) const {
dump_field(out, "uuid", it, 4); dump_field(out, "uuid", it, 4);
} }
dump_field(out, "handle", this->handle); dump_field(out, "handle", this->handle);
dump_field(out, "short_uuid", this->short_uuid);
} }
void BluetoothGATTCharacteristic::dump_to(std::string &out) const { void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTCharacteristic"); MessageDumpHelper helper(out, "BluetoothGATTCharacteristic");
@ -1574,6 +1575,7 @@ void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
it.dump_to(out); it.dump_to(out);
out.append("\n"); out.append("\n");
} }
dump_field(out, "short_uuid", this->short_uuid);
} }
void BluetoothGATTService::dump_to(std::string &out) const { void BluetoothGATTService::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTService"); MessageDumpHelper helper(out, "BluetoothGATTService");
@ -1586,6 +1588,7 @@ void BluetoothGATTService::dump_to(std::string &out) const {
it.dump_to(out); it.dump_to(out);
out.append("\n"); out.append("\n");
} }
dump_field(out, "short_uuid", this->short_uuid);
} }
void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse"); MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse");

View File

@ -24,6 +24,24 @@ static void fill_128bit_uuid_array(std::array<uint64_t, 2> &out, esp_bt_uuid_t u
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]); ((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<uint64_t, 2> &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() { void BluetoothConnection::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Connection:"); ESP_LOGCONFIG(TAG, "BLE Connection:");
BLEClientBase::dump_config(); BLEClientBase::dump_config();
@ -74,6 +92,9 @@ void BluetoothConnection::send_service_for_discovery_() {
return; return;
} }
// Check if client supports efficient UUIDs
bool use_efficient_uuids = this->supports_efficient_uuids_();
// Prepare response for up to 3 services // Prepare response for up to 3 services
api::BluetoothGATTGetServicesResponse resp; api::BluetoothGATTGetServicesResponse resp;
resp.address = this->address_; resp.address = this->address_;
@ -100,7 +121,9 @@ void BluetoothConnection::send_service_for_discovery_() {
this->send_service_++; this->send_service_++;
resp.services.emplace_back(); resp.services.emplace_back();
auto &service_resp = resp.services.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; service_resp.handle = service_result.start_handle;
// Get the number of characteristics directly with one call // Get the number of characteristics directly with one call
@ -145,7 +168,9 @@ void BluetoothConnection::send_service_for_discovery_() {
service_resp.characteristics.emplace_back(); service_resp.characteristics.emplace_back();
auto &characteristic_resp = service_resp.characteristics.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.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties; characteristic_resp.properties = char_result.properties;
char_offset++; char_offset++;
@ -189,7 +214,9 @@ void BluetoothConnection::send_service_for_discovery_() {
characteristic_resp.descriptors.emplace_back(); characteristic_resp.descriptors.emplace_back();
auto &descriptor_resp = characteristic_resp.descriptors.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; descriptor_resp.handle = desc_result.handle;
desc_offset++; desc_offset++;
} }

View File

@ -27,6 +27,7 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
protected: protected:
friend class BluetoothProxy; friend class BluetoothProxy;
bool supports_efficient_uuids_() const;
void send_service_for_discovery_(); void send_service_for_discovery_();
void reset_connection_(esp_err_t reason); void reset_connection_(esp_err_t reason);

View File

@ -1075,6 +1075,11 @@ class FixedArrayRepeatedType(TypeInfo):
def __init__(self, field: descriptor.FieldDescriptorProto, size: int) -> None: def __init__(self, field: descriptor.FieldDescriptorProto, size: int) -> None:
super().__init__(field) super().__init__(field)
self.array_size = size 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 # Create the element type info
validate_field_type(field.type, field.name) validate_field_type(field.type, field.name)
self._ti: TypeInfo = TYPE_INFO[field.type](field) self._ti: TypeInfo = TYPE_INFO[field.type](field)
@ -1113,6 +1118,18 @@ class FixedArrayRepeatedType(TypeInfo):
else: else:
return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" 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 # Unroll small arrays for efficiency
if self.array_size == 1: if self.array_size == 1:
return encode_element(f"this->{self.field_name}[0]") return encode_element(f"this->{self.field_name}[0]")
@ -1141,6 +1158,18 @@ class FixedArrayRepeatedType(TypeInfo):
return "" return ""
def get_size_calculation(self, name: str, force: bool = False) -> str: 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 # For fixed arrays, we always encode all elements
# Special case for single-element arrays - no loop needed # Special case for single-element arrays - no loop needed