1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-31 23:21:54 +00:00

[select] Store options in flash to reduce RAM usage

This commit is contained in:
J. Nick Koston
2025-10-24 04:13:34 -07:00
parent 2c85ba037e
commit d27e78e909
12 changed files with 58 additions and 26 deletions

View File

@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6 [(container_pointer) = "std::vector"];
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];

View File

@@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_ENTITY_ICON
buffer.encode_string(5, this->icon_ref_);
#endif
for (const auto &it : *this->options) {
buffer.encode_string(6, it, true);
for (const char *it : *this->options) {
buffer.encode_string(6, it, strlen(it), true);
}
buffer.encode_bool(7, this->disabled_by_default);
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
@@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->icon_ref_.size());
#endif
if (!this->options->empty()) {
for (const auto &it : *this->options) {
size.add_length_force(1, it.size());
for (const char *it : *this->options) {
size.add_length_force(1, strlen(it));
}
}
size.add_bool(1, this->disabled_by_default);

View File

@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_select_response"; }
#endif
const std::vector<std::string> *options{};
const FixedVector<const char *> *options{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -9,7 +9,8 @@ static const char *const TAG = "copy.select";
void CopySelect::setup() {
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
traits.set_options(source_->traits.get_options());
const auto &source_options = source_->traits.get_options();
traits.set_options({source_options.begin(), source_options.end()});
if (source_->has_state())
this->publish_state(source_->state);

View File

@@ -358,7 +358,7 @@ class LvSelectable : public LvCompound {
virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0;
void set_selected_text(const std::string &text, lv_anim_enable_t anim);
std::string get_selected_text();
std::vector<std::string> get_options() { return this->options_; }
const std::vector<std::string> &get_options() { return this->options_; }
void set_options(std::vector<std::string> options);
protected:

View File

@@ -53,7 +53,17 @@ class LVGLSelect : public select::Select, public Component {
this->widget_->set_selected_text(value, this->anim_);
this->publish();
}
void set_options_() { this->traits.set_options(this->widget_->get_options()); }
void set_options_() {
// Widget uses std::vector<std::string>, SelectTraits uses FixedVector<const char*>
// Convert by extracting c_str() pointers
const auto &opts = this->widget_->get_options();
std::vector<const char *> opt_ptrs;
opt_ptrs.reserve(opts.size());
for (const auto &opt : opts) {
opt_ptrs.push_back(opt.c_str());
}
this->traits.set_options({opt_ptrs.begin(), opt_ptrs.end()});
}
LvSelectable *widget_;
lv_anim_enable_t anim_;

View File

@@ -1,5 +1,6 @@
#include "select.h"
#include "esphome/core/log.h"
#include <cstring>
namespace esphome {
namespace select {
@@ -35,7 +36,7 @@ size_t Select::size() const {
optional<size_t> Select::index_of(const std::string &option) const {
const auto &options = traits.get_options();
for (size_t i = 0; i < options.size(); i++) {
if (options[i] == option) {
if (strcmp(options[i], option.c_str()) == 0) {
return i;
}
}
@@ -53,7 +54,7 @@ optional<size_t> Select::active_index() const {
optional<std::string> Select::at(size_t index) const {
if (this->has_index(index)) {
const auto &options = traits.get_options();
return options.at(index);
return std::string(options.at(index));
} else {
return {};
}

View File

@@ -3,9 +3,9 @@
namespace esphome {
namespace select {
void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
void SelectTraits::set_options(std::initializer_list<const char *> options) { this->options_ = options; }
const std::vector<std::string> &SelectTraits::get_options() const { return this->options_; }
const FixedVector<const char *> &SelectTraits::get_options() const { return this->options_; }
} // namespace select
} // namespace esphome

View File

@@ -1,18 +1,18 @@
#pragma once
#include <vector>
#include <string>
#include "esphome/core/helpers.h"
#include <initializer_list>
namespace esphome {
namespace select {
class SelectTraits {
public:
void set_options(std::vector<std::string> options);
const std::vector<std::string> &get_options() const;
void set_options(std::initializer_list<const char *> options);
const FixedVector<const char *> &get_options() const;
protected:
std::vector<std::string> options_;
FixedVector<const char *> options_;
};
} // namespace select

View File

@@ -22,7 +22,7 @@ void TemplateSelect::setup() {
ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str());
} else if (!this->has_index(index)) {
value = this->initial_option_;
ESP_LOGD(TAG, "State from initial (restored index %d out of bounds): %s", index, value.c_str());
ESP_LOGD(TAG, "State from initial (restored index %zu out of bounds): %s", index, value.c_str());
} else {
value = this->at(index).value();
ESP_LOGD(TAG, "State from restore: %s", value.c_str());

View File

@@ -304,6 +304,11 @@ template<typename T> class FixedVector {
return data_[size_ - 1];
}
/// Access first element (no bounds checking - matches std::vector behavior)
/// Caller must ensure vector is not empty (size() > 0)
T &front() { return data_[0]; }
const T &front() const { return data_[0]; }
/// Access last element (no bounds checking - matches std::vector behavior)
/// Caller must ensure vector is not empty (size() > 0)
T &back() { return data_[size_ - 1]; }
@@ -317,6 +322,11 @@ template<typename T> class FixedVector {
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }
/// Access element with bounds checking (matches std::vector behavior)
/// Caller must ensure index is valid (i < size())
T &at(size_t i) { return data_[i]; }
const T &at(size_t i) const { return data_[i]; }
// Iterator support for range-based for loops
T *begin() { return data_; }
T *end() { return data_ + size_; }

View File

@@ -1533,11 +1533,16 @@ class RepeatedTypeInfo(TypeInfo):
def encode_content(self) -> str:
if self._use_pointer:
# For pointer fields, just dereference (pointer should never be null in our use case)
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<uint32_t>(it), true);\n"
# Special handling for const char* elements (when container_no_template contains "const char")
if "const char" in self._container_no_template:
o = f"for (const char *it : *this->{self.field_name}) {{\n"
o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n"
else:
o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n"
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<uint32_t>(it), true);\n"
else:
o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n"
o += "}"
return o
o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n"
@@ -1588,9 +1593,14 @@ class RepeatedTypeInfo(TypeInfo):
o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n"
else:
# Other types need the actual value
auto_ref = "" if self._ti_is_bool else "&"
o += f" for (const auto {auto_ref}it : {container_ref}) {{\n"
o += f" {self._ti.get_size_calculation('it', True)}\n"
# Special handling for const char* elements
if self._use_pointer and "const char" in self._container_no_template:
o += f" for (const char *it : {container_ref}) {{\n"
o += " size.add_length_force(1, strlen(it));\n"
else:
auto_ref = "" if self._ti_is_bool else "&"
o += f" for (const auto {auto_ref}it : {container_ref}) {{\n"
o += f" {self._ti.get_size_calculation('it', True)}\n"
o += " }\n"
o += "}"