From a9fd0a3b26eed1059aa82390b0addbc00e1d916d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 18:21:14 -1000 Subject: [PATCH] fixed_vector, bluetooth services --- esphome/components/api/api.proto | 4 +- esphome/components/api/api_options.proto | 6 +++ esphome/components/api/api_pb2.h | 4 +- esphome/components/api/proto.h | 26 +++++++++-- .../bluetooth_proxy/bluetooth_connection.cpp | 8 ++-- esphome/core/helpers.h | 46 +++++++++++++++++++ script/api_protobuf/api_protobuf.py | 4 ++ 7 files changed, 85 insertions(+), 13 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 87f477799d..9b714d00f1 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1519,7 +1519,7 @@ message BluetoothGATTCharacteristic { repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; uint32 properties = 3; - repeated BluetoothGATTDescriptor descriptors = 4; + repeated BluetoothGATTDescriptor descriptors = 4 [(fixed_vector) = true]; // New field for efficient UUID (v1.12+) // Only one of uuid or short_uuid will be set. @@ -1531,7 +1531,7 @@ message BluetoothGATTCharacteristic { message BluetoothGATTService { repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true]; uint32 handle = 2; - repeated BluetoothGATTCharacteristic characteristics = 3; + repeated BluetoothGATTCharacteristic characteristics = 3 [(fixed_vector) = true]; // New field for efficient UUID (v1.12+) // Only one of uuid or short_uuid will be set. diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 633f39b552..ead8ac0bbc 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -64,4 +64,10 @@ extend google.protobuf.FieldOptions { // This is typically done through methods returning const T& or special accessor // methods like get_options() or supported_modes_for_api_(). optional string container_pointer = 50001; + + // fixed_vector: Use FixedVector instead of std::vector for repeated fields + // When set, the repeated field will use FixedVector which requires calling + // init(size) before adding elements. This eliminates std::vector template overhead + // and is ideal when the exact size is known before populating the array. + optional bool fixed_vector = 50013 [default=false]; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index d9e68ece9b..1798458393 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1923,7 +1923,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage { std::array uuid{}; uint32_t handle{0}; uint32_t properties{0}; - std::vector descriptors{}; + FixedVector descriptors{}; uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -1937,7 +1937,7 @@ class BluetoothGATTService final : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; - std::vector characteristics{}; + FixedVector characteristics{}; uint32_t short_uuid{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 9d780692ec..a6a09bf7c5 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -749,13 +749,29 @@ class ProtoSize { template inline void add_repeated_message(uint32_t field_id_size, const std::vector &messages) { // Skip if the vector is empty - if (messages.empty()) { - return; + if (!messages.empty()) { + // Use the force version for all messages in the repeated field + for (const auto &message : messages) { + add_message_object_force(field_id_size, message); + } } + } - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); + /** + * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector + * version) + * + * @tparam MessageType The type of the nested messages in the FixedVector + * @param messages FixedVector of message objects + */ + template + inline void add_repeated_message(uint32_t field_id_size, const FixedVector &messages) { + // Skip if the fixed vector is empty + if (!messages.empty()) { + // Use the force version for all messages in the repeated field + for (const auto &message : messages) { + add_message_object_force(field_id_size, message); + } } } }; diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index cde82fbfb0..6f172b0bcf 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -230,8 +230,8 @@ void BluetoothConnection::send_service_for_discovery_() { service_resp.handle = service_result.start_handle; if (total_char_count > 0) { - // Reserve space and process characteristics - service_resp.characteristics.reserve(total_char_count); + // Initialize FixedVector with exact count and process characteristics + service_resp.characteristics.init(total_char_count); uint16_t char_offset = 0; esp_gattc_char_elem_t char_result; while (true) { // characteristics @@ -275,8 +275,8 @@ void BluetoothConnection::send_service_for_discovery_() { continue; } - // Reserve space and process descriptors - characteristic_resp.descriptors.reserve(total_desc_count); + // Initialize FixedVector with exact count and process descriptors + characteristic_resp.descriptors.init(total_desc_count); uint16_t desc_offset = 0; esp_gattc_descr_elem_t desc_result; while (true) { // descriptors diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b5a0a1c8ac..2bdfcb4e2a 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -181,6 +181,31 @@ template class FixedVector { FixedVector(const FixedVector &) = delete; FixedVector &operator=(const FixedVector &) = delete; + // Enable move semantics for use in containers + FixedVector(FixedVector &&other) noexcept : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { + other.data_ = nullptr; + other.size_ = 0; + other.capacity_ = 0; + } + + FixedVector &operator=(FixedVector &&other) noexcept { + if (this != &other) { + // Delete our current data + if (data_ != nullptr) { + delete[] data_; + } + // Take ownership of other's data + data_ = other.data_; + size_ = other.size_; + capacity_ = other.capacity_; + // Leave other in valid empty state + other.data_ = nullptr; + other.size_ = 0; + other.capacity_ = 0; + } + return *this; + } + // Allocate capacity - can only be called once on empty vector void init(size_t n) { if (data_ == nullptr && n > 0) { @@ -199,12 +224,33 @@ template class FixedVector { } } + /// Construct element in place and return reference + /// Caller must ensure sufficient capacity was allocated via init() + T &emplace_back() { + if (size_ < capacity_) { + return data_[size_++]; + } + // Should never happen with proper init() - return last element to avoid crash + return data_[capacity_ - 1]; + } + + /// Access last element + T &back() { return data_[size_ - 1]; } + const T &back() const { return data_[size_ - 1]; } + size_t size() const { return size_; } + bool empty() const { return size_ == 0; } /// Access element without bounds checking (matches std::vector behavior) /// Caller must ensure index is valid (i < size()) T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } + + /// Iterators for range-based for loops + T *begin() { return data_; } + T *end() { return data_ + size_; } + const T *begin() const { return data_; } + const T *end() const { return data_ + size_; } }; ///@} diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 487c187372..9a55f1d136 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1415,6 +1415,8 @@ class RepeatedTypeInfo(TypeInfo): # Check if this is a pointer field by looking for container_pointer option self._container_type = get_field_opt(field, pb.container_pointer, "") self._use_pointer = bool(self._container_type) + # Check if this should use FixedVector instead of std::vector + self._use_fixed_vector = get_field_opt(field, pb.fixed_vector, False) # For repeated fields, we need to get the base type info # but we can't call create_field_type_info as it would cause recursion @@ -1438,6 +1440,8 @@ class RepeatedTypeInfo(TypeInfo): if "<" in self._container_type and ">" in self._container_type: return f"const {self._container_type}*" return f"const {self._container_type}<{self._ti.cpp_type}>*" + if self._use_fixed_vector: + return f"FixedVector<{self._ti.cpp_type}>" return f"std::vector<{self._ti.cpp_type}>" @property