From a9fd0a3b26eed1059aa82390b0addbc00e1d916d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 18:21:14 -1000 Subject: [PATCH 01/16] 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 From 347501d895cb42d1a15633d4b2fd684ae6a2af6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 19:39:55 -1000 Subject: [PATCH 02/16] wifi fixed vector --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 4 +- .../wifi/wifi_component_esp8266.cpp | 8 ++++ .../wifi/wifi_component_esp_idf.cpp | 5 +- .../wifi/wifi_component_libretiny.cpp | 2 +- esphome/core/helpers.h | 46 +++++++++++++++---- 6 files changed, 51 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2e083d4c68..1bb2674ad7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -552,7 +552,7 @@ void WiFiComponent::start_scanning() { // Using insertion sort instead of std::stable_sort saves flash memory // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements) -static void insertion_sort_scan_results(std::vector &results) { +static void insertion_sort_scan_results(FixedVector &results) { const size_t size = results.size(); for (size_t i = 1; i < size; i++) { // Make a copy to avoid issues with move semantics during comparison diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ee62ec1a69..e1c3d6df88 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -278,7 +278,7 @@ class WiFiComponent : public Component { std::string get_use_address() const; void set_use_address(const std::string &use_address); - const std::vector &get_scan_result() const { return scan_result_; } + const FixedVector &get_scan_result() const { return scan_result_; } network::IPAddress wifi_soft_ap_ip(); @@ -385,7 +385,7 @@ class WiFiComponent : public Component { std::string use_address_; std::vector sta_; std::vector sta_priorities_; - std::vector scan_result_; + FixedVector scan_result_; WiFiAP selected_ap_; WiFiAP ap_; optional output_power_; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 3b3b4b139c..59909b2cb5 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -696,7 +696,15 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { this->retry_connect(); return; } + + // Count the number of results first auto *head = reinterpret_cast(arg); + size_t count = 0; + for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { + count++; + } + + this->scan_result_.init(count); for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, std::string(reinterpret_cast(it->ssid), it->ssid_len), it->channel, it->rssi, diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index ccec800205..4c719ef4c3 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -763,8 +763,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { const auto &it = data->data.sta_scan_done; ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); - scan_result_.clear(); this->scan_done_ = true; + scan_result_.clear(); + if (it.status != 0) { // scan error return; @@ -784,7 +785,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } records.resize(number); - scan_result_.reserve(number); + scan_result_.init(number); for (int i = 0; i < number; i++) { auto &record = records[i]; bssid_t bssid; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index b15f710150..cb179d9022 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() { if (num < 0) return; - this->scan_result_.reserve(static_cast(num)); + this->scan_result_.init(static_cast(num)); for (int i = 0; i < num; i++) { String ssid = WiFi.SSID(i); wifi_auth_mode_t authmode = WiFi.encryptionType(i); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b5a0a1c8ac..12e921e3be 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -168,34 +168,54 @@ template class FixedVector { size_t size_{0}; size_t capacity_{0}; + // Helper to destroy elements and free memory + void cleanup_() { + if (data_ != nullptr) { + // Manually destroy all elements + for (size_t i = 0; i < size_; i++) { + data_[i].~T(); + } + // Free raw memory + ::operator delete(data_); + } + } + public: FixedVector() = default; - ~FixedVector() { - if (data_ != nullptr) { - delete[] data_; - } - } + ~FixedVector() { cleanup_(); } // Disable copy to avoid accidental copies FixedVector(const FixedVector &) = delete; FixedVector &operator=(const FixedVector &) = delete; - // Allocate capacity - can only be called once on empty vector + // Allocate capacity - can be called multiple times to reinit void init(size_t n) { - if (data_ == nullptr && n > 0) { - data_ = new T[n]; + cleanup_(); + data_ = nullptr; + capacity_ = 0; + size_ = 0; + if (n > 0) { + // Allocate raw memory without calling constructors + data_ = static_cast(::operator new(n * sizeof(T))); capacity_ = n; - size_ = 0; } } + // Clear the vector (reset size to 0, keep capacity) + void clear() { size_ = 0; } + + // Check if vector is empty + bool empty() const { return size_ == 0; } + /// Add element without bounds checking /// Caller must ensure sufficient capacity was allocated via init() /// Silently ignores pushes beyond capacity (no exception or assertion) void push_back(const T &value) { if (size_ < capacity_) { - data_[size_++] = value; + // Use placement new to construct the object in pre-allocated memory + new (&data_[size_]) T(value); + size_++; } } @@ -205,6 +225,12 @@ template class FixedVector { /// 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]; } + + // Iterator support 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_; } }; ///@} From c652aa375a402a9888821955699bf1c93fb9de1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:10:46 -1000 Subject: [PATCH 03/16] Bump pylint from 3.3.9 to 4.0.0 (#11211) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 51b8e6f8ed..c4227fdbde 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==3.3.9 +pylint==4.0.0 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.14.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating From fe07c342467b4d42388ff2eeafff338d5e03f20b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:00:45 +0000 Subject: [PATCH 04/16] Bump aioesphomeapi from 41.14.0 to 41.16.0 (#11215) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9937709c1c..1142c587b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==41.14.0 +aioesphomeapi==41.16.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From 22370c0ad178ad9931cf4d54197a9edf52f04759 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:03:08 -1000 Subject: [PATCH 05/16] merge --- esphome/core/helpers.h | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 80ce21af57..084ad28882 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -168,14 +168,22 @@ template class FixedVector { size_t size_{0}; size_t capacity_{0}; + // Helper to destroy elements and free memory + void cleanup_() { + if (data_ != nullptr) { + // Manually destroy all elements + for (size_t i = 0; i < size_; i++) { + data_[i].~T(); + } + // Free raw memory + ::operator delete(data_); + } + } + public: FixedVector() = default; - ~FixedVector() { - if (data_ != nullptr) { - delete[] data_; - } - } + ~FixedVector() { cleanup_(); } // Enable move semantics for use in containers FixedVector(FixedVector &&other) noexcept : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { @@ -202,21 +210,33 @@ template class FixedVector { return *this; } - // Allocate capacity - can only be called once on empty vector + // Allocate capacity - can be called multiple times to reinit void init(size_t n) { - if (data_ == nullptr && n > 0) { - data_ = new T[n]; + cleanup_(); + data_ = nullptr; + capacity_ = 0; + size_ = 0; + if (n > 0) { + // Allocate raw memory without calling constructors + data_ = static_cast(::operator new(n * sizeof(T))); capacity_ = n; - size_ = 0; } } + // Clear the vector (reset size to 0, keep capacity) + void clear() { size_ = 0; } + + // Check if vector is empty + bool empty() const { return size_ == 0; } + /// Add element without bounds checking /// Caller must ensure sufficient capacity was allocated via init() /// Silently ignores pushes beyond capacity (no exception or assertion) void push_back(const T &value) { if (size_ < capacity_) { - data_[size_++] = value; + // Use placement new to construct the object in pre-allocated memory + new (&data_[size_]) T(value); + size_++; } } @@ -242,7 +262,7 @@ template class FixedVector { T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } - /// Iterators for range-based for loops + // Iterator support for range-based for loops T *begin() { return data_; } T *end() { return data_ + size_; } const T *begin() const { return data_; } From fbef9b126403ca49999b589ba5670fbe980b7c9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:03:59 -1000 Subject: [PATCH 06/16] revert --- .../components/bluetooth_proxy/bluetooth_connection.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 6f172b0bcf..cde82fbfb0 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) { - // Initialize FixedVector with exact count and process characteristics - service_resp.characteristics.init(total_char_count); + // 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 @@ -275,8 +275,8 @@ void BluetoothConnection::send_service_for_discovery_() { continue; } - // Initialize FixedVector with exact count and process descriptors - characteristic_resp.descriptors.init(total_desc_count); + // 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 From ddf6e0a7b61ac09db7bfdafc17362cf4804b189f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:04:15 -1000 Subject: [PATCH 07/16] revert --- 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 +++++------------------- 4 files changed, 9 insertions(+), 31 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 9b714d00f1..87f477799d 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 [(fixed_vector) = true]; + repeated BluetoothGATTDescriptor descriptors = 4; // 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 [(fixed_vector) = true]; + repeated BluetoothGATTCharacteristic characteristics = 3; // 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 ead8ac0bbc..633f39b552 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -64,10 +64,4 @@ 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 1798458393..d9e68ece9b 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}; - FixedVector descriptors{}; + std::vector 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}; - FixedVector characteristics{}; + std::vector 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 a6a09bf7c5..9d780692ec 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -749,29 +749,13 @@ 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()) { - // Use the force version for all messages in the repeated field - for (const auto &message : messages) { - add_message_object_force(field_id_size, message); - } + if (messages.empty()) { + return; } - } - /** - * @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); - } + // Use the force version for all messages in the repeated field + for (const auto &message : messages) { + add_message_object_force(field_id_size, message); } } }; From d5234e335709e63eb022240c98c9c885994b247f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:04:39 -1000 Subject: [PATCH 08/16] merge --- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4c719ef4c3..e45b873e8d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -763,8 +763,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { const auto &it = data->data.sta_scan_done; ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); - this->scan_done_ = true; scan_result_.clear(); + this->scan_done_ = true; if (it.status != 0) { // scan error From ce46f1630859861534f91ed15e03a3ae5c335855 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:05:19 -1000 Subject: [PATCH 09/16] merge --- esphome/components/wifi/wifi_component_esp_idf.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index e45b873e8d..951f5803a6 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -765,7 +765,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { scan_result_.clear(); this->scan_done_ = true; - if (it.status != 0) { // scan error return; From 7792a115c26d93b7d18cb112008457a30dfad2ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:05:31 -1000 Subject: [PATCH 10/16] merge --- script/api_protobuf/api_protobuf.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 9a55f1d136..487c187372 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1415,8 +1415,6 @@ 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 @@ -1440,8 +1438,6 @@ 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 From bb2f568f3d4971b536a6c5a59bb25bb59fc45365 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:07:52 -1000 Subject: [PATCH 11/16] merge --- esphome/core/helpers.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 084ad28882..6b04af3c7c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -226,9 +226,6 @@ template class FixedVector { // Clear the vector (reset size to 0, keep capacity) void clear() { size_ = 0; } - // Check if vector is empty - bool empty() const { return size_ == 0; } - /// Add element without bounds checking /// Caller must ensure sufficient capacity was allocated via init() /// Silently ignores pushes beyond capacity (no exception or assertion) From c9a1664398e8a4a9c8f9a3c38d5e67dadf5c6303 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:08:27 -1000 Subject: [PATCH 12/16] merge --- esphome/core/helpers.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 084ad28882..6b04af3c7c 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -226,9 +226,6 @@ template class FixedVector { // Clear the vector (reset size to 0, keep capacity) void clear() { size_ = 0; } - // Check if vector is empty - bool empty() const { return size_ == 0; } - /// Add element without bounds checking /// Caller must ensure sufficient capacity was allocated via init() /// Silently ignores pushes beyond capacity (no exception or assertion) From b878aa0270c2faaedee25342d1a15b69c43dbad6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:09:44 -1000 Subject: [PATCH 13/16] fix --- esphome/core/helpers.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6b04af3c7c..a3c0447a8d 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -226,6 +226,14 @@ template class FixedVector { // Clear the vector (reset size to 0, keep capacity) void clear() { size_ = 0; } + // Shrink capacity to fit current size (frees all memory) + void shrink_to_fit() { + cleanup_(); + data_ = nullptr; + capacity_ = 0; + size_ = 0; + } + /// Add element without bounds checking /// Caller must ensure sufficient capacity was allocated via init() /// Silently ignores pushes beyond capacity (no exception or assertion) From de10d781259fb73b605fed83d5c14812997234b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:10:41 -1000 Subject: [PATCH 14/16] dry --- esphome/core/helpers.h | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a3c0447a8d..e838c82f3e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -180,6 +180,13 @@ template class FixedVector { } } + // Helper to reset pointers after cleanup + void reset_() { + data_ = nullptr; + capacity_ = 0; + size_ = 0; + } + public: FixedVector() = default; @@ -187,25 +194,19 @@ template class FixedVector { // 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; + other.reset_(); } FixedVector &operator=(FixedVector &&other) noexcept { if (this != &other) { // Delete our current data - if (data_ != nullptr) { - delete[] data_; - } + cleanup_(); // 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; + other.reset_(); } return *this; } @@ -213,9 +214,7 @@ template class FixedVector { // Allocate capacity - can be called multiple times to reinit void init(size_t n) { cleanup_(); - data_ = nullptr; - capacity_ = 0; - size_ = 0; + reset_(); if (n > 0) { // Allocate raw memory without calling constructors data_ = static_cast(::operator new(n * sizeof(T))); @@ -229,9 +228,7 @@ template class FixedVector { // Shrink capacity to fit current size (frees all memory) void shrink_to_fit() { cleanup_(); - data_ = nullptr; - capacity_ = 0; - size_ = 0; + reset_(); } /// Add element without bounds checking From 453ab0adb8ec9f6da7af1d84590e1a2d309711e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:10:56 -1000 Subject: [PATCH 15/16] backmerge --- esphome/core/helpers.h | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 6b04af3c7c..e838c82f3e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -180,6 +180,13 @@ template class FixedVector { } } + // Helper to reset pointers after cleanup + void reset_() { + data_ = nullptr; + capacity_ = 0; + size_ = 0; + } + public: FixedVector() = default; @@ -187,25 +194,19 @@ template class FixedVector { // 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; + other.reset_(); } FixedVector &operator=(FixedVector &&other) noexcept { if (this != &other) { // Delete our current data - if (data_ != nullptr) { - delete[] data_; - } + cleanup_(); // 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; + other.reset_(); } return *this; } @@ -213,9 +214,7 @@ template class FixedVector { // Allocate capacity - can be called multiple times to reinit void init(size_t n) { cleanup_(); - data_ = nullptr; - capacity_ = 0; - size_ = 0; + reset_(); if (n > 0) { // Allocate raw memory without calling constructors data_ = static_cast(::operator new(n * sizeof(T))); @@ -226,6 +225,12 @@ template class FixedVector { // Clear the vector (reset size to 0, keep capacity) void clear() { size_ = 0; } + // Shrink capacity to fit current size (frees all memory) + void shrink_to_fit() { + cleanup_(); + reset_(); + } + /// Add element without bounds checking /// Caller must ensure sufficient capacity was allocated via init() /// Silently ignores pushes beyond capacity (no exception or assertion) From 7b5a86e4df1a90f2521b283d7ec8a17c5c5e2359 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Oct 2025 14:15:37 -1000 Subject: [PATCH 16/16] fixes --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 3cdc034be6..8308421220 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -554,7 +554,7 @@ void WiFiComponent::start_scanning() { // Using insertion sort instead of std::stable_sort saves flash memory // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements) -static void insertion_sort_scan_results(FixedVector &results) { +template static void insertion_sort_scan_results(VectorType &results) { const size_t size = results.size(); for (size_t i = 1; i < size; i++) { // Make a copy to avoid issues with move semantics during comparison diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index e1c3d6df88..bbb4649027 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -121,6 +121,14 @@ struct EAPAuth { using bssid_t = std::array; +// Use std::vector for RP2040 since scan count is unknown (callback-based) +// Use FixedVector for other platforms where count is queried first +#ifdef USE_RP2040 +template using wifi_scan_vector_t = std::vector; +#else +template using wifi_scan_vector_t = FixedVector; +#endif + class WiFiAP { public: void set_ssid(const std::string &ssid); @@ -278,7 +286,7 @@ class WiFiComponent : public Component { std::string get_use_address() const; void set_use_address(const std::string &use_address); - const FixedVector &get_scan_result() const { return scan_result_; } + const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } network::IPAddress wifi_soft_ap_ip(); @@ -385,7 +393,7 @@ class WiFiComponent : public Component { std::string use_address_; std::vector sta_; std::vector sta_priorities_; - FixedVector scan_result_; + wifi_scan_vector_t scan_result_; WiFiAP selected_ap_; WiFiAP ap_; optional output_power_;