From 00d9baed11fa3c8a7330f88c963cd7d87a624817 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Aug 2025 20:26:00 -1000 Subject: [PATCH 1/3] [bluetooth_proxy] Eliminate heap allocations in connection state reporting (#10010) --- esphome/components/api/api.proto | 5 ++- esphome/components/api/api_connection.cpp | 6 +-- esphome/components/api/api_options.proto | 1 + esphome/components/api/api_pb2.cpp | 10 +++-- esphome/components/api/api_pb2.h | 4 +- .../components/bluetooth_proxy/__init__.py | 4 ++ .../bluetooth_proxy/bluetooth_connection.cpp | 24 +++++++++++ .../bluetooth_proxy/bluetooth_connection.h | 3 ++ .../bluetooth_proxy/bluetooth_proxy.cpp | 33 ++++---------- .../bluetooth_proxy/bluetooth_proxy.h | 8 ++-- .../esp32_ble_client/ble_client_base.h | 2 +- esphome/core/defines.h | 1 + script/api_protobuf/api_protobuf.py | 43 ++++++++++++++++++- 13 files changed, 104 insertions(+), 40 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4aa5cc4be0..e0b2c19a21 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1621,7 +1621,10 @@ message BluetoothConnectionsFreeResponse { uint32 free = 1; uint32 limit = 2; - repeated uint64 allocated = 3; + repeated uint64 allocated = 3 [ + (fixed_array_size_define) = "BLUETOOTH_PROXY_MAX_CONNECTIONS", + (fixed_array_skip_zero) = true + ]; } message BluetoothGATTErrorResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8ac6c3b71e..5fff270c99 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1105,10 +1105,8 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) bool APIConnection::send_subscribe_bluetooth_connections_free_response( const SubscribeBluetoothConnectionsFreeRequest &msg) { - BluetoothConnectionsFreeResponse resp; - resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free(); - resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit(); - return this->send_message(resp, BluetoothConnectionsFreeResponse::MESSAGE_TYPE); + bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this); + return true; } void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) { diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index d4b5700024..ed0e0d7455 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -29,6 +29,7 @@ extend google.protobuf.FieldOptions { optional uint32 fixed_array_size = 50007; optional bool no_zero_copy = 50008 [default=false]; optional bool fixed_array_skip_zero = 50009 [default=false]; + optional string fixed_array_size_define = 50010; // 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 29d0f2842c..8c14153155 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2073,15 +2073,17 @@ void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const { void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->free); buffer.encode_uint32(2, this->limit); - for (auto &it : this->allocated) { - buffer.encode_uint64(3, it, true); + for (const auto &it : this->allocated) { + if (it != 0) { + buffer.encode_uint64(3, it, true); + } } } void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->free); size.add_uint32(1, this->limit); - if (!this->allocated.empty()) { - for (const auto &it : this->allocated) { + for (const auto &it : this->allocated) { + if (it != 0) { size.add_uint64_force(1, it); } } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 524674e6ef..0bc75ef00b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2076,13 +2076,13 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { class BluetoothConnectionsFreeResponse : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 81; - static constexpr uint8_t ESTIMATED_SIZE = 16; + static constexpr uint8_t ESTIMATED_SIZE = 20; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_connections_free_response"; } #endif uint32_t free{0}; uint32_t limit{0}; - std::vector allocated{}; + std::array allocated{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index a1e9d464df..ec1df6a06c 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -87,6 +87,10 @@ async def to_code(config): cg.add(var.set_active(config[CONF_ACTIVE])) await esp32_ble_tracker.register_raw_ble_device(var, config) + # Define max connections for protobuf fixed array + connection_count = len(config.get(CONF_CONNECTIONS, [])) + cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count) + for connection_conf in config.get(CONF_CONNECTIONS, []): connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) await cg.register_component(connection_var, connection_conf) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index fd1324dcdc..01c2aa3d22 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -78,6 +78,30 @@ void BluetoothConnection::dump_config() { BLEClientBase::dump_config(); } +void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) { + auto &allocated = this->proxy_->connections_free_response_.allocated; + auto *it = std::find(allocated.begin(), allocated.end(), find_value); + if (it != allocated.end()) { + *it = set_value; + } +} + +void BluetoothConnection::set_address(uint64_t address) { + // If we're clearing an address (disconnecting), update the pre-allocated message + if (address == 0 && this->address_ != 0) { + this->proxy_->connections_free_response_.free++; + this->update_allocated_slot_(this->address_, 0); + } + // If we're setting a new address (connecting), update the pre-allocated message + else if (address != 0 && this->address_ == 0) { + this->proxy_->connections_free_response_.free--; + this->update_allocated_slot_(0, address); + } + + // Call parent implementation to actually set the address + BLEClientBase::set_address(address); +} + void BluetoothConnection::loop() { BLEClientBase::loop(); diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 622d257bf8..042868e7a4 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -24,12 +24,15 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { esp_err_t notify_characteristic(uint16_t handle, bool enable); + void set_address(uint64_t address) override; + protected: friend class BluetoothProxy; bool supports_efficient_uuids_() const; void send_service_for_discovery_(); void reset_connection_(esp_err_t reason); + void update_allocated_slot_(uint64_t find_value, uint64_t set_value); // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index de5508c777..a59a33117a 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -35,6 +35,9 @@ void BluetoothProxy::setup() { // Don't pre-allocate pool - let it grow only if needed in busy environments // Many devices in quiet areas will never need the overflow pool + this->connections_free_response_.limit = this->connections_.size(); + this->connections_free_response_.free = this->connections_.size(); + this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { if (this->api_connection_ != nullptr) { this->send_bluetooth_scanner_state_(state); @@ -134,20 +137,6 @@ void BluetoothProxy::dump_config() { YESNO(this->active_), this->connections_.size()); } -int BluetoothProxy::get_bluetooth_connections_free() { - int free = 0; - for (auto *connection : this->connections_) { - if (connection->address_ == 0) { - free++; - ESP_LOGV(TAG, "[%d] Free connection", connection->get_connection_index()); - } else { - ESP_LOGV(TAG, "[%d] Used connection by [%s]", connection->get_connection_index(), - connection->address_str().c_str()); - } - } - return free; -} - void BluetoothProxy::loop() { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { for (auto *connection : this->connections_) { @@ -439,17 +428,13 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); } void BluetoothProxy::send_connections_free() { - if (this->api_connection_ == nullptr) - return; - api::BluetoothConnectionsFreeResponse call; - call.free = this->get_bluetooth_connections_free(); - call.limit = this->get_bluetooth_connections_limit(); - for (auto *connection : this->connections_) { - if (connection->address_ != 0) { - call.allocated.push_back(connection->address_); - } + if (this->api_connection_ != nullptr) { + this->send_connections_free(this->api_connection_); } - this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); +} + +void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) { + api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); } void BluetoothProxy::send_gatt_services_done(uint64_t address) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index d249515fdf..70deef1ebd 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -49,6 +49,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t { }; class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { + friend class BluetoothConnection; // Allow connection to update connections_free_response_ public: BluetoothProxy(); #ifdef USE_ESP32_BLE_DEVICE @@ -74,15 +75,13 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); - int get_bluetooth_connections_free(); - int get_bluetooth_connections_limit() { return this->connections_.size(); } - void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); void unsubscribe_api_connection(api::APIConnection *api_connection); api::APIConnection *get_api_connection() { return this->api_connection_; } void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); void send_connections_free(); + void send_connections_free(api::APIConnection *api_connection); void send_gatt_services_done(uint64_t address); void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); @@ -149,6 +148,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com // Group 3: 4-byte types uint32_t last_advertisement_flush_time_{0}; + // Pre-allocated response message - always ready to send + api::BluetoothConnectionsFreeResponse connections_free_response_; + // Group 4: 1-byte types grouped together bool active_; uint8_t advertisement_count_{0}; diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 457a88ec1d..0a2fda4476 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -48,7 +48,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; } - void set_address(uint64_t address) { + virtual void set_address(uint64_t address) { this->address_ = address; this->remote_bda_[0] = (address >> 40) & 0xFF; this->remote_bda_[1] = (address >> 32) & 0xFF; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index e226f748a8..55652e443e 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -147,6 +147,7 @@ #define USE_ESPHOME_TASK_LOG_BUFFER #define USE_BLUETOOTH_PROXY +#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 #define USE_CAPTIVE_PORTAL #define USE_ESP32_BLE #define USE_ESP32_BLE_CLIENT diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 24e2b25e90..fa2f87d98d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -342,6 +342,11 @@ def create_field_type_info( # 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) + # Check if this repeated field has fixed_array_size_define option + if ( + size_define := get_field_opt(field, pb.fixed_array_size_define) + ) is not None: + return FixedArrayRepeatedType(field, size_define) return RepeatedTypeInfo(field) # Check for fixed_array_size option on bytes fields @@ -1066,9 +1071,10 @@ class FixedArrayRepeatedType(TypeInfo): control how many items we receive when decoding. """ - def __init__(self, field: descriptor.FieldDescriptorProto, size: int) -> None: + def __init__(self, field: descriptor.FieldDescriptorProto, size: int | str) -> None: super().__init__(field) self.array_size = size + self.is_define = isinstance(size, str) # 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( @@ -1113,6 +1119,14 @@ class FixedArrayRepeatedType(TypeInfo): # If skip_zero is enabled, wrap encoding in a zero check if self.skip_zero: + if self.is_define: + # When using a define, we need to use a loop-based approach + o = f"for (const auto &it : this->{self.field_name}) {{\n" + o += " if (it != 0) {\n" + o += f" {encode_element('it')}\n" + o += " }\n" + o += "}" + return o # 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)] @@ -1123,6 +1137,13 @@ class FixedArrayRepeatedType(TypeInfo): ] return f"if ({non_zero_checks}) {{\n" + "\n".join(encode_lines) + "\n}" + # When using a define, always use loop-based approach + if self.is_define: + o = f"for (const auto &it : this->{self.field_name}) {{\n" + o += f" {encode_element('it')}\n" + o += "}" + return o + # Unroll small arrays for efficiency if self.array_size == 1: return encode_element(f"this->{self.field_name}[0]") @@ -1153,6 +1174,14 @@ class FixedArrayRepeatedType(TypeInfo): 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: + if self.is_define: + # When using a define, we need to use a loop-based approach + o = f"for (const auto &it : {name}) {{\n" + o += " if (it != 0) {\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" + o += " }\n" + o += "}" + return o # 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)] @@ -1163,6 +1192,13 @@ class FixedArrayRepeatedType(TypeInfo): ] return f"if ({non_zero_checks}) {{\n" + "\n".join(size_lines) + "\n}" + # When using a define, always use loop-based approach + if self.is_define: + o = f"for (const auto &it : {name}) {{\n" + o += f" {self._ti.get_size_calculation('it', True)}\n" + o += "}" + return o + # For fixed arrays, we always encode all elements # Special case for single-element arrays - no loop needed @@ -1186,6 +1222,11 @@ class FixedArrayRepeatedType(TypeInfo): def get_estimated_size(self) -> int: # For fixed arrays, estimate underlying type size * array size underlying_size = self._ti.get_estimated_size() + if self.is_define: + # When using a define, we don't know the actual size so just guess 3 + # This is only used for documentation and never actually used since + # fixed arrays are only for SOURCE_SERVER (encode-only) messages + return underlying_size * 3 return underlying_size * self.array_size From 4f58e1c8b92f233af368f7d1698bcead16f9c8e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Aug 2025 20:26:22 -1000 Subject: [PATCH 2/3] [core] Convert entity vectors to static allocation for reduced memory usage (#10018) --- esphome/core/application.h | 150 ++++++++-------------------- esphome/core/component_iterator.cpp | 13 --- esphome/core/component_iterator.h | 16 ++- esphome/core/config.py | 4 +- esphome/core/defines.h | 23 +++++ esphome/core/helpers.h | 36 +++++++ 6 files changed, 118 insertions(+), 124 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index a83789837f..b7824a254b 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -216,69 +216,6 @@ class Application { /// Reserve space for components to avoid memory fragmentation void reserve_components(size_t count) { this->components_.reserve(count); } -#ifdef USE_BINARY_SENSOR - void reserve_binary_sensor(size_t count) { this->binary_sensors_.reserve(count); } -#endif -#ifdef USE_SWITCH - void reserve_switch(size_t count) { this->switches_.reserve(count); } -#endif -#ifdef USE_BUTTON - void reserve_button(size_t count) { this->buttons_.reserve(count); } -#endif -#ifdef USE_SENSOR - void reserve_sensor(size_t count) { this->sensors_.reserve(count); } -#endif -#ifdef USE_TEXT_SENSOR - void reserve_text_sensor(size_t count) { this->text_sensors_.reserve(count); } -#endif -#ifdef USE_FAN - void reserve_fan(size_t count) { this->fans_.reserve(count); } -#endif -#ifdef USE_COVER - void reserve_cover(size_t count) { this->covers_.reserve(count); } -#endif -#ifdef USE_CLIMATE - void reserve_climate(size_t count) { this->climates_.reserve(count); } -#endif -#ifdef USE_LIGHT - void reserve_light(size_t count) { this->lights_.reserve(count); } -#endif -#ifdef USE_NUMBER - void reserve_number(size_t count) { this->numbers_.reserve(count); } -#endif -#ifdef USE_DATETIME_DATE - void reserve_date(size_t count) { this->dates_.reserve(count); } -#endif -#ifdef USE_DATETIME_TIME - void reserve_time(size_t count) { this->times_.reserve(count); } -#endif -#ifdef USE_DATETIME_DATETIME - void reserve_datetime(size_t count) { this->datetimes_.reserve(count); } -#endif -#ifdef USE_SELECT - void reserve_select(size_t count) { this->selects_.reserve(count); } -#endif -#ifdef USE_TEXT - void reserve_text(size_t count) { this->texts_.reserve(count); } -#endif -#ifdef USE_LOCK - void reserve_lock(size_t count) { this->locks_.reserve(count); } -#endif -#ifdef USE_VALVE - void reserve_valve(size_t count) { this->valves_.reserve(count); } -#endif -#ifdef USE_MEDIA_PLAYER - void reserve_media_player(size_t count) { this->media_players_.reserve(count); } -#endif -#ifdef USE_ALARM_CONTROL_PANEL - void reserve_alarm_control_panel(size_t count) { this->alarm_control_panels_.reserve(count); } -#endif -#ifdef USE_EVENT - void reserve_event(size_t count) { this->events_.reserve(count); } -#endif -#ifdef USE_UPDATE - void reserve_update(size_t count) { this->updates_.reserve(count); } -#endif #ifdef USE_AREAS void reserve_area(size_t count) { this->areas_.reserve(count); } #endif @@ -394,92 +331,90 @@ class Application { const std::vector &get_areas() { return this->areas_; } #endif #ifdef USE_BINARY_SENSOR - const std::vector &get_binary_sensors() { return this->binary_sensors_; } + auto &get_binary_sensors() const { return this->binary_sensors_; } GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors) #endif #ifdef USE_SWITCH - const std::vector &get_switches() { return this->switches_; } + auto &get_switches() const { return this->switches_; } GET_ENTITY_METHOD(switch_::Switch, switch, switches) #endif #ifdef USE_BUTTON - const std::vector &get_buttons() { return this->buttons_; } + auto &get_buttons() const { return this->buttons_; } GET_ENTITY_METHOD(button::Button, button, buttons) #endif #ifdef USE_SENSOR - const std::vector &get_sensors() { return this->sensors_; } + auto &get_sensors() const { return this->sensors_; } GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors) #endif #ifdef USE_TEXT_SENSOR - const std::vector &get_text_sensors() { return this->text_sensors_; } + auto &get_text_sensors() const { return this->text_sensors_; } GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors) #endif #ifdef USE_FAN - const std::vector &get_fans() { return this->fans_; } + auto &get_fans() const { return this->fans_; } GET_ENTITY_METHOD(fan::Fan, fan, fans) #endif #ifdef USE_COVER - const std::vector &get_covers() { return this->covers_; } + auto &get_covers() const { return this->covers_; } GET_ENTITY_METHOD(cover::Cover, cover, covers) #endif #ifdef USE_LIGHT - const std::vector &get_lights() { return this->lights_; } + auto &get_lights() const { return this->lights_; } GET_ENTITY_METHOD(light::LightState, light, lights) #endif #ifdef USE_CLIMATE - const std::vector &get_climates() { return this->climates_; } + auto &get_climates() const { return this->climates_; } GET_ENTITY_METHOD(climate::Climate, climate, climates) #endif #ifdef USE_NUMBER - const std::vector &get_numbers() { return this->numbers_; } + auto &get_numbers() const { return this->numbers_; } GET_ENTITY_METHOD(number::Number, number, numbers) #endif #ifdef USE_DATETIME_DATE - const std::vector &get_dates() { return this->dates_; } + auto &get_dates() const { return this->dates_; } GET_ENTITY_METHOD(datetime::DateEntity, date, dates) #endif #ifdef USE_DATETIME_TIME - const std::vector &get_times() { return this->times_; } + auto &get_times() const { return this->times_; } GET_ENTITY_METHOD(datetime::TimeEntity, time, times) #endif #ifdef USE_DATETIME_DATETIME - const std::vector &get_datetimes() { return this->datetimes_; } + auto &get_datetimes() const { return this->datetimes_; } GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes) #endif #ifdef USE_TEXT - const std::vector &get_texts() { return this->texts_; } + auto &get_texts() const { return this->texts_; } GET_ENTITY_METHOD(text::Text, text, texts) #endif #ifdef USE_SELECT - const std::vector &get_selects() { return this->selects_; } + auto &get_selects() const { return this->selects_; } GET_ENTITY_METHOD(select::Select, select, selects) #endif #ifdef USE_LOCK - const std::vector &get_locks() { return this->locks_; } + auto &get_locks() const { return this->locks_; } GET_ENTITY_METHOD(lock::Lock, lock, locks) #endif #ifdef USE_VALVE - const std::vector &get_valves() { return this->valves_; } + auto &get_valves() const { return this->valves_; } GET_ENTITY_METHOD(valve::Valve, valve, valves) #endif #ifdef USE_MEDIA_PLAYER - const std::vector &get_media_players() { return this->media_players_; } + auto &get_media_players() const { return this->media_players_; } GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players) #endif #ifdef USE_ALARM_CONTROL_PANEL - const std::vector &get_alarm_control_panels() { - return this->alarm_control_panels_; - } + auto &get_alarm_control_panels() const { return this->alarm_control_panels_; } GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels) #endif #ifdef USE_EVENT - const std::vector &get_events() { return this->events_; } + auto &get_events() const { return this->events_; } GET_ENTITY_METHOD(event::Event, event, events) #endif #ifdef USE_UPDATE - const std::vector &get_updates() { return this->updates_; } + auto &get_updates() const { return this->updates_; } GET_ENTITY_METHOD(update::UpdateEntity, update, updates) #endif @@ -558,67 +493,68 @@ class Application { std::vector areas_{}; #endif #ifdef USE_BINARY_SENSOR - std::vector binary_sensors_{}; + StaticVector binary_sensors_{}; #endif #ifdef USE_SWITCH - std::vector switches_{}; + StaticVector switches_{}; #endif #ifdef USE_BUTTON - std::vector buttons_{}; + StaticVector buttons_{}; #endif #ifdef USE_EVENT - std::vector events_{}; + StaticVector events_{}; #endif #ifdef USE_SENSOR - std::vector sensors_{}; + StaticVector sensors_{}; #endif #ifdef USE_TEXT_SENSOR - std::vector text_sensors_{}; + StaticVector text_sensors_{}; #endif #ifdef USE_FAN - std::vector fans_{}; + StaticVector fans_{}; #endif #ifdef USE_COVER - std::vector covers_{}; + StaticVector covers_{}; #endif #ifdef USE_CLIMATE - std::vector climates_{}; + StaticVector climates_{}; #endif #ifdef USE_LIGHT - std::vector lights_{}; + StaticVector lights_{}; #endif #ifdef USE_NUMBER - std::vector numbers_{}; + StaticVector numbers_{}; #endif #ifdef USE_DATETIME_DATE - std::vector dates_{}; + StaticVector dates_{}; #endif #ifdef USE_DATETIME_TIME - std::vector times_{}; + StaticVector times_{}; #endif #ifdef USE_DATETIME_DATETIME - std::vector datetimes_{}; + StaticVector datetimes_{}; #endif #ifdef USE_SELECT - std::vector selects_{}; + StaticVector selects_{}; #endif #ifdef USE_TEXT - std::vector texts_{}; + StaticVector texts_{}; #endif #ifdef USE_LOCK - std::vector locks_{}; + StaticVector locks_{}; #endif #ifdef USE_VALVE - std::vector valves_{}; + StaticVector valves_{}; #endif #ifdef USE_MEDIA_PLAYER - std::vector media_players_{}; + StaticVector media_players_{}; #endif #ifdef USE_ALARM_CONTROL_PANEL - std::vector alarm_control_panels_{}; + StaticVector + alarm_control_panels_{}; #endif #ifdef USE_UPDATE - std::vector updates_{}; + StaticVector updates_{}; #endif #ifdef USE_SOCKET_SELECT_SUPPORT diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 1e8f670d8b..668c4a1fda 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -17,19 +17,6 @@ void ComponentIterator::begin(bool include_internal) { this->include_internal_ = include_internal; } -template -void ComponentIterator::process_platform_item_(const std::vector &items, - bool (ComponentIterator::*on_item)(PlatformItem *)) { - if (this->at_ >= items.size()) { - this->advance_platform_(); - } else { - PlatformItem *item = items[this->at_]; - if ((item->is_internal() && !this->include_internal_) || (this->*on_item)(item)) { - this->at_++; - } - } -} - void ComponentIterator::advance_platform_() { this->state_ = static_cast(static_cast(this->state_) + 1); this->at_ = 0; diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 7a9771b8f2..fdc30485bc 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -172,9 +172,19 @@ class ComponentIterator { uint16_t at_{0}; // Supports up to 65,535 entities per type bool include_internal_{false}; - template - void process_platform_item_(const std::vector &items, - bool (ComponentIterator::*on_item)(PlatformItem *)); + template + void process_platform_item_(const Container &items, + bool (ComponentIterator::*on_item)(typename Container::value_type)) { + if (this->at_ >= items.size()) { + this->advance_platform_(); + } else { + typename Container::value_type item = items[this->at_]; + if ((item->is_internal() && !this->include_internal_) || (this->*on_item)(item)) { + this->at_++; + } + } + } + void advance_platform_(); }; diff --git a/esphome/core/config.py b/esphome/core/config.py index 6d93117164..3bc030ad50 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -421,8 +421,10 @@ async def _add_automations(config): @coroutine_with_priority(-100.0) async def _add_platform_reserves() -> None: + # Generate compile-time entity count defines for static_entity_vector for platform_name, count in sorted(CORE.platform_counts.items()): - cg.add(cg.RawStatement(f"App.reserve_{platform_name}({count});"), prepend=True) + define_name = f"ESPHOME_ENTITY_{platform_name.upper()}_COUNT" + cg.add_define(define_name, count) @coroutine_with_priority(100.0) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 55652e443e..3ed0af91eb 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -239,3 +239,26 @@ // #define USE_BSEC2 // Requires a library with proprietary license #define USE_DASHBOARD_IMPORT + +// Default entity counts for static analysis +#define ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT 1 +#define ESPHOME_ENTITY_BINARY_SENSOR_COUNT 1 +#define ESPHOME_ENTITY_BUTTON_COUNT 1 +#define ESPHOME_ENTITY_CLIMATE_COUNT 1 +#define ESPHOME_ENTITY_COVER_COUNT 1 +#define ESPHOME_ENTITY_DATE_COUNT 1 +#define ESPHOME_ENTITY_DATETIME_COUNT 1 +#define ESPHOME_ENTITY_EVENT_COUNT 1 +#define ESPHOME_ENTITY_FAN_COUNT 1 +#define ESPHOME_ENTITY_LIGHT_COUNT 1 +#define ESPHOME_ENTITY_LOCK_COUNT 1 +#define ESPHOME_ENTITY_MEDIA_PLAYER_COUNT 1 +#define ESPHOME_ENTITY_NUMBER_COUNT 1 +#define ESPHOME_ENTITY_SELECT_COUNT 1 +#define ESPHOME_ENTITY_SENSOR_COUNT 1 +#define ESPHOME_ENTITY_SWITCH_COUNT 1 +#define ESPHOME_ENTITY_TEXT_COUNT 1 +#define ESPHOME_ENTITY_TEXT_SENSOR_COUNT 1 +#define ESPHOME_ENTITY_TIME_COUNT 1 +#define ESPHOME_ENTITY_UPDATE_COUNT 1 +#define ESPHOME_ENTITY_VALVE_COUNT 1 diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 5204804e1e..b05cc11029 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -91,6 +91,42 @@ template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n); ///@} +/// @name Container utilities +///@{ + +/// Minimal static vector - saves memory by avoiding std::vector overhead +template class StaticVector { + public: + using value_type = T; + using iterator = typename std::array::iterator; + using const_iterator = typename std::array::const_iterator; + + private: + std::array data_{}; + size_t count_{0}; + + public: + // Minimal vector-compatible interface - only what we actually use + void push_back(const T &value) { + if (count_ < N) { + data_[count_++] = value; + } + } + + size_t size() const { return count_; } + + T &operator[](size_t i) { return data_[i]; } + const T &operator[](size_t i) const { return data_[i]; } + + // For range-based for loops + iterator begin() { return data_.begin(); } + iterator end() { return data_.begin() + count_; } + const_iterator begin() const { return data_.begin(); } + const_iterator end() const { return data_.begin() + count_; } +}; + +///@} + /// @name Mathematics ///@{ From 20959c2366f82c599e5c3e1ad0dba41fa3427f9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Aug 2025 14:12:08 -1000 Subject: [PATCH 3/3] [bluetooth_proxy] Optimize memory usage with fixed-size array and const string references --- .../bluetooth_proxy/bluetooth_proxy.cpp | 15 +++++++++------ .../components/bluetooth_proxy/bluetooth_proxy.h | 14 +++++++++----- .../components/esp32_ble_client/ble_client_base.h | 2 +- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index a59a33117a..302945bc12 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -35,8 +35,8 @@ void BluetoothProxy::setup() { // Don't pre-allocate pool - let it grow only if needed in busy environments // Many devices in quiet areas will never need the overflow pool - this->connections_free_response_.limit = this->connections_.size(); - this->connections_free_response_.free = this->connections_.size(); + this->connections_free_response_.limit = this->connection_count_; + this->connections_free_response_.free = this->connection_count_; this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { if (this->api_connection_ != nullptr) { @@ -134,12 +134,13 @@ void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, " Active: %s\n" " Connections: %d", - YESNO(this->active_), this->connections_.size()); + YESNO(this->active_), this->connection_count_); } void BluetoothProxy::loop() { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { - for (auto *connection : this->connections_) { + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; if (connection->get_address() != 0 && !connection->disconnect_pending()) { connection->disconnect(); } @@ -162,7 +163,8 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par } BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { - for (auto *connection : this->connections_) { + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; if (connection->get_address() == address) return connection; } @@ -170,7 +172,8 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese if (!reserve) return nullptr; - for (auto *connection : this->connections_) { + for (uint8_t i = 0; i < this->connection_count_; i++) { + auto *connection = this->connections_[i]; if (connection->get_address() == 0) { connection->send_service_ = DONE_SENDING_SERVICES; connection->set_address(address); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 70deef1ebd..d367dad438 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include #include #include @@ -63,8 +64,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; void register_connection(BluetoothConnection *connection) { - this->connections_.push_back(connection); - connection->proxy_ = this; + if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) { + this->connections_[this->connection_count_++] = connection; + connection->proxy_ = this; + } } void bluetooth_device_request(const api::BluetoothDeviceRequest &msg); @@ -138,8 +141,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com // Group 1: Pointers (4 bytes each, naturally aligned) api::APIConnection *api_connection_{nullptr}; - // Group 2: Container types (typically 12 bytes on 32-bit) - std::vector connections_{}; + // Group 2: Fixed-size array of connection pointers + std::array connections_{}; // BLE advertisement batching std::vector advertisement_pool_; @@ -154,7 +157,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com // Group 4: 1-byte types grouped together bool active_; uint8_t advertisement_count_{0}; - // 2 bytes used, 2 bytes padding + uint8_t connection_count_{0}; + // 3 bytes used, 1 byte padding }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 0a2fda4476..0bbff8d3c6 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -66,7 +66,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { (uint8_t) (this->address_ >> 0) & 0xff); } } - std::string address_str() const { return this->address_str_; } + const std::string &address_str() const { return this->address_str_; } BLEService *get_service(espbt::ESPBTUUID uuid); BLEService *get_service(uint16_t uuid);