mirror of
https://github.com/esphome/esphome.git
synced 2026-05-30 07:16:11 +08:00
[api] Add max_value proto option for constant-size varint codegen (#15424)
This commit is contained in:
@@ -1606,7 +1606,7 @@ message BluetoothLEAdvertisementResponse {
|
|||||||
message BluetoothLERawAdvertisement {
|
message BluetoothLERawAdvertisement {
|
||||||
uint64 address = 1 [(force) = true];
|
uint64 address = 1 [(force) = true];
|
||||||
sint32 rssi = 2 [(force) = true];
|
sint32 rssi = 2 [(force) = true];
|
||||||
uint32 address_type = 3;
|
uint32 address_type = 3 [(max_value) = 4];
|
||||||
|
|
||||||
bytes data = 4 [(fixed_array_size) = 62, (force) = true];
|
bytes data = 4 [(fixed_array_size) = 62, (force) = true];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,4 +96,10 @@ extend google.protobuf.FieldOptions {
|
|||||||
// variant of the calc_ method. Use on fields that are almost always non-default
|
// variant of the calc_ method. Use on fields that are almost always non-default
|
||||||
// to eliminate dead branches on hot paths.
|
// to eliminate dead branches on hot paths.
|
||||||
optional bool force = 50016 [default=false];
|
optional bool force = 50016 [default=false];
|
||||||
|
|
||||||
|
// max_value: Maximum value a field can have.
|
||||||
|
// When max_value < 128, the code generator emits constant-size calculations
|
||||||
|
// and direct byte writes instead of varint branching, since the encoded varint
|
||||||
|
// is guaranteed to be 1 byte.
|
||||||
|
optional uint32 max_value = 50017;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2255,15 +2255,15 @@ void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer &buffer) const {
|
|||||||
buffer.encode_varint_raw(encode_zigzag32(this->rssi));
|
buffer.encode_varint_raw(encode_zigzag32(this->rssi));
|
||||||
buffer.encode_uint32(3, this->address_type);
|
buffer.encode_uint32(3, this->address_type);
|
||||||
buffer.write_raw_byte(34);
|
buffer.write_raw_byte(34);
|
||||||
buffer.encode_varint_raw(this->data_len);
|
buffer.write_raw_byte(static_cast<uint8_t>(this->data_len));
|
||||||
buffer.encode_raw(this->data, this->data_len);
|
buffer.encode_raw(this->data, this->data_len);
|
||||||
}
|
}
|
||||||
uint32_t BluetoothLERawAdvertisement::calculate_size() const {
|
uint32_t BluetoothLERawAdvertisement::calculate_size() const {
|
||||||
uint32_t size = 0;
|
uint32_t size = 0;
|
||||||
size += ProtoSize::calc_uint64_force(1, this->address);
|
size += ProtoSize::calc_uint64_force(1, this->address);
|
||||||
size += ProtoSize::calc_sint32_force(1, this->rssi);
|
size += ProtoSize::calc_sint32_force(1, this->rssi);
|
||||||
size += ProtoSize::calc_uint32(1, this->address_type);
|
size += this->address_type ? 2 : 0;
|
||||||
size += ProtoSize::calc_length_force(1, this->data_len);
|
size += 2 + this->data_len;
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const {
|
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const {
|
||||||
|
|||||||
@@ -156,6 +156,11 @@ class TypeInfo(ABC):
|
|||||||
"""Check if this field should always be encoded (skip zero/empty check)."""
|
"""Check if this field should always be encoded (skip zero/empty check)."""
|
||||||
return get_field_opt(self._field, pb.force, False)
|
return get_field_opt(self._field, pb.force, False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_value(self) -> int | None:
|
||||||
|
"""Get the max_value option for this field, or None if not set."""
|
||||||
|
return get_field_opt(self._field, pb.max_value, None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wire_type(self) -> WireType:
|
def wire_type(self) -> WireType:
|
||||||
"""Get the wire type for the field."""
|
"""Get the wire type for the field."""
|
||||||
@@ -235,37 +240,56 @@ class TypeInfo(ABC):
|
|||||||
"encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);",
|
"encode_bool": "buffer.write_raw_byte({value} ? 0x01 : 0x00);",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# When max_value < 128, the varint is always 1 byte — use a direct byte write
|
||||||
|
RAW_ENCODE_SMALL_MAP: dict[str, str] = {
|
||||||
|
"encode_uint32": "buffer.write_raw_byte(static_cast<uint8_t>({value}));",
|
||||||
|
"encode_uint64": "buffer.write_raw_byte(static_cast<uint8_t>({value}));",
|
||||||
|
}
|
||||||
|
|
||||||
def _encode_with_precomputed_tag(self, value_expr: str) -> str | None:
|
def _encode_with_precomputed_tag(self, value_expr: str) -> str | None:
|
||||||
"""Try to emit a precomputed-tag encode for a forced field.
|
"""Try to emit a precomputed-tag encode for a forced field.
|
||||||
|
|
||||||
Returns the raw encode string if the tag is a single byte and the
|
Returns the raw encode string if the tag is a single byte and the
|
||||||
encode_func has a known raw equivalent, or None otherwise.
|
encode_func has a known raw equivalent, or None otherwise.
|
||||||
|
When max_value < 128, uses direct byte write instead of varint encoding.
|
||||||
"""
|
"""
|
||||||
if not self.force:
|
if not self.force:
|
||||||
return None
|
return None
|
||||||
tag = self.calculate_tag()
|
tag = self.calculate_tag()
|
||||||
if tag >= 128:
|
if tag >= 128:
|
||||||
return None
|
return None
|
||||||
raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func)
|
max_val = self.max_value
|
||||||
|
raw_expr = None
|
||||||
|
if max_val is not None and max_val < 128:
|
||||||
|
raw_expr = self.RAW_ENCODE_SMALL_MAP.get(self.encode_func)
|
||||||
|
if raw_expr is None:
|
||||||
|
raw_expr = self.RAW_ENCODE_MAP.get(self.encode_func)
|
||||||
if raw_expr is None:
|
if raw_expr is None:
|
||||||
return None
|
return None
|
||||||
return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}"
|
return f"buffer.write_raw_byte({tag});\n{raw_expr.format(value=value_expr)}"
|
||||||
|
|
||||||
def _encode_bytes_with_precomputed_tag(
|
def _encode_bytes_with_precomputed_tag(
|
||||||
self, data_expr: str, len_expr: str
|
self, data_expr: str, len_expr: str, max_len: int | None = None
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Try to emit a precomputed-tag encode for a forced bytes/string field.
|
"""Try to emit a precomputed-tag encode for a forced bytes/string field.
|
||||||
|
|
||||||
Returns the raw encode string if the tag is a single byte, or None.
|
Returns the raw encode string if the tag is a single byte, or None.
|
||||||
|
When max_len < 128, uses direct byte write for the length varint.
|
||||||
"""
|
"""
|
||||||
if not self.force:
|
if not self.force:
|
||||||
return None
|
return None
|
||||||
tag = self.calculate_tag()
|
tag = self.calculate_tag()
|
||||||
if tag >= 128:
|
if tag >= 128:
|
||||||
return None
|
return None
|
||||||
|
# When max_len < 128, length varint is always 1 byte
|
||||||
|
len_encode = (
|
||||||
|
f"buffer.write_raw_byte(static_cast<uint8_t>({len_expr}));"
|
||||||
|
if max_len is not None and max_len < 128
|
||||||
|
else f"buffer.encode_varint_raw({len_expr});"
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
f"buffer.write_raw_byte({tag});\n"
|
f"buffer.write_raw_byte({tag});\n"
|
||||||
f"buffer.encode_varint_raw({len_expr});\n"
|
f"{len_encode}\n"
|
||||||
f"buffer.encode_raw({data_expr}, {len_expr});"
|
f"buffer.encode_raw({data_expr}, {len_expr});"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -346,6 +370,25 @@ class TypeInfo(ABC):
|
|||||||
value = value_expr or name
|
value = value_expr or name
|
||||||
return f"size += ProtoSize::{method}({field_id_size}, {value});"
|
return f"size += ProtoSize::{method}({field_id_size}, {value});"
|
||||||
|
|
||||||
|
def _get_single_byte_varint_size(
|
||||||
|
self, name: str, force: bool, extra_expr: str | None = None
|
||||||
|
) -> str:
|
||||||
|
"""Size calculation when the varint is guaranteed to be 1 byte.
|
||||||
|
|
||||||
|
Used when max_value < 128 or fixed_array_size < 128.
|
||||||
|
The fixed part is field_id_size + 1 (tag + 1-byte varint).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Expression to check for zero (non-force only)
|
||||||
|
force: Whether to skip the zero check
|
||||||
|
extra_expr: Additional variable expression to add (e.g., data length)
|
||||||
|
"""
|
||||||
|
fixed = self.calculate_field_id_size() + 1
|
||||||
|
size_expr = f"{fixed} + {extra_expr}" if extra_expr else str(fixed)
|
||||||
|
if force:
|
||||||
|
return f"size += {size_expr};"
|
||||||
|
return f"size += {name} ? {size_expr} : 0;"
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||||
"""Calculate the size needed for encoding this field.
|
"""Calculate the size needed for encoding this field.
|
||||||
@@ -1191,8 +1234,9 @@ class FixedArrayBytesType(TypeInfo):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def encode_content(self) -> str:
|
def encode_content(self) -> str:
|
||||||
|
max_len = self.array_size if isinstance(self.array_size, int) else None
|
||||||
if result := self._encode_bytes_with_precomputed_tag(
|
if result := self._encode_bytes_with_precomputed_tag(
|
||||||
f"this->{self.field_name}", f"this->{self.field_name}_len"
|
f"this->{self.field_name}", f"this->{self.field_name}_len", max_len=max_len
|
||||||
):
|
):
|
||||||
return result
|
return result
|
||||||
if self.force:
|
if self.force:
|
||||||
@@ -1212,13 +1256,14 @@ class FixedArrayBytesType(TypeInfo):
|
|||||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||||
# Use the actual length stored in the _len field
|
# Use the actual length stored in the _len field
|
||||||
length_field = f"this->{self.field_name}_len"
|
length_field = f"this->{self.field_name}_len"
|
||||||
field_id_size = self.calculate_field_id_size()
|
|
||||||
|
|
||||||
if force:
|
# When array_size < 128, length varint is always 1 byte
|
||||||
# For repeated fields, always calculate size (no zero check)
|
if isinstance(self.array_size, int) and self.array_size < 128:
|
||||||
return f"size += ProtoSize::calc_length_force({field_id_size}, {length_field});"
|
return self._get_single_byte_varint_size(
|
||||||
# For non-repeated fields, length already checks for zero
|
length_field, force, extra_expr=length_field
|
||||||
return f"size += ProtoSize::calc_length({field_id_size}, {length_field});"
|
)
|
||||||
|
|
||||||
|
return self._get_simple_size_calculation(length_field, force, "length")
|
||||||
|
|
||||||
def get_estimated_size(self) -> int:
|
def get_estimated_size(self) -> int:
|
||||||
# Estimate based on typical BLE advertisement size
|
# Estimate based on typical BLE advertisement size
|
||||||
@@ -1245,6 +1290,9 @@ class UInt32Type(TypeInfo):
|
|||||||
return o
|
return o
|
||||||
|
|
||||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||||
|
max_val = self.max_value
|
||||||
|
if max_val is not None and max_val < 128:
|
||||||
|
return self._get_single_byte_varint_size(name, force)
|
||||||
return self._get_simple_size_calculation(name, force, "uint32")
|
return self._get_simple_size_calculation(name, force, "uint32")
|
||||||
|
|
||||||
def get_estimated_size(self) -> int:
|
def get_estimated_size(self) -> int:
|
||||||
|
|||||||
Reference in New Issue
Block a user