Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2026-01-10 17:09:03 -10:00
8 changed files with 153 additions and 11 deletions
+11
View File
@@ -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 *<field>_data_{nullptr};
// - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false];
}
+23
View File
@@ -32,6 +32,7 @@ from .const import (
CONF_SDK_SILENT,
CONF_UART_PORT,
FAMILIES,
FAMILY_BK7231N,
FAMILY_COMPONENT,
FAMILY_FRIENDLY,
KEY_BOARD,
@@ -50,6 +51,22 @@ CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["preferences"]
IS_TARGET_PLATFORM = True
# BK7231N SDK options to disable unused features.
# Disabling BLE saves ~21KB RAM and ~200KB Flash because BLE init code is
# called unconditionally by the SDK. ESPHome doesn't use BLE on LibreTiny.
#
# This only works on BK7231N (BLE 5.x). Other BK72XX chips using BLE 4.2
# (BK7231T, BK7231Q, BK7251; BK7252 boards use the BK7251 family) have a bug
# where the BLE library still links and references undefined symbols when
# CFG_SUPPORT_BLE=0.
#
# Other options like CFG_TX_EVM_TEST, CFG_RX_SENSITIVITY_TEST, CFG_SUPPORT_BKREG,
# CFG_SUPPORT_OTA_HTTP, and CFG_USE_SPI_SLAVE were evaluated but provide no # NOLINT
# measurable benefit - the linker already strips unreferenced code via -gc-sections.
_BK7231N_SYS_CONFIG_OPTIONS = [
"CFG_SUPPORT_BLE=0",
]
def _detect_variant(value):
if KEY_LIBRETINY not in CORE.data:
@@ -346,4 +363,10 @@ async def component_to_code(config):
cg.add_platformio_option("custom_fw_name", "esphome")
cg.add_platformio_option("custom_fw_version", __version__)
# Apply chip-specific SDK options to save RAM/Flash
if config[CONF_FAMILY] == FAMILY_BK7231N:
cg.add_platformio_option(
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
)
await cg.register_component(var, config)
+4 -2
View File
@@ -6,8 +6,10 @@ namespace one_wire {
static const char *const TAG = "one_wire";
const std::string &OneWireDevice::get_address_name() {
if (this->address_name_.empty())
this->address_name_ = std::string("0x") + format_hex(this->address_);
if (this->address_name_.empty()) {
char hex_buf[19]; // "0x" + 16 hex chars + null
this->address_name_ = format_hex_prefixed_to(hex_buf, this->address_);
}
return this->address_name_;
}
@@ -24,11 +24,9 @@ void SmlTextSensor::publish_val(const ObisInfo &obis_info) {
case SML_HEX: {
// Buffer for "0x" + up to 32 bytes as hex + null
char buf[67];
buf[0] = '0';
buf[1] = 'x';
// Max 32 bytes of data fit in remaining buffer ((65-1)/2)
// Max 32 bytes of data fit in buffer ((67-3)/2)
size_t hex_bytes = std::min(obis_info.value.size(), size_t(32));
format_hex_to(buf + 2, sizeof(buf) - 2, obis_info.value.begin(), hex_bytes);
format_hex_prefixed_to(buf, obis_info.value.begin(), hex_bytes);
publish_state(buf, 2 + hex_bytes * 2);
break;
}
+2 -2
View File
@@ -2083,8 +2083,8 @@ void WiFiComponent::release_scan_results_() {
// std::vector - use swap trick since shrink_to_fit is non-binding
decltype(this->scan_result_)().swap(this->scan_result_);
#else
// FixedVector::shrink_to_fit() actually frees all memory
this->scan_result_.shrink_to_fit();
// FixedVector::release() frees all memory
this->scan_result_.release();
#endif
}
}
+25 -2
View File
@@ -296,8 +296,8 @@ template<typename T> class FixedVector {
size_ = 0;
}
// Shrink capacity to fit current size (frees all memory)
void shrink_to_fit() {
// Release all memory (destroys elements and frees memory)
void release() {
cleanup_();
reset_();
}
@@ -773,6 +773,29 @@ inline char *format_hex_to(char (&buffer)[N], T val) {
/// Calculate buffer size needed for format_hex_to: "XXXXXXXX...\0" = bytes * 2 + 1
constexpr size_t format_hex_size(size_t byte_count) { return byte_count * 2 + 1; }
/// Calculate buffer size needed for format_hex_prefixed_to: "0xXXXXXXXX...\0" = bytes * 2 + 3
constexpr size_t format_hex_prefixed_size(size_t byte_count) { return byte_count * 2 + 3; }
/// Format an unsigned integer as "0x" prefixed lowercase hex to buffer.
template<size_t N, typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
inline char *format_hex_prefixed_to(char (&buffer)[N], T val) {
static_assert(N >= sizeof(T) * 2 + 3, "Buffer too small for prefixed hex");
buffer[0] = '0';
buffer[1] = 'x';
val = convert_big_endian(val);
format_hex_to(buffer + 2, N - 2, reinterpret_cast<const uint8_t *>(&val), sizeof(T));
return buffer;
}
/// Format byte array as "0x" prefixed lowercase hex to buffer.
template<size_t N> inline char *format_hex_prefixed_to(char (&buffer)[N], const uint8_t *data, size_t length) {
static_assert(N >= 5, "Buffer must hold at least '0x' + one hex byte + null");
buffer[0] = '0';
buffer[1] = 'x';
format_hex_to(buffer + 2, N - 2, data, length);
return buffer;
}
/// Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0"
constexpr size_t format_hex_pretty_size(size_t byte_count) { return byte_count * 3; }
+1 -1
View File
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20260110.0
aioesphomeapi==43.10.1
aioesphomeapi==43.11.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import
+85
View File
@@ -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."""