From c76e386a79d16a2935f69c10fe352d2fb9fd07e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 18 Oct 2025 09:59:24 -1000 Subject: [PATCH] no vector --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_options.proto | 9 +++++ esphome/components/api/api_pb2.cpp | 14 +++++--- esphome/components/api/api_pb2.h | 2 +- esphome/components/api/api_pb2_dump.cpp | 6 ++-- script/api_protobuf/api_protobuf.py | 42 ++++++++++++++++++++++++ 6 files changed, 65 insertions(+), 10 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4c1de4c4f5..c64fc038d6 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -506,7 +506,7 @@ message ListEntitiesLightResponse { string name = 3; reserved 4; // Deprecated: was string unique_id - repeated ColorMode supported_color_modes = 12 [(fixed_vector) = true]; + repeated ColorMode supported_color_modes = 12 [(enum_as_bitmask) = true]; // next four supports_* are for legacy clients, newer clients should use color modes // Deprecated in API version 1.6 bool legacy_supports_brightness = 5 [deprecated=true]; diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index ead8ac0bbc..450b5e83de 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -70,4 +70,13 @@ extend google.protobuf.FieldOptions { // 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]; + + // enum_as_bitmask: Encode repeated enum fields as a uint32_t bitmask + // When set on a repeated enum field, the field will be stored as a single uint32_t + // where each bit represents whether that enum value is present. This is ideal for + // enums with ≤32 values and eliminates all vector template instantiation overhead. + // The enum values should be sequential starting from 0. + // Encoding: bit N set means enum value N is present in the set. + // Example: {ColorMode::RGB, ColorMode::WHITE} → bitmask with bits 5 and 6 set + optional bool enum_as_bitmask = 50014 [default=false]; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6bc434b658..c7b88bb312 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -471,8 +471,10 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->object_id_ref_); buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name_ref_); - for (auto &it : this->supported_color_modes) { - buffer.encode_uint32(12, static_cast(it), true); + for (uint8_t bit = 0; bit < 32; bit++) { + if (this->supported_color_modes & (1U << bit)) { + buffer.encode_uint32(12, bit, true); + } } buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); @@ -492,9 +494,11 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { size.add_length(1, this->object_id_ref_.size()); size.add_fixed32(1, this->key); size.add_length(1, this->name_ref_.size()); - if (!this->supported_color_modes.empty()) { - for (const auto &it : this->supported_color_modes) { - size.add_uint32_force(1, static_cast(it)); + if (this->supported_color_modes != 0) { + for (uint8_t bit = 0; bit < 32; bit++) { + if (this->supported_color_modes & (1U << bit)) { + size.add_uint32_force(1, static_cast(bit)); + } } } size.add_float(1, this->min_mireds); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 528b8fc108..5b86b7f276 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_light_response"; } #endif - FixedVector supported_color_modes{}; + uint32_t supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index cda68ceee8..f9f45ad071 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -913,9 +913,9 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { dump_field(out, "object_id", this->object_id_ref_); dump_field(out, "key", this->key); dump_field(out, "name", this->name_ref_); - for (const auto &it : this->supported_color_modes) { - dump_field(out, "supported_color_modes", static_cast(it), 4); - } + out.append(" supported_color_modes: 0x"); + out.append(uint32_to_string(this->supported_color_modes)); + out.append("\n"); dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "max_mireds", this->max_mireds); for (const auto &it : this->effects) { diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 4936434fc2..9e140ca9ce 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1418,6 +1418,8 @@ class RepeatedTypeInfo(TypeInfo): 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) + # Check if this should be encoded as a bitmask + self._use_bitmask = get_field_opt(field, pb.enum_as_bitmask, 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 @@ -1434,6 +1436,9 @@ class RepeatedTypeInfo(TypeInfo): @property def cpp_type(self) -> str: + if self._use_bitmask: + # For bitmask fields, store as a single uint32_t + return "uint32_t" if self._use_pointer and self._container_type: # For pointer fields, use the specified container type # If the container type already includes the element type (e.g., std::set) @@ -1466,6 +1471,12 @@ class RepeatedTypeInfo(TypeInfo): # Pointer fields don't support decoding if self._use_pointer: return None + if self._use_bitmask: + # For bitmask fields, decode enum value and set corresponding bit + content = self._ti.decode_varint + if content is None: + return None + return f"case {self.number}: this->{self.field_name} |= (1U << static_cast({content})); break;" content = self._ti.decode_varint if content is None: return None @@ -1519,6 +1530,18 @@ class RepeatedTypeInfo(TypeInfo): @property def encode_content(self) -> str: + if self._use_bitmask: + # For bitmask fields, iterate through set bits and encode each enum value + # The bitmask is stored as uint32_t where bit N represents enum value N + assert isinstance(self._ti, EnumType), ( + "enum_as_bitmask only works with enum fields" + ) + o = "for (uint8_t bit = 0; bit < 32; bit++) {\n" + o += f" if (this->{self.field_name} & (1U << bit)) {{\n" + o += f" buffer.{self._ti.encode_func}({self.number}, bit, true);\n" + o += " }\n" + o += "}" + return o 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" @@ -1538,6 +1561,13 @@ class RepeatedTypeInfo(TypeInfo): @property def dump_content(self) -> str: + if self._use_bitmask: + # For bitmask fields, dump the hex value of the bitmask + return ( + f'out.append(" {self.field_name}: 0x");\n' + f"out.append(uint32_to_string(this->{self.field_name}));\n" + f'out.append("\\n");' + ) if self._use_pointer: # For pointer fields, dereference and use the existing helper return _generate_array_dump_content( @@ -1554,6 +1584,18 @@ class RepeatedTypeInfo(TypeInfo): # For repeated fields, we always need to pass force=True to the underlying type's calculation # This is because the encode method always sets force=true for repeated fields + if self._use_bitmask: + # For bitmask fields, iterate through set bits and calculate size + # Each set bit encodes one enum value (as varint) + o = f"if ({name} != 0) {{\n" + o += " for (uint8_t bit = 0; bit < 32; bit++) {\n" + o += f" if ({name} & (1U << bit)) {{\n" + o += f" {self._ti.get_size_calculation('bit', True)}\n" + o += " }\n" + o += " }\n" + o += "}" + return o + # Handle message types separately as they use a dedicated helper if isinstance(self._ti, MessageType): field_id_size = self._ti.calculate_field_id_size()