Reduce binary size with field-level conditional compilation for protobuf messages (#9473)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
J. Nick Koston
2025-07-16 18:36:26 +12:00
committed by GitHub
co-authored by pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
parent f3c0c0c00c
commit 3ab1ee7a04
8 changed files with 834 additions and 110 deletions
File diff suppressed because it is too large Load Diff
@@ -1463,7 +1463,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
#endif
resp.name = App.get_name();
resp.friendly_name = App.get_friendly_name();
#ifdef USE_AREAS
resp.suggested_area = App.get_area();
#endif
resp.mac_address = get_mac_address_pretty();
resp.esphome_version = ESPHOME_VERSION;
resp.compilation_time = App.get_compilation_time();
+3 -1
View File
@@ -286,8 +286,10 @@ class APIConnection : public APIServerConnection {
if (entity->has_own_name())
response.name = entity->get_name();
// Set common EntityBase properties
// Set common EntityBase properties
#ifdef USE_ENTITY_ICON
response.icon = entity->get_icon();
#endif
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES
+4
View File
@@ -23,3 +23,7 @@ extend google.protobuf.MessageOptions {
optional bool no_delay = 1040 [default=false];
optional string base_class = 1041;
}
extend google.protobuf.FieldOptions {
optional string field_ifdef = 1042;
}
File diff suppressed because it is too large Load Diff
+28
View File
@@ -491,22 +491,50 @@ class DeviceInfoResponse : public ProtoMessage {
std::string esphome_version{};
std::string compilation_time{};
std::string model{};
#ifdef USE_DEEP_SLEEP
bool has_deep_sleep{false};
#endif
#ifdef ESPHOME_PROJECT_NAME
std::string project_name{};
#endif
#ifdef ESPHOME_PROJECT_NAME
std::string project_version{};
#endif
#ifdef USE_WEBSERVER
uint32_t webserver_port{0};
#endif
#ifdef USE_BLUETOOTH_PROXY
uint32_t legacy_bluetooth_proxy_version{0};
#endif
#ifdef USE_BLUETOOTH_PROXY
uint32_t bluetooth_proxy_feature_flags{0};
#endif
std::string manufacturer{};
std::string friendly_name{};
#ifdef USE_VOICE_ASSISTANT
uint32_t legacy_voice_assistant_version{0};
#endif
#ifdef USE_VOICE_ASSISTANT
uint32_t voice_assistant_feature_flags{0};
#endif
#ifdef USE_AREAS
std::string suggested_area{};
#endif
#ifdef USE_BLUETOOTH_PROXY
std::string bluetooth_mac_address{};
#endif
#ifdef USE_API_NOISE
bool api_encryption_supported{false};
#endif
#ifdef USE_DEVICES
std::vector<DeviceInfo> devices{};
#endif
#ifdef USE_AREAS
std::vector<AreaInfo> areas{};
#endif
#ifdef USE_AREAS
AreaInfo area{};
#endif
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
File diff suppressed because it is too large Load Diff
+69 -9
View File
@@ -75,6 +75,30 @@ def indent(text: str, padding: str = " ") -> str:
return "\n".join(indent_list(text, padding))
def wrap_with_ifdef(content: str | list[str], ifdef: str | None) -> list[str]:
"""Wrap content with #ifdef directives if ifdef is provided.
Args:
content: Single string or list of strings to wrap
ifdef: The ifdef condition, or None to skip wrapping
Returns:
List of strings with ifdef wrapping if needed
"""
if not ifdef:
if isinstance(content, str):
return [content]
return content
result = [f"#ifdef {ifdef}"]
if isinstance(content, str):
result.append(content)
else:
result.extend(content)
result.append("#endif")
return result
def camel_to_snake(name: str) -> str:
# https://stackoverflow.com/a/1176023
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
@@ -1064,26 +1088,62 @@ def build_message_type(
# Skip field declarations for fields that are in the base class
# but include their encode/decode logic
if field.name not in common_field_names:
protected_content.extend(ti.protected_content)
public_content.extend(ti.public_content)
# Check for field_ifdef option
field_ifdef = None
if field.options.HasExtension(pb.field_ifdef):
field_ifdef = field.options.Extensions[pb.field_ifdef]
if ti.protected_content:
protected_content.extend(
wrap_with_ifdef(ti.protected_content, field_ifdef)
)
if ti.public_content:
public_content.extend(wrap_with_ifdef(ti.public_content, field_ifdef))
# Only collect encode logic if this message needs it
if needs_encode:
encode.append(ti.encode_content)
size_calc.append(ti.get_size_calculation(f"this->{ti.field_name}"))
# Check for field_ifdef option
field_ifdef = None
if field.options.HasExtension(pb.field_ifdef):
field_ifdef = field.options.Extensions[pb.field_ifdef]
encode.extend(wrap_with_ifdef(ti.encode_content, field_ifdef))
size_calc.extend(
wrap_with_ifdef(
ti.get_size_calculation(f"this->{ti.field_name}"), field_ifdef
)
)
# Only collect decode methods if this message needs them
if needs_decode:
# Check for field_ifdef option for decode as well
field_ifdef = None
if field.options.HasExtension(pb.field_ifdef):
field_ifdef = field.options.Extensions[pb.field_ifdef]
if ti.decode_varint_content:
decode_varint.append(ti.decode_varint_content)
decode_varint.extend(
wrap_with_ifdef(ti.decode_varint_content, field_ifdef)
)
if ti.decode_length_content:
decode_length.append(ti.decode_length_content)
decode_length.extend(
wrap_with_ifdef(ti.decode_length_content, field_ifdef)
)
if ti.decode_32bit_content:
decode_32bit.append(ti.decode_32bit_content)
decode_32bit.extend(
wrap_with_ifdef(ti.decode_32bit_content, field_ifdef)
)
if ti.decode_64bit_content:
decode_64bit.append(ti.decode_64bit_content)
decode_64bit.extend(
wrap_with_ifdef(ti.decode_64bit_content, field_ifdef)
)
if ti.dump_content:
dump.append(ti.dump_content)
# Check for field_ifdef option for dump as well
field_ifdef = None
if field.options.HasExtension(pb.field_ifdef):
field_ifdef = field.options.Extensions[pb.field_ifdef]
dump.extend(wrap_with_ifdef(ti.dump_content, field_ifdef))
cpp = ""
if decode_varint: