[modbus] Add a compile-time register value decoder (#18863)

This commit is contained in:
Bonne Eggleston
2026-08-28 14:54:31 -05:00
committed by GitHub
parent 0dce7f4845
commit 246670e22b
2 changed files with 85 additions and 0 deletions
@@ -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) {