diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index a070ce250c..486064da01 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -473,6 +473,51 @@ inline int64_t payload_to_number(const std::vector &data, SensorValueTy */ std::optional registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type); +/// Combine two register words into a 32-bit value. +constexpr uint32_t registers_to_uint32(uint16_t high_word, uint16_t low_word) { + return (static_cast(high_word) << 16) | low_word; +} + +// Always false, whatever the type: it exists only to make the static_assert below depend on the +// template argument. Not a queryable trait. +template inline constexpr bool VALUE_TYPE_SUPPORTED = false; + +/** Decode one value whose type is known at compile time, from registers in host byte order. + * Unlike registers_to_number(), the type is a template argument, so only the one decode is compiled + * and the caller gets the value's natural type back rather than an int64_t. The "_R" types take the + * low word first; the rest take the high word first. + * Supports the WORD, DWORD and FP32 types, including their _S and _R forms; the QWORD types are + * out of scope and fail to compile, so use registers_to_number() for those. + * Use register_width_for() for the number of registers the caller must supply. + * Note that the FP32 branches are only usable in a constant expression where std::bit_cast is + * available; elsewhere bit_cast falls back to a non-constexpr memcpy (see core/helpers.h). + */ +template constexpr auto registers_to_value(const uint16_t *registers) { + if constexpr (VALUE_TYPE == SensorValueType::U_WORD) { + return registers[0]; + } else if constexpr (VALUE_TYPE == SensorValueType::S_WORD) { + return static_cast(registers[0]); + } else if constexpr (VALUE_TYPE == SensorValueType::U_WORD_S) { + return byteswap(registers[0]); + } else if constexpr (VALUE_TYPE == SensorValueType::S_WORD_S) { + return static_cast(byteswap(registers[0])); + } else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD) { + return registers_to_uint32(registers[0], registers[1]); + } else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD_R) { + return registers_to_uint32(registers[1], registers[0]); + } else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD) { + return static_cast(registers_to_uint32(registers[0], registers[1])); + } else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD_R) { + return static_cast(registers_to_uint32(registers[1], registers[0])); + } else if constexpr (VALUE_TYPE == SensorValueType::FP32) { + return bit_cast(registers_to_uint32(registers[0], registers[1])); + } else if constexpr (VALUE_TYPE == SensorValueType::FP32_R) { + return bit_cast(registers_to_uint32(registers[1], registers[0])); + } else { + static_assert(VALUE_TYPE_SUPPORTED, "registers_to_value() does not support this value type"); + } +} + /// The widest standard numeric value (a QWORD) spans 4 registers, so one entity value never writes more. static constexpr uint16_t MAX_FEW_REGISTERS = 4; diff --git a/tests/components/modbus/modbus_helpers_test.cpp b/tests/components/modbus/modbus_helpers_test.cpp index 87af49710f..21c264ea69 100644 --- a/tests/components/modbus/modbus_helpers_test.cpp +++ b/tests/components/modbus/modbus_helpers_test.cpp @@ -432,6 +432,46 @@ TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) { EXPECT_FALSE(registers_to_number(registers, 1, SensorValueType::U_DWORD).has_value()); } +// --- registers_to_value ---------------------------------------------------- +// The compile-time decoder must agree with the runtime one for every type it supports, +// so the two implementations cannot drift apart. + +template void expect_matches_registers_to_number(const uint16_t *registers) { + const auto expected = registers_to_number(registers, register_width_for(VALUE_TYPE), VALUE_TYPE); + // Plain control flow rather than ASSERT_TRUE: the optional analysis does not see through the macro. + if (!expected.has_value()) { + ADD_FAILURE() << "registers_to_number() returned no value for value_type=" << static_cast(VALUE_TYPE); + return; + } + const int64_t number = expected.value(); + if constexpr (VALUE_TYPE == SensorValueType::FP32 || VALUE_TYPE == SensorValueType::FP32_R) { + EXPECT_FLOAT_EQ(registers_to_value(registers), bit_cast(static_cast(number))) + << "value_type=" << static_cast(VALUE_TYPE); + } else { + EXPECT_EQ(static_cast(registers_to_value(registers)), number) + << "value_type=" << static_cast(VALUE_TYPE); + } +} + +TEST(ModbusHelpersTest, RegistersToValueMatchesRegistersToNumber) { + // A high bit in each word exercises sign handling and word order together. + const uint16_t registers[] = {0x8001, 0xFE02}; + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); +} + +TEST(ModbusHelpersTest, RegistersToUint32CombinesWordsHighFirst) { + EXPECT_EQ(registers_to_uint32(0x1234, 0x5678), 0x12345678u); +} + // --- packed bit helpers ------------------------------------------------------ TEST(ModbusHelpersTest, PackBitsAppendsToContainer) {