From 4e3b2abd5fc4282c5163bf62fabcc7801493e144 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 20:08:29 -0600 Subject: [PATCH 1/2] Fix ODR violation: generate api_pb2_defines.h for consistent ProtoVarInt layout USE_API_VARINT64 was only defined in api_pb2.h, but proto.cpp (where decode() and parse_wide() live) includes proto.h directly. This caused: 1. parse_wide() not compiled in proto.cpp (guarded by #ifdef) 2. decode() used 32-bit-only parse(), returning {} for varints > 5 bytes 3. ODR violation: ProtoVarInt was 4 bytes in proto.cpp but 8 bytes in api_pb2*.cpp translation units For bluetooth_proxy, 48-bit BLE addresses encode as 7-byte varints. The failed parse caused decode() to return early, leaving request_type at its default value of 0 (BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT), producing spurious "V1 connections removed" errors. Fix: Generate api_pb2_defines.h with the USE_API_VARINT64 define and include it from proto.h, ensuring all translation units see a consistent ProtoVarInt layout. --- esphome/components/api/api_pb2.h | 4 ---- esphome/components/api/api_pb2_defines.h | 8 ++++++++ esphome/components/api/proto.h | 1 + script/api_protobuf/api_protobuf.py | 26 +++++++++++++++--------- 4 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 esphome/components/api/api_pb2_defines.h diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 8424f3b629..651c3b3d98 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2,10 +2,6 @@ // See script/api_protobuf/api_protobuf.py #pragma once -#include "esphome/core/defines.h" -#ifdef USE_BLUETOOTH_PROXY -#define USE_API_VARINT64 -#endif #include "esphome/core/string_ref.h" #include "proto.h" diff --git a/esphome/components/api/api_pb2_defines.h b/esphome/components/api/api_pb2_defines.h new file mode 100644 index 0000000000..6f60f24f99 --- /dev/null +++ b/esphome/components/api/api_pb2_defines.h @@ -0,0 +1,8 @@ +// This file was automatically generated with a tool. +// See script/api_protobuf/api_protobuf.py +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_BLUETOOTH_PROXY +#define USE_API_VARINT64 +#endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 69b0440e38..ce3f9e1808 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -1,5 +1,6 @@ #pragma once +#include "api_pb2_defines.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 2324708324..5d3c61fcda 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2593,22 +2593,28 @@ def main() -> None: ) # Find the ifdef guard for 64-bit varint fields (int64/uint64/sint64). - # Emitted before proto.h so parse_wide_() and 64-bit accessors are available. + # Generated into api_pb2_defines.h so proto.h can include it, ensuring + # consistent ProtoVarInt layout across all translation units. has_varint64, varint64_guard = get_varint64_ifdef(file, message_ifdef_map) + # Generate api_pb2_defines.h — included by proto.h to ensure all translation + # units see USE_API_VARINT64 consistently (avoids ODR violations in ProtoVarInt). + defines_content = FILE_HEADER + defines_content += "#pragma once\n\n" + defines_content += '#include "esphome/core/defines.h"\n' + if has_varint64: + defines_content += "\n".join( + wrap_with_ifdef(["#define USE_API_VARINT64"], varint64_guard) + ) + defines_content += "\n" + + with open(root / "api_pb2_defines.h", "w", encoding="utf-8") as f: + f.write(defines_content) + content = FILE_HEADER content += """\ #pragma once -#include "esphome/core/defines.h" -""" - if has_varint64: - content += "\n".join( - wrap_with_ifdef(["#define USE_API_VARINT64"], varint64_guard) - ) - content += "\n" - - content += """\ #include "esphome/core/string_ref.h" #include "proto.h" From 21b9a4139abe934869149b0f6c3db7b9a3df407c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 20:23:17 -0600 Subject: [PATCH 2/2] Address review feedback: fix unconditional guard bug, add ifndef guard, clarify comment - Fix get_varint64_ifdef() to return unconditional guard when any 64-bit varint field has no ifdef (None in ifdefs set) - Wrap USE_API_VARINT64 define with #ifndef to avoid redefinition warnings when esphome/core/defines.h also defines it (test/IDE builds) - Clarify proto.h comment about uint32_t shift behavior at byte 4 - Add namespace to generated api_pb2_defines.h for linter compliance --- esphome/components/api/api_pb2_defines.h | 4 ++++ esphome/components/api/proto.h | 2 +- script/api_protobuf/api_protobuf.py | 13 ++++++++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_pb2_defines.h b/esphome/components/api/api_pb2_defines.h index 6f60f24f99..8ebd60fb5d 100644 --- a/esphome/components/api/api_pb2_defines.h +++ b/esphome/components/api/api_pb2_defines.h @@ -4,5 +4,9 @@ #include "esphome/core/defines.h" #ifdef USE_BLUETOOTH_PROXY +#ifndef USE_API_VARINT64 #define USE_API_VARINT64 #endif +#endif + +namespace esphome::api {} // namespace esphome::api diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index ce3f9e1808..87fce485f1 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -119,7 +119,7 @@ class ProtoVarInt { } // 32-bit phase: process remaining bytes with native 32-bit shifts. // Without USE_API_VARINT64: cover bytes 1-4 (shifts 7, 14, 21, 28) — the uint32_t - // shift at byte 4 truncates upper bits but those are always zero for valid uint32 values. + // shift at byte 4 (shift by 28) may lose bits 32-34, but those are always zero for valid uint32 values. // With USE_API_VARINT64: cover bytes 1-3 (shifts 7, 14, 21) so parse_wide handles // byte 4+ with full 64-bit arithmetic (avoids truncating values > UINT32_MAX). uint32_t result32 = buffer[0] & 0x7F; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 5d3c61fcda..c862983fc4 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1929,6 +1929,9 @@ def get_varint64_ifdef( } if not ifdefs: return False, None + if None in ifdefs: + # At least one 64-bit varint field is unconditional, so the guard must be unconditional. + return True, None ifdefs.discard(None) return True, ifdefs.pop() if len(ifdefs) == 1 else None @@ -2603,10 +2606,14 @@ def main() -> None: defines_content += "#pragma once\n\n" defines_content += '#include "esphome/core/defines.h"\n' if has_varint64: - defines_content += "\n".join( - wrap_with_ifdef(["#define USE_API_VARINT64"], varint64_guard) - ) + lines = [ + "#ifndef USE_API_VARINT64", + "#define USE_API_VARINT64", + "#endif", + ] + defines_content += "\n".join(wrap_with_ifdef(lines, varint64_guard)) defines_content += "\n" + defines_content += "\nnamespace esphome::api {} // namespace esphome::api\n" with open(root / "api_pb2_defines.h", "w", encoding="utf-8") as f: f.write(defines_content)