From f2eb61a767d986fa425efbc5ef233c3456054fab Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 10 Jan 2026 19:43:27 -0600 Subject: [PATCH] [api] Proto code generator changes for #12985 (#13100) Co-authored-by: J. Nick Koston --- esphome/components/api/api_options.proto | 11 +++ script/api_protobuf/api_protobuf.py | 85 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/esphome/components/api/api_options.proto b/esphome/components/api/api_options.proto index 6b33408e2f..1916e84625 100644 --- a/esphome/components/api/api_options.proto +++ b/esphome/components/api/api_options.proto @@ -80,4 +80,15 @@ extend google.protobuf.FieldOptions { // Example: [(container_pointer_no_template) = "light::ColorModeMask"] // generates: const light::ColorModeMask *supported_color_modes{}; optional string container_pointer_no_template = 50014; + + // packed_buffer: Expose raw packed buffer instead of decoding into container + // When set on a packed repeated field, the generated code stores a pointer + // to the raw protobuf buffer instead of decoding values. This enables + // zero-copy passthrough when the consumer can decode on-demand. + // The field must be a packed repeated field (packed=true). + // Generates three fields: + // - const uint8_t *_data_{nullptr}; + // - uint16_t _length_{0}; + // - uint16_t _count_{0}; + optional bool packed_buffer = 50015 [default=false]; } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 274a672c7c..c61555805e 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -339,6 +339,9 @@ def create_field_type_info( ) -> TypeInfo: """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" if field.label == FieldDescriptorProto.LABEL_REPEATED: + # Check if this is a packed_buffer field (zero-copy packed repeated) + if get_field_opt(field, pb.packed_buffer, False): + return PackedBufferTypeInfo(field) # Check if this repeated field has fixed_array_with_length_define option if ( fixed_size := get_field_opt(field, pb.fixed_array_with_length_define) @@ -947,6 +950,88 @@ class PointerToStringBufferType(PointerToBufferTypeBase): return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string +class PackedBufferTypeInfo(TypeInfo): + """Type for packed repeated fields that expose raw buffer instead of decoding. + + When a repeated field is marked with [(packed_buffer) = true], this type + generates code that stores a pointer to the raw protobuf buffer along with + its length and the count of values. This enables zero-copy passthrough when + the consumer can decode the packed varints on-demand. + """ + + def __init__(self, field: descriptor.FieldDescriptorProto) -> None: + # packed_buffer is decode-only (SOURCE_CLIENT messages) + super().__init__(field, needs_decode=True, needs_encode=False) + + @property + def cpp_type(self) -> str: + # Not used - we have multiple fields + return "const uint8_t*" + + @property + def wire_type(self) -> WireType: + """Packed fields use LENGTH_DELIMITED wire type.""" + return WireType.LENGTH_DELIMITED + + @property + def public_content(self) -> list[str]: + """Generate three fields: data pointer, length, and count.""" + return [ + f"const uint8_t *{self.field_name}_data_{{nullptr}};", + f"uint16_t {self.field_name}_length_{{0}};", + f"uint16_t {self.field_name}_count_{{0}};", + ] + + @property + def decode_length_content(self) -> str: + """Store pointer to buffer and calculate count of packed varints.""" + return f"""case {self.number}: {{ + this->{self.field_name}_data_ = value.data(); + this->{self.field_name}_length_ = value.size(); + this->{self.field_name}_count_ = count_packed_varints(value.data(), value.size()); + break; + }}""" + + @property + def encode_content(self) -> str: + """No encoding - this is decode-only for SOURCE_CLIENT messages.""" + return None + + @property + def dump_content(self) -> str: + """Dump shows buffer info but not decoded values.""" + return ( + f'out.append(" {self.name}: ");\n' + + 'out.append("packed buffer [");\n' + + f"out.append(std::to_string(this->{self.field_name}_count_));\n" + + 'out.append(" values, ");\n' + + f"out.append(std::to_string(this->{self.field_name}_length_));\n" + + 'out.append(" bytes]\\n");' + ) + + def dump(self, name: str) -> str: + """Dump method for packed buffer - not typically used but required by abstract base.""" + return 'out.append("packed buffer");' + + def get_size_calculation(self, name: str, force: bool = False) -> str: + """No size calculation needed - decode-only.""" + return "" + + def get_estimated_size(self) -> int: + """Estimate size for packed buffer field. + + Typical IR/RF timing array has ~50-200 values, each encoded as 1-3 bytes. + Estimate 100 values * 2 bytes = 200 bytes typical. + """ + return ( + self.calculate_field_id_size() + 2 + 200 + ) # field ID + length varint + data + + @classmethod + def can_use_dump_field(cls) -> bool: + return False + + class FixedArrayBytesType(TypeInfo): """Special type for fixed-size byte arrays."""