From 856a0b0c8379e2bff785a8a8c387633cfc251c6e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 10:04:59 -1000 Subject: [PATCH 1/3] [api] Use integer comparison for float zero checks in protobuf encoding Replace floating-point comparison (value != 0.0f) with integer bit-pattern check via a shared float_to_raw() helper in both encode_float() and calc_float(). This avoids software float library calls on platforms without hardware FPU (ESP8266, some LibreTiny chips) and ensures both functions use identical zero-check logic so size calculation always matches encoding. --- esphome/components/api/proto.h | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index d6e993d3a5..dff2d877ee 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -24,6 +24,10 @@ constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded me constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag +// Reinterpret float bits as uint32_t without floating-point comparison. +// Used by both encode_float() and calc_float() to ensure identical zero checks. +inline constexpr uint32_t float_to_raw(float value) { return __builtin_bit_cast(uint32_t, value); } + // Helper functions for ZigZag encoding/decoding inline constexpr uint32_t encode_zigzag32(int32_t value) { return (static_cast(value) << 1) ^ (static_cast(value >> 31)); @@ -299,15 +303,10 @@ class ProtoWriteBuffer { // 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support // is needed in the future, the necessary encoding/decoding functions must be added. void encode_float(uint32_t field_id, float value, bool force = false) { - if (value == 0.0f && !force) + uint32_t raw = float_to_raw(value); + if (raw == 0 && !force) return; - - union { - float value; - uint32_t raw; - } val{}; - val.value = value; - this->encode_fixed32(field_id, val.raw); + this->encode_fixed32(field_id, raw); } void encode_int32(uint32_t field_id, int32_t value, bool force = false) { if (value < 0) { @@ -601,7 +600,7 @@ class ProtoSize { static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; } static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; } static constexpr uint32_t calc_float(uint32_t field_id_size, float value) { - return value != 0.0f ? field_id_size + 4 : 0; + return float_to_raw(value) != 0 ? field_id_size + 4 : 0; } static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) { return value ? field_id_size + 4 : 0; From e7d1d08fa1d71d114e57f4da39bec3fdbbea989e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 10:12:04 -1000 Subject: [PATCH 2/3] Use memcpy instead of __builtin_bit_cast for ESP8266 compat --- esphome/components/api/proto.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index dff2d877ee..c05ce78f43 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -26,7 +26,11 @@ constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type // Reinterpret float bits as uint32_t without floating-point comparison. // Used by both encode_float() and calc_float() to ensure identical zero checks. -inline constexpr uint32_t float_to_raw(float value) { return __builtin_bit_cast(uint32_t, value); } +inline uint32_t float_to_raw(float value) { + uint32_t raw; + memcpy(&raw, &value, sizeof(raw)); + return raw; +} // Helper functions for ZigZag encoding/decoding inline constexpr uint32_t encode_zigzag32(int32_t value) { @@ -599,7 +603,7 @@ class ProtoSize { } static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; } static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; } - static constexpr uint32_t calc_float(uint32_t field_id_size, float value) { + static uint32_t calc_float(uint32_t field_id_size, float value) { return float_to_raw(value) != 0 ? field_id_size + 4 : 0; } static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) { From 85be827bb0a34c89a051711d7760cb151c0bc0f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Mar 2026 10:16:37 -1000 Subject: [PATCH 3/3] =?UTF-8?q?Use=20union=20instead=20of=20memcpy=20in=20?= =?UTF-8?q?float=5Fto=5Fraw=20=E2=80=94=20xtensa-gcc=20doesn't=20optimize?= =?UTF-8?q?=20memcpy=20in=20all=20contexts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esphome/components/api/proto.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index c05ce78f43..5a49ad2d37 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -27,9 +27,12 @@ constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type // Reinterpret float bits as uint32_t without floating-point comparison. // Used by both encode_float() and calc_float() to ensure identical zero checks. inline uint32_t float_to_raw(float value) { - uint32_t raw; - memcpy(&raw, &value, sizeof(raw)); - return raw; + union { + float f; + uint32_t u; + } v; + v.f = value; + return v.u; } // Helper functions for ZigZag encoding/decoding