From 91e1a4ff7630b4fb6f50e970785cc8196ad0fa5a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Jul 2025 12:49:48 -1000 Subject: [PATCH] fixed arrays --- esphome/components/api/api.proto | 8 +- esphome/components/api/api_pb2.cpp | 30 ++--- esphome/components/api/api_pb2.h | 14 +- .../bluetooth_proxy/bluetooth_connection.cpp | 28 ++-- script/api_protobuf/api_protobuf.py | 123 ++++++++++++++++++ 5 files changed, 163 insertions(+), 40 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e7c2fcaf8a..93e84702e2 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1463,19 +1463,19 @@ message BluetoothGATTGetServicesRequest { } message BluetoothGATTDescriptor { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [(fixed_array_size) = 2]; uint32 handle = 2; } message BluetoothGATTCharacteristic { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [(fixed_array_size) = 2]; uint32 handle = 2; uint32 properties = 3; repeated BluetoothGATTDescriptor descriptors = 4; } message BluetoothGATTService { - repeated uint64 uuid = 1; + repeated uint64 uuid = 1 [(fixed_array_size) = 2]; uint32 handle = 2; repeated BluetoothGATTCharacteristic characteristics = 3; } @@ -1486,7 +1486,7 @@ message BluetoothGATTGetServicesResponse { option (ifdef) = "USE_BLUETOOTH_PROXY"; uint64 address = 1; - repeated BluetoothGATTService services = 2; + repeated BluetoothGATTService services = 2 [(fixed_array_size) = 1]; } message BluetoothGATTGetServicesDoneResponse { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 28d135ed6d..9d8b109986 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1891,21 +1891,19 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI return true; } void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->uuid) { + for (const auto &it : this->uuid) { buffer.encode_uint64(1, it, true); } buffer.encode_uint32(2, this->handle); } void BluetoothGATTDescriptor::calculate_size(uint32_t &total_size) const { - if (!this->uuid.empty()) { - for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field_repeated(total_size, 1, it); - } + for (const auto &it : this->uuid) { + ProtoSize::add_uint64_field_repeated(total_size, 1, it); } ProtoSize::add_uint32_field(total_size, 1, this->handle); } void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->uuid) { + for (const auto &it : this->uuid) { buffer.encode_uint64(1, it, true); } buffer.encode_uint32(2, this->handle); @@ -1915,17 +1913,15 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { } } void BluetoothGATTCharacteristic::calculate_size(uint32_t &total_size) const { - if (!this->uuid.empty()) { - for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field_repeated(total_size, 1, it); - } + for (const auto &it : this->uuid) { + ProtoSize::add_uint64_field_repeated(total_size, 1, it); } ProtoSize::add_uint32_field(total_size, 1, this->handle); ProtoSize::add_uint32_field(total_size, 1, this->properties); ProtoSize::add_repeated_message(total_size, 1, this->descriptors); } void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { - for (auto &it : this->uuid) { + for (const auto &it : this->uuid) { buffer.encode_uint64(1, it, true); } buffer.encode_uint32(2, this->handle); @@ -1934,23 +1930,23 @@ void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { } } void BluetoothGATTService::calculate_size(uint32_t &total_size) const { - if (!this->uuid.empty()) { - for (const auto &it : this->uuid) { - ProtoSize::add_uint64_field_repeated(total_size, 1, it); - } + for (const auto &it : this->uuid) { + ProtoSize::add_uint64_field_repeated(total_size, 1, it); } ProtoSize::add_uint32_field(total_size, 1, this->handle); ProtoSize::add_repeated_message(total_size, 1, this->characteristics); } void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); - for (auto &it : this->services) { + for (const auto &it : this->services) { buffer.encode_message(2, it, true); } } void BluetoothGATTGetServicesResponse::calculate_size(uint32_t &total_size) const { ProtoSize::add_uint64_field(total_size, 1, this->address); - ProtoSize::add_repeated_message(total_size, 1, this->services); + for (const auto &it : this->services) { + ProtoSize::add_message_object_repeated(total_size, 1, it); + } } void BluetoothGATTGetServicesDoneResponse::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 7255aa7903..7a9726f6e4 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1796,7 +1796,8 @@ class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { }; class BluetoothGATTDescriptor : public ProtoMessage { public: - std::vector uuid{}; + std::array uuid{}; + size_t uuid_index_{0}; uint32_t handle{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; @@ -1808,7 +1809,8 @@ class BluetoothGATTDescriptor : public ProtoMessage { }; class BluetoothGATTCharacteristic : public ProtoMessage { public: - std::vector uuid{}; + std::array uuid{}; + size_t uuid_index_{0}; uint32_t handle{0}; uint32_t properties{0}; std::vector descriptors{}; @@ -1822,7 +1824,8 @@ class BluetoothGATTCharacteristic : public ProtoMessage { }; class BluetoothGATTService : public ProtoMessage { public: - std::vector uuid{}; + std::array uuid{}; + size_t uuid_index_{0}; uint32_t handle{0}; std::vector characteristics{}; void encode(ProtoWriteBuffer buffer) const override; @@ -1836,12 +1839,13 @@ class BluetoothGATTService : public ProtoMessage { class BluetoothGATTGetServicesResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 71; - static constexpr uint8_t ESTIMATED_SIZE = 38; + static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } #endif uint64_t address{0}; - std::vector services{}; + std::array services{}; + size_t services_index_{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(uint32_t &total_size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 7c883b74a2..aae08286c9 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -13,16 +13,17 @@ namespace bluetooth_proxy { static const char *const TAG = "bluetooth_proxy.connection"; -static std::vector get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { +static std::array get_128bit_uuid_array(esp_bt_uuid_t uuid_source) { esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); - return std::vector{((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | - ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | - ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | - ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]), - ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | - ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | - ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | - ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; + return std::array{ + ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | + ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | + ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | + ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]), + ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | + ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | + ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | + ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])}; } void BluetoothConnection::dump_config() { @@ -95,9 +96,8 @@ void BluetoothConnection::send_service_for_discovery_() { api::BluetoothGATTGetServicesResponse resp; resp.address = this->address_; - resp.services.emplace_back(); - auto &service_resp = resp.services.back(); - service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); + auto &service_resp = resp.services[0]; + service_resp.uuid = get_128bit_uuid_array(service_result.uuid); service_resp.handle = service_result.start_handle; // Get the number of characteristics directly with one call @@ -136,7 +136,7 @@ void BluetoothConnection::send_service_for_discovery_() { service_resp.characteristics.emplace_back(); auto &characteristic_resp = service_resp.characteristics.back(); - characteristic_resp.uuid = get_128bit_uuid_vec(char_result.uuid); + characteristic_resp.uuid = get_128bit_uuid_array(char_result.uuid); characteristic_resp.handle = char_result.char_handle; characteristic_resp.properties = char_result.properties; char_offset++; @@ -176,7 +176,7 @@ void BluetoothConnection::send_service_for_discovery_() { characteristic_resp.descriptors.emplace_back(); auto &descriptor_resp = characteristic_resp.descriptors.back(); - descriptor_resp.uuid = get_128bit_uuid_vec(desc_result.uuid); + descriptor_resp.uuid = get_128bit_uuid_array(desc_result.uuid); descriptor_resp.handle = desc_result.handle; desc_offset++; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2678b7009a..3ca390658b 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -327,6 +327,9 @@ def create_field_type_info( ) -> TypeInfo: """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" if field.label == 3: # repeated + # Check if this repeated field has fixed_array_size option + if (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None: + return FixedArrayRepeatedType(field, fixed_size) return RepeatedTypeInfo(field) # Check for fixed_array_size option on bytes fields @@ -883,6 +886,126 @@ class SInt64Type(TypeInfo): return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint +class FixedArrayRepeatedType(TypeInfo): + """Special type for fixed-size repeated fields using std::array.""" + + def __init__(self, field: descriptor.FieldDescriptorProto, size: int) -> None: + super().__init__(field) + self.array_size = size + # Create the element type info + validate_field_type(field.type, field.name) + self._ti: TypeInfo = TYPE_INFO[field.type](field) + + @property + def cpp_type(self) -> str: + return f"std::array<{self._ti.cpp_type}, {self.array_size}>" + + @property + def reference_type(self) -> str: + return f"{self.cpp_type} &" + + @property + def const_reference_type(self) -> str: + return f"const {self.cpp_type} &" + + @property + def wire_type(self) -> WireType: + """Get the wire type for this fixed array field.""" + return self._ti.wire_type + + @property + def public_content(self) -> list[str]: + # Add the array member and a counter for decoding + return [ + f"{self.cpp_type} {self.field_name}{{}};", + f"size_t {self.field_name}_index_{{0}};", + ] + + @property + def decode_varint_content(self) -> str: + content = self._ti.decode_varint + if content is None: + return None + return f"case {self.number}: if (this->{self.field_name}_index_ < {self.array_size}) {{ this->{self.field_name}[this->{self.field_name}_index_++] = {content}; }} break;" + + @property + def decode_length_content(self) -> str: + content = self._ti.decode_length + if content is None and isinstance(self._ti, MessageType): + # Special handling for non-template message decoding + return f"case {self.number}: if (this->{self.field_name}_index_ < {self.array_size}) {{ value.decode_to_message(this->{self.field_name}[this->{self.field_name}_index_++]); }} break;" + if content is None: + return None + return f"case {self.number}: if (this->{self.field_name}_index_ < {self.array_size}) {{ this->{self.field_name}[this->{self.field_name}_index_++] = {content}; }} break;" + + @property + def decode_32bit_content(self) -> str: + content = self._ti.decode_32bit + if content is None: + return None + return f"case {self.number}: if (this->{self.field_name}_index_ < {self.array_size}) {{ this->{self.field_name}[this->{self.field_name}_index_++] = {content}; }} break;" + + @property + def decode_64bit_content(self) -> str: + content = self._ti.decode_64bit + if content is None: + return None + return f"case {self.number}: if (this->{self.field_name}_index_ < {self.array_size}) {{ this->{self.field_name}[this->{self.field_name}_index_++] = {content}; }} break;" + + @property + def _ti_is_bool(self) -> bool: + # std::array doesn't have the same specialization issues as std::vector for bool + return False + + @property + def encode_content(self) -> str: + o = f"for (const auto &it : this->{self.field_name}) {{\n" + if isinstance(self._ti, EnumType): + o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" + else: + o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o += "}" + return o + + @property + def dump_content(self) -> str: + o = f"for (const auto &it : this->{self.field_name}) {{\n" + o += f' out.append(" {self.name}: ");\n' + o += indent(self._ti.dump("it")) + "\n" + o += ' out.append("\\n");\n' + o += "}\n" + return o + + def dump(self, _: str): + pass + + def get_size_calculation(self, name: str, force: bool = False) -> str: + # For fixed arrays, we always encode all elements + # Check if this is a fixed-size type by seeing if it has a fixed byte count + num_bytes = self._ti.get_fixed_size_bytes() + if num_bytes is not None: + # Fixed types have constant size per element, so we can multiply + field_id_size = self._ti.calculate_field_id_size() + # Pre-calculate the total bytes per element + bytes_per_element = field_id_size + num_bytes + o = f"total_size += {self.array_size} * {bytes_per_element};" + else: + # Other types need the actual value + o = f"for (const auto &it : {name}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" + o += "}" + return o + + def get_estimated_size(self) -> int: + # For fixed arrays, estimate underlying type size * array size + underlying_size = ( + self._ti.get_estimated_size() + if hasattr(self._ti, "get_estimated_size") + else 8 + ) + return underlying_size * self.array_size + + class RepeatedTypeInfo(TypeInfo): def __init__(self, field: descriptor.FieldDescriptorProto) -> None: super().__init__(field)