Merge branch 'dev' into precompute-tag-forced-varint-fields

This commit is contained in:
J. Nick Koston
2026-03-23 13:41:55 -10:00
committed by GitHub
128 changed files with 3637 additions and 2082 deletions
+1
View File
@@ -83,6 +83,7 @@ ISOLATED_COMPONENTS = {
"openthread": "Conflicts with wifi: used by most components",
"openthread_info": "Conflicts with wifi: used by most components",
"matrix_keypad": "Needs isolation due to keypad",
"microphone": "Defines PDM microphone requiring I2S port 0 - conflicts with micro_wake_word PDM mic when merged",
"modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus",
"neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)",
"packages": "cannot merge packages",
+67 -44
View File
@@ -298,7 +298,7 @@ class TypeInfo(ABC):
@property
def dump_content(self) -> str:
# Default implementation - subclasses can override if they need special handling
return f'dump_field(out, "{self.name}", {self.dump_field_value(f"this->{self.field_name}")});'
return f'dump_field(out, ESPHOME_PSTR("{self.name}"), {self.dump_field_value(f"this->{self.field_name}")});'
@abstractmethod
def dump(self, name: str) -> str:
@@ -720,14 +720,14 @@ class StringType(TypeInfo):
def dump_content(self) -> str:
# For SOURCE_CLIENT only, use std::string
if not self._needs_encode:
return f'dump_field(out, "{self.name}", this->{self.field_name});'
return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});'
# For SOURCE_SERVER, use StringRef with _ref_ suffix
if not self._needs_decode:
return f'dump_field(out, "{self.name}", this->{self.field_name}_ref_);'
return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name}_ref_);'
# For SOURCE_BOTH, we need custom logic
o = f'out.append(" {self.name}: ");\n'
o = f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n'
o += self.dump(f"this->{self.field_name}") + "\n"
o += 'out.append("\\n");'
return o
@@ -800,7 +800,7 @@ class MessageType(TypeInfo):
@property
def dump_content(self) -> str:
o = f'out.append(" {self.name}: ");\n'
o = f'out.append(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n'
o += f"this->{self.field_name}.dump_to(out);\n"
o += 'out.append("\\n");'
return o
@@ -890,7 +890,7 @@ class BytesType(TypeInfo):
# For SOURCE_CLIENT only, always use std::string
if not self._needs_encode:
return (
f'dump_bytes_field(out, "{self.name}", '
f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), '
f"reinterpret_cast<const uint8_t*>(this->{self.field_name}.data()), "
f"this->{self.field_name}.size());"
)
@@ -898,17 +898,17 @@ class BytesType(TypeInfo):
# For SOURCE_SERVER, always use pointer/length
if not self._needs_decode:
return (
f'dump_bytes_field(out, "{self.name}", '
f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), '
f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);"
)
# For SOURCE_BOTH, check if pointer is set (sending) or use string (received)
return (
f"if (this->{self.field_name}_ptr_ != nullptr) {{\n"
f' dump_bytes_field(out, "{self.name}", '
f' dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), '
f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);\n"
f"}} else {{\n"
f' dump_bytes_field(out, "{self.name}", '
f' dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), '
f"reinterpret_cast<const uint8_t*>(this->{self.field_name}.data()), "
f"this->{self.field_name}.size());\n"
f"}}"
@@ -991,7 +991,7 @@ class PointerToBytesBufferType(PointerToBufferTypeBase):
@property
def dump_content(self) -> str:
return (
f'dump_bytes_field(out, "{self.name}", '
f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), '
f"this->{self.field_name}, this->{self.field_name}_len);"
)
@@ -1043,7 +1043,7 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
@property
def dump_content(self) -> str:
return f'dump_field(out, "{self.name}", this->{self.field_name});'
return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});'
def get_size_calculation(self, name: str, force: bool = False) -> str:
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());"
@@ -1103,12 +1103,12 @@ class PackedBufferTypeInfo(TypeInfo):
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(2, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n'
+ 'out.append_p(ESPHOME_PSTR("packed buffer ["));\n'
+ f"append_uint(out, this->{self.field_name}_count_);\n"
+ 'out.append(" values, ");\n'
+ 'out.append_p(ESPHOME_PSTR(" values, "));\n'
+ f"append_uint(out, this->{self.field_name}_length_);\n"
+ 'out.append(" bytes]\\n");'
+ 'out.append_p(ESPHOME_PSTR(" bytes]\\n"));'
)
def dump(self, name: str) -> str:
@@ -1205,7 +1205,7 @@ class FixedArrayBytesType(TypeInfo):
@property
def dump_content(self) -> str:
return (
f'dump_bytes_field(out, "{self.name}", '
f'dump_bytes_field(out, ESPHOME_PSTR("{self.name}"), '
f"this->{self.field_name}, this->{self.field_name}_len);"
)
@@ -1279,7 +1279,7 @@ class EnumType(TypeInfo):
return f"buffer.{self.encode_func}({self.number}, static_cast<uint32_t>(this->{self.field_name}));"
def dump(self, name: str) -> str:
return f"out.append(proto_enum_to_string<{self.cpp_type}>({name}));"
return f"out.append_p(proto_enum_to_string<{self.cpp_type}>({name}));"
def dump_field_value(self, value: str) -> str:
# Enums need explicit cast for the template
@@ -1401,15 +1401,15 @@ def _generate_array_dump_content(
# Check if underlying type can use dump_field
if is_const_char_ptr:
# Special case for const char* - use it directly
o += f' dump_field(out, "{name}", it, 4);\n'
o += f' dump_field(out, ESPHOME_PSTR("{name}"), it, 4);\n'
elif ti.can_use_dump_field():
# For types that have dump_field overloads, use them with extra indent
# std::vector<bool> iterators return proxy objects, need explicit cast
value_expr = "static_cast<bool>(it)" if is_bool else ti.dump_field_value("it")
o += f' dump_field(out, "{name}", {value_expr}, 4);\n'
o += f' dump_field(out, ESPHOME_PSTR("{name}"), {value_expr}, 4);\n'
else:
# For complex types (messages, bytes), use the old pattern
o += f' out.append(" {name}: ");\n'
o += f' out.append(4, \' \').append_p(ESPHOME_PSTR("{name}")).append(": ");\n'
o += indent(ti.dump("it")) + "\n"
o += ' out.append("\\n");\n'
o += "}"
@@ -1618,9 +1618,9 @@ class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType):
o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n"
# Check if underlying type can use dump_field
if self._ti.can_use_dump_field():
o += f' dump_field(out, "{self.name}", {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n'
o += f' dump_field(out, ESPHOME_PSTR("{self.name}"), {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n'
else:
o += f' out.append(" {self.name}: ");\n'
o += f' out.append(4, \' \').append_p(ESPHOME_PSTR("{self.name}")).append(": ");\n'
o += indent(self._ti.dump(f"this->{self.field_name}[i]")) + "\n"
o += ' out.append("\\n");\n'
o += "}"
@@ -2098,9 +2098,9 @@ def build_enum_type(desc, enum_ifdef_map) -> tuple[str, str, str]:
dump_cpp += " switch (value) {\n"
for v in desc.value:
dump_cpp += f" case enums::{v.name}:\n"
dump_cpp += f' return "{v.name}";\n'
dump_cpp += f' return ESPHOME_PSTR("{v.name}");\n'
dump_cpp += " default:\n"
dump_cpp += ' return "UNKNOWN";\n'
dump_cpp += ' return ESPHOME_PSTR("UNKNOWN");\n'
dump_cpp += " }\n"
dump_cpp += "}\n"
@@ -2182,7 +2182,7 @@ def build_message_type(
public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP")
snake_name = camel_to_snake(desc.name)
public_content.append(
f'const char *message_name() const override {{ return "{snake_name}"; }}'
f'const LogString *message_name() const override {{ return LOG_STR("{snake_name}"); }}'
)
public_content.append("#endif")
@@ -2350,7 +2350,7 @@ def build_message_type(
o += "}\n"
cpp += o
# Generate the decode() declaration in header (public method)
prot = "void decode(const uint8_t *buffer, size_t length) override;"
prot = "void decode(const uint8_t *buffer, size_t length);"
public_content.append(prot)
# Only generate encode method if this message needs encoding and has fields
@@ -2390,12 +2390,12 @@ def build_message_type(
if dump:
# Always use MessageDumpHelper for consistent output formatting
dump_impl += "\n"
dump_impl += f' MessageDumpHelper helper(out, "{desc.name}");\n'
dump_impl += f' MessageDumpHelper helper(out, ESPHOME_PSTR("{desc.name}"));\n'
dump_impl += indent("\n".join(dump)) + "\n"
dump_impl += " return out.c_str();\n"
else:
dump_impl += "\n"
dump_impl += f' out.append("{desc.name} {{}}");\n'
dump_impl += f' out.append_p(ESPHOME_PSTR("{desc.name} {{}}"));\n'
dump_impl += " return out.c_str();\n"
dump_impl += "}\n"
@@ -2683,7 +2683,7 @@ def build_service_message_type(
is_empty = not has_fields
if is_empty:
EMPTY_MESSAGES.add(mt.name)
hout += f"virtual void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n"
hout += f"void {func}({'' if is_empty else f'const {mt.name} &value'}){{}};\n"
case = ""
if not is_empty:
case += f"{mt.name} msg;\n"
@@ -2782,6 +2782,7 @@ namespace esphome::api {
dump_cpp += """\
#include "api_pb2.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include <cinttypes>
@@ -2789,6 +2790,21 @@ namespace esphome::api {
namespace esphome::api {
#ifdef USE_ESP8266
// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site
void DumpBuffer::append_p_esp8266(const char *str) {
size_t len = strlen_P(str);
size_t space = CAPACITY - 1 - pos_;
if (len > space)
len = space;
if (len > 0) {
memcpy_P(buf_ + pos_, str, len);
pos_ += len;
buf_[pos_] = '\\0';
}
}
#endif
// Helper function to append a quoted string, handling empty StringRef
static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) {
out.append("'");
@@ -2799,8 +2815,9 @@ static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) {
}
// Common helpers for dump_field functions
// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms)
static inline void append_field_prefix(DumpBuffer &out, const char *field_name, int indent) {
out.append(indent, ' ').append(field_name).append(": ");
out.append(indent, ' ').append_p(field_name).append(": ");
}
static inline void append_uint(DumpBuffer &out, uint32_t value) {
@@ -2808,10 +2825,11 @@ static inline void append_uint(DumpBuffer &out, uint32_t value) {
}
// RAII helper for message dump formatting
// message_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms)
class MessageDumpHelper {
public:
MessageDumpHelper(DumpBuffer &out, const char *message_name) : out_(out) {
out_.append(message_name);
out_.append_p(message_name);
out_.append(" {\\n");
}
~MessageDumpHelper() { out_.append(" }"); }
@@ -2821,6 +2839,10 @@ class MessageDumpHelper {
};
// Helper functions to reduce code duplication in dump methods
// field_name parameters are PROGMEM pointers (flash on ESP8266, regular pointers on other platforms)
// Not all overloads are used in every build (depends on enabled components)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\\n", value));
@@ -2865,21 +2887,23 @@ static void dump_field(DumpBuffer &out, const char *field_name, const char *valu
out.append("\\n");
}
template<typename T>
static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) {
// proto_enum_to_string returns PROGMEM pointers, so use append_p
template<typename T> static void dump_field(DumpBuffer &out, const char *field_name, T value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append(proto_enum_to_string<T>(value));
out.append_p(proto_enum_to_string<T>(value));
out.append("\\n");
}
// Helper for bytes fields - uses stack buffer to avoid heap allocation
// Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer
// field_name is a PROGMEM pointer (flash on ESP8266, regular pointer on other platforms)
static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) {
char hex_buf[format_hex_pretty_size(160)];
append_field_prefix(out, field_name, indent);
format_hex_pretty_to(hex_buf, data, len);
out.append(hex_buf).append("\\n");
}
#pragma GCC diagnostic pop
"""
@@ -3035,6 +3059,7 @@ namespace esphome::api {
cpp = FILE_HEADER
cpp += """\
#include "api_pb2_service.h"
#include "api_connection.h"
#include "esphome/core/log.h"
namespace esphome::api {
@@ -3045,13 +3070,13 @@ static const char *const TAG = "api.service";
class_name = "APIServerConnectionBase"
hpp += f"class {class_name} : public ProtoService {{\n"
hpp += f"class {class_name} {{\n"
hpp += " public:\n"
# Add logging helper method declarations
hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
hpp += " protected:\n"
hpp += " void log_send_message_(const char *name, const char *dump);\n"
hpp += " void log_send_message_(const LogString *name, const char *dump);\n"
hpp += (
" void log_receive_message_(const LogString *name, const ProtoMessage &msg);\n"
)
@@ -3064,10 +3089,8 @@ static const char *const TAG = "api.service";
# Add logging helper method implementations to cpp
cpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
cpp += (
f"void {class_name}::log_send_message_(const char *name, const char *dump) {{\n"
)
cpp += ' ESP_LOGVV(TAG, "send_message %s: %s", name, dump);\n'
cpp += f"void {class_name}::log_send_message_(const LogString *name, const char *dump) {{\n"
cpp += ' ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump);\n'
cpp += "}\n"
cpp += f"void {class_name}::log_receive_message_(const LogString *name, const ProtoMessage &msg) {{\n"
cpp += " DumpBuffer dump_buf;\n"
@@ -3138,11 +3161,11 @@ static const char *const TAG = "api.service";
result += "#endif\n"
return result
# Generate read_message with auth check before dispatch
hpp += " protected:\n"
hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n"
# Generate read_message_ as APIConnection method (not base class) so the compiler
# can devirtualize and inline the on_* handler calls within the same class.
# APIConnection declares this method in api_connection.h.
out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n"
out = "void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {\n"
# Auth check block before dispatch switch
out += " // Check authentication/connection requirements\n"
+17
View File
@@ -890,6 +890,22 @@ def lint_no_powf_in_core(fname, match):
)
@lint_re_check(
r"[^\w]std\s*::\s*bind\s*\(" + CPP_RE_EOL,
include=cpp_include,
)
def lint_no_std_bind(fname, match):
return (
f"{highlight('std::bind()')} is not allowed in new ESPHome code. "
f"Lambdas are clearer, produce smaller binaries, and are more likely to fit within "
f"the {highlight('std::function')} small-buffer optimization (avoiding heap allocation).\n"
f"Please use a lambda instead.\n"
f" Before: {highlight('std::bind(&Class::method, this, std::placeholders::_1)')}\n"
f" After: {highlight('[this](auto arg) { this->method(arg); }')}\n"
f"(If strictly necessary, add `// NOLINT` to the end of the line)"
)
LOG_MULTILINE_RE = re.compile(r"ESP_LOG\w+\s*\(.*?;", re.DOTALL)
LOG_BAD_CONTINUATION_RE = re.compile(r'\\n(?:[^ \\"\r\n\t]|"\s*\n\s*"[^ \\])')
LOG_PERCENT_S_CONTINUATION_RE = re.compile(r'\\n(?:%s|"\s*\n\s*"%s)')
@@ -947,6 +963,7 @@ def lint_log_multiline_continuation(fname, content):
"esphome/components/nextion/nextion_base.h",
"esphome/components/select/select.h",
"esphome/components/sensor/sensor.h",
"esphome/components/spi/spi.h",
"esphome/components/stepper/stepper.h",
"esphome/components/switch/switch.h",
"esphome/components/text/text.h",