mirror of
https://github.com/esphome/esphome.git
synced 2026-08-30 17:46:01 +00:00
[modbus] Add a compile-time register value decoder (#18863)
This commit is contained in:
@@ -473,6 +473,51 @@ inline int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueTy
|
||||
*/
|
||||
std::optional<int64_t> 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<uint32_t>(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<SensorValueType> 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<SensorValueType VALUE_TYPE> 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<int16_t>(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<int16_t>(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<int32_t>(registers_to_uint32(registers[0], registers[1]));
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD_R) {
|
||||
return static_cast<int32_t>(registers_to_uint32(registers[1], registers[0]));
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::FP32) {
|
||||
return bit_cast<float>(registers_to_uint32(registers[0], registers[1]));
|
||||
} else if constexpr (VALUE_TYPE == SensorValueType::FP32_R) {
|
||||
return bit_cast<float>(registers_to_uint32(registers[1], registers[0]));
|
||||
} else {
|
||||
static_assert(VALUE_TYPE_SUPPORTED<VALUE_TYPE>, "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;
|
||||
|
||||
|
||||
@@ -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<SensorValueType VALUE_TYPE> 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<int>(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<VALUE_TYPE>(registers), bit_cast<float>(static_cast<uint32_t>(number)))
|
||||
<< "value_type=" << static_cast<int>(VALUE_TYPE);
|
||||
} else {
|
||||
EXPECT_EQ(static_cast<int64_t>(registers_to_value<VALUE_TYPE>(registers)), number)
|
||||
<< "value_type=" << static_cast<int>(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<SensorValueType::U_WORD>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::S_WORD>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::U_WORD_S>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::S_WORD_S>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::U_DWORD>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::U_DWORD_R>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::S_DWORD>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::S_DWORD_R>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::FP32>(registers);
|
||||
expect_matches_registers_to_number<SensorValueType::FP32_R>(registers);
|
||||
}
|
||||
|
||||
TEST(ModbusHelpersTest, RegistersToUint32CombinesWordsHighFirst) {
|
||||
EXPECT_EQ(registers_to_uint32(0x1234, 0x5678), 0x12345678u);
|
||||
}
|
||||
|
||||
// --- packed bit helpers ------------------------------------------------------
|
||||
|
||||
TEST(ModbusHelpersTest, PackBitsAppendsToContainer) {
|
||||
|
||||
Reference in New Issue
Block a user