mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
375 lines
18 KiB
C++
375 lines
18 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include "esphome/components/modbus/modbus_helpers.h"
|
|
|
|
namespace esphome::modbus::helpers {
|
|
|
|
using FC = FunctionCode;
|
|
|
|
// --- server_frame_length ---------------------------------------------------
|
|
// Frame layout: address(1) + function(1) + ... + CRC(2). Fixtures borrowed from
|
|
// tests/integration/fixtures/uart_mock_modbus.yaml.
|
|
|
|
TEST(ModbusServerFrameLength, TooShortReturnsMinimum) {
|
|
const uint8_t frame[] = {0x01};
|
|
EXPECT_EQ(server_frame_length(frame, 1), MIN_FRAME_SIZE);
|
|
}
|
|
|
|
TEST(ModbusServerFrameLength, ReadHoldingUsesByteCount) {
|
|
// inject_rx for basic_register: 2 data bytes -> 5 + 2 = 7
|
|
const uint8_t frame[] = {0x01, 0x03, 0x02, 0x01, 0x03, 0xF9, 0xD5};
|
|
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 7);
|
|
}
|
|
|
|
TEST(ModbusServerFrameLength, ReadByteCountCappedAtMax) {
|
|
const uint8_t frame[] = {0x01, 0x03, 0xFF}; // claim 255 bytes
|
|
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 5 + MAX_NUM_OF_REGISTERS_TO_READ * 2);
|
|
}
|
|
|
|
TEST(ModbusServerFrameLength, ReadMissingByteCountReturnsHeaderOnly) {
|
|
const uint8_t frame[] = {0x01, 0x03};
|
|
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 5);
|
|
}
|
|
|
|
TEST(ModbusServerFrameLength, ExceptionResponse) {
|
|
// exception_response fixture: function code 0x83 has the exception bit set
|
|
const uint8_t frame[] = {0x01, 0x83, 0x02, 0xC0, 0xF1};
|
|
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 5);
|
|
}
|
|
|
|
TEST(ModbusServerFrameLength, WriteResponsesAreFixed) {
|
|
for (FC fc :
|
|
{FC::WRITE_SINGLE_COIL, FC::WRITE_SINGLE_REGISTER, FC::WRITE_MULTIPLE_COILS, FC::WRITE_MULTIPLE_REGISTERS}) {
|
|
const uint8_t frame[] = {0x01, static_cast<uint8_t>(fc)};
|
|
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 8) << "fc=" << static_cast<int>(fc);
|
|
}
|
|
}
|
|
|
|
TEST(ModbusServerFrameLength, MiscFixedAndUnknown) {
|
|
const uint8_t mask[] = {0x01, static_cast<uint8_t>(FC::MASK_WRITE_REGISTER)};
|
|
const uint8_t fifo[] = {0x01, static_cast<uint8_t>(FC::READ_FIFO_QUEUE)};
|
|
const uint8_t unknown[] = {0x01, 0x42};
|
|
EXPECT_EQ(server_frame_length(mask, sizeof(mask)), 10);
|
|
EXPECT_EQ(server_frame_length(fifo, sizeof(fifo)), 6);
|
|
EXPECT_EQ(server_frame_length(unknown, sizeof(unknown)), MIN_FRAME_SIZE);
|
|
}
|
|
|
|
// --- client_frame_length ---------------------------------------------------
|
|
|
|
TEST(ModbusClientFrameLength, TooShortReturnsMinimum) {
|
|
const uint8_t frame[] = {0x01};
|
|
EXPECT_EQ(client_frame_length(frame, 1), MIN_FRAME_SIZE);
|
|
}
|
|
|
|
TEST(ModbusClientFrameLength, ReadAndWriteSingleAreFixed) {
|
|
// basic_register request fixture is a read-holding request -> 8 bytes
|
|
const uint8_t read[] = {0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A};
|
|
EXPECT_EQ(client_frame_length(read, sizeof(read)), 8);
|
|
for (FC fc : {FC::READ_COILS, FC::READ_DISCRETE_INPUTS, FC::READ_INPUT_REGISTERS, FC::WRITE_SINGLE_COIL,
|
|
FC::WRITE_SINGLE_REGISTER}) {
|
|
const uint8_t frame[] = {0x01, static_cast<uint8_t>(fc)};
|
|
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 8) << "fc=" << static_cast<int>(fc);
|
|
}
|
|
}
|
|
|
|
TEST(ModbusClientFrameLength, WriteMultipleUsesByteCount) {
|
|
// write 2 registers (4 data bytes): addr(2)+qty(2)+count(1) then data; count is frame[6]
|
|
const uint8_t frame[] = {0x01, 0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x0B, 0x00, 0x16};
|
|
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9 + 4);
|
|
}
|
|
|
|
TEST(ModbusClientFrameLength, WriteMultipleByteCountCapped) {
|
|
const uint8_t frame[] = {0x01, 0x0F, 0x00, 0x00, 0x00, 0x02, 0xFF};
|
|
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9 + MAX_NUM_OF_REGISTERS_TO_WRITE * 2);
|
|
}
|
|
|
|
TEST(ModbusClientFrameLength, ReadWriteMultipleByteCountCappedAtSpecLimit) {
|
|
// FC 0x17's write byte count caps at the spec 6.17 limit of 121 registers (242 bytes), deliberately
|
|
// tighter than FC 0x10's 123, so a corrupt byte count cannot make the parser wait past the real frame.
|
|
const uint8_t pdu[] = {0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xFF}; // claims 255 bytes
|
|
EXPECT_EQ(client_pdu_length(pdu, sizeof(pdu)), 10 + MAX_NUM_OF_REGISTERS_TO_WRITE_RW * 2);
|
|
}
|
|
|
|
TEST(ModbusClientFrameLength, WriteMultipleMissingByteCount) {
|
|
const uint8_t frame[] = {0x01, 0x10, 0x00, 0x00, 0x00, 0x02};
|
|
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9);
|
|
}
|
|
|
|
TEST(ModbusClientFrameLength, MiscFixedAndUnknown) {
|
|
const uint8_t mask[] = {0x01, static_cast<uint8_t>(FC::MASK_WRITE_REGISTER)};
|
|
const uint8_t fifo[] = {0x01, static_cast<uint8_t>(FC::READ_FIFO_QUEUE)};
|
|
const uint8_t unknown[] = {0x01, 0x42};
|
|
EXPECT_EQ(client_frame_length(mask, sizeof(mask)), 10);
|
|
EXPECT_EQ(client_frame_length(fifo, sizeof(fifo)), 6);
|
|
EXPECT_EQ(client_frame_length(unknown, sizeof(unknown)), MIN_FRAME_SIZE);
|
|
}
|
|
|
|
// --- file-record length cap --------------------------------------------------
|
|
// FC 0x14/0x15 are parsed only to keep the frame parser in sync; the byte count caps at 251
|
|
// (MAX_PDU_SIZE - 2), reproducing the released frame-relative bound of MAX_FRAME_SIZE - 5.
|
|
|
|
TEST(ModbusFileRecordCap, PduLengthCapsByteCountAt251) {
|
|
const uint8_t pdu[] = {static_cast<uint8_t>(FC::READ_FILE_RECORD), 0xFF}; // claims 255 bytes
|
|
EXPECT_EQ(server_pdu_length(pdu, sizeof(pdu)), 2 + (MAX_PDU_SIZE - 2));
|
|
EXPECT_EQ(client_pdu_length(pdu, sizeof(pdu)), 2 + (MAX_PDU_SIZE - 2));
|
|
// Frame wrappers: address(1) + PDU + CRC(2) stays within the RTU 256-byte frame limit.
|
|
const uint8_t frame[] = {0x01, static_cast<uint8_t>(FC::WRITE_FILE_RECORD), 0xFF};
|
|
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), MAX_FRAME_SIZE);
|
|
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), MAX_FRAME_SIZE);
|
|
}
|
|
|
|
TEST(ModbusFileRecordCap, StandardChecksAcceptUpTo251) {
|
|
// A full-length PDU at the cap: function(1) + byte count(1) + 251 data bytes = MAX_PDU_SIZE.
|
|
std::vector<uint8_t> at_cap(MAX_PDU_SIZE, 0x00);
|
|
at_cap[0] = static_cast<uint8_t>(FC::READ_FILE_RECORD);
|
|
at_cap[1] = MAX_PDU_SIZE - 2;
|
|
EXPECT_TRUE(is_server_pdu_standard(at_cap.data(), at_cap.size()));
|
|
EXPECT_TRUE(is_client_pdu_standard(at_cap.data(), at_cap.size()));
|
|
// Byte count 252 in the same 253-byte buffer: the parsed length still matches (capped), so this
|
|
// exercises the byte-count bound itself rather than the length identity.
|
|
at_cap[1] = MAX_PDU_SIZE - 1;
|
|
EXPECT_FALSE(is_server_pdu_standard(at_cap.data(), at_cap.size()));
|
|
EXPECT_FALSE(is_client_pdu_standard(at_cap.data(), at_cap.size()));
|
|
}
|
|
|
|
// --- is_client_pdu_standard / is_server_pdu_standard -------------------------
|
|
// The gatekeepers for the typed client dispatch: a PDU must be exactly its function code's standard
|
|
// shape, with byte count, quantity, and address range all consistent.
|
|
|
|
TEST(ModbusPduStandard, ClientReadAndWriteConformant) {
|
|
const uint8_t read_regs[] = {0x03, 0x01, 0x00, 0x00, 0x02};
|
|
EXPECT_TRUE(is_client_pdu_standard(read_regs, sizeof(read_regs)));
|
|
const uint8_t write_regs[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01, 0x00, 0x02};
|
|
EXPECT_TRUE(is_client_pdu_standard(write_regs, sizeof(write_regs)));
|
|
// 10 coils pack into 2 data bytes - the coil formula, not the register one.
|
|
const uint8_t write_coils[] = {0x0F, 0x00, 0x30, 0x00, 0x0A, 0x02, 0xFF, 0x03};
|
|
EXPECT_TRUE(is_client_pdu_standard(write_coils, sizeof(write_coils)));
|
|
}
|
|
|
|
TEST(ModbusPduStandard, ClientRejectsNonConformant) {
|
|
// Truncated: header claims 4 data bytes, only 2 present.
|
|
const uint8_t truncated[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x04, 0x00, 0x01};
|
|
EXPECT_FALSE(is_client_pdu_standard(truncated, sizeof(truncated)));
|
|
// Byte count disagrees with quantity (2 registers need 4 bytes, header says 2).
|
|
const uint8_t inconsistent[] = {0x10, 0x00, 0x20, 0x00, 0x02, 0x02, 0x00, 0x01};
|
|
EXPECT_FALSE(is_client_pdu_standard(inconsistent, sizeof(inconsistent)));
|
|
// Coil write using the register byte-count formula (10 coils with 20 data bytes).
|
|
const uint8_t coil_as_regs[] = {0x0F, 0x00, 0x30, 0x00, 0x0A, 0x14, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
EXPECT_FALSE(is_client_pdu_standard(coil_as_regs, sizeof(coil_as_regs)));
|
|
// Quantity zero and quantity beyond the per-function-code maximum.
|
|
const uint8_t zero_qty[] = {0x03, 0x01, 0x00, 0x00, 0x00};
|
|
EXPECT_FALSE(is_client_pdu_standard(zero_qty, sizeof(zero_qty)));
|
|
const uint8_t too_many[] = {0x03, 0x01, 0x00, 0x00, 0x7E}; // 126 > 125
|
|
EXPECT_FALSE(is_client_pdu_standard(too_many, sizeof(too_many)));
|
|
// Address range overflow: 0xFFFF + 2 registers exceeds the 16-bit register space.
|
|
const uint8_t wraps[] = {0x03, 0xFF, 0xFF, 0x00, 0x02};
|
|
EXPECT_FALSE(is_client_pdu_standard(wraps, sizeof(wraps)));
|
|
}
|
|
|
|
TEST(ModbusPduStandard, ServerReadResponses) {
|
|
const uint8_t ok[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00};
|
|
EXPECT_TRUE(is_server_pdu_standard(ok, sizeof(ok)));
|
|
// Byte-count header disagrees with the actual length.
|
|
const uint8_t lying[] = {0x03, 0x06, 0x00, 0x2A, 0x01, 0x00};
|
|
EXPECT_FALSE(is_server_pdu_standard(lying, sizeof(lying)));
|
|
// An empty PDU (the on_error path) is not a standard response.
|
|
EXPECT_FALSE(is_server_pdu_standard(ok, 0));
|
|
}
|
|
|
|
TEST(ModbusPduStandard, ServerResponsesRejectDegenerateShapes) {
|
|
// A read response always carries data: byte count zero is non-conformant.
|
|
const uint8_t zero_bc[] = {0x03, 0x00};
|
|
EXPECT_FALSE(is_server_pdu_standard(zero_bc, sizeof(zero_bc)));
|
|
// Registers are 2 bytes each: an odd byte count would silently truncate a register.
|
|
const uint8_t odd_bc[] = {0x03, 0x03, 0x00, 0x01, 0x02};
|
|
EXPECT_FALSE(is_server_pdu_standard(odd_bc, sizeof(odd_bc)));
|
|
// Bit reads have no parity requirement: one packed byte is a fine coil response.
|
|
const uint8_t coil_one_byte[] = {0x01, 0x01, 0x05};
|
|
EXPECT_TRUE(is_server_pdu_standard(coil_one_byte, sizeof(coil_one_byte)));
|
|
// A write-multiple echo claiming 65535 registers written is bounded like the request side.
|
|
const uint8_t wild_echo[] = {0x10, 0x00, 0x00, 0xFF, 0xFF};
|
|
EXPECT_FALSE(is_server_pdu_standard(wild_echo, sizeof(wild_echo)));
|
|
const uint8_t ok_echo[] = {0x10, 0x00, 0x00, 0x00, 0x02};
|
|
EXPECT_TRUE(is_server_pdu_standard(ok_echo, sizeof(ok_echo)));
|
|
}
|
|
|
|
TEST(ModbusPduStandard, SingleCoilValueMustBeCanonical) {
|
|
// FC 0x05's value field allows exactly 0xFF00 (ON) and 0x0000 (OFF); anything else is non-standard.
|
|
const uint8_t on[] = {0x05, 0x00, 0x10, 0xFF, 0x00};
|
|
const uint8_t off[] = {0x05, 0x00, 0x10, 0x00, 0x00};
|
|
const uint8_t junk[] = {0x05, 0x00, 0x10, 0x12, 0x34};
|
|
EXPECT_TRUE(is_client_pdu_standard(on, sizeof(on)));
|
|
EXPECT_TRUE(is_client_pdu_standard(off, sizeof(off)));
|
|
EXPECT_FALSE(is_client_pdu_standard(junk, sizeof(junk)));
|
|
EXPECT_TRUE(is_server_pdu_standard(on, sizeof(on))); // the response echoes the request
|
|
EXPECT_FALSE(is_server_pdu_standard(junk, sizeof(junk)));
|
|
}
|
|
|
|
TEST(ModbusPduStandard, NonStandardFunctionCodesAcceptedOnLengthAlone) {
|
|
// Custom, unimplemented, and exception function codes have no standard shape to check: they are
|
|
// accepted whenever the parsed length matches, so a dispatcher can still route them by function
|
|
// code instead of having them rejected outright. This is the documented contract - see the header.
|
|
const uint8_t custom[] = {0x42}; // user-defined space; 1 byte matches the MIN_PDU_SIZE fallback
|
|
EXPECT_TRUE(is_client_pdu_standard(custom, sizeof(custom)));
|
|
EXPECT_TRUE(is_server_pdu_standard(custom, sizeof(custom)));
|
|
const uint8_t unimplemented[] = {0x07}; // READ_EXCEPTION_STATUS
|
|
EXPECT_TRUE(is_server_pdu_standard(unimplemented, sizeof(unimplemented)));
|
|
const uint8_t exception[] = {0x83, 0x02}; // exception response; length pinned to 2 bytes
|
|
EXPECT_TRUE(is_server_pdu_standard(exception, sizeof(exception)));
|
|
// The length identity still gates: extra bytes beyond the parsed fallback are non-conformant.
|
|
const uint8_t custom_long[] = {0x42, 0x01};
|
|
EXPECT_FALSE(is_client_pdu_standard(custom_long, sizeof(custom_long)));
|
|
}
|
|
|
|
// --- create_client_pdu -----------------------------------------------------
|
|
// PDU = function code + data (no address, no CRC).
|
|
|
|
TEST(ModbusCreateClientPdu, ReadHolding) {
|
|
auto pdu = create_client_pdu(FC::READ_HOLDING_REGISTERS, 0x0003, 1);
|
|
const std::vector<uint8_t> expected{0x03, 0x00, 0x03, 0x00, 0x01};
|
|
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, WriteSingleOmitsQuantity) {
|
|
const uint8_t values[] = {0x00, 0x0B};
|
|
auto pdu = create_client_pdu(FC::WRITE_SINGLE_REGISTER, 0x0003, 1, values, sizeof(values));
|
|
const std::vector<uint8_t> expected{0x06, 0x00, 0x03, 0x00, 0x0B};
|
|
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, WriteSingleTooFewValuesReturnsEmpty) {
|
|
const uint8_t values[] = {0x00};
|
|
auto pdu = create_client_pdu(FC::WRITE_SINGLE_COIL, 0x0003, 1, values, sizeof(values));
|
|
EXPECT_TRUE(pdu.empty());
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, WriteMultipleIncludesByteCount) {
|
|
const uint8_t values[] = {0x00, 0x0B, 0x00, 0x16};
|
|
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 2, values, sizeof(values));
|
|
const std::vector<uint8_t> expected{0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x0B, 0x00, 0x16};
|
|
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, WriteMultipleOverCapacityReturnsEmpty) {
|
|
std::vector<uint8_t> values(MAX_PDU_SIZE - 6 + 1, 0xAA);
|
|
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 1, values.data(), values.size());
|
|
EXPECT_TRUE(pdu.empty());
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, UnsupportedFunctionCodeReturnsEmpty) {
|
|
auto pdu = create_client_pdu(FC::READ_FIFO_QUEUE, 0x0000, 1);
|
|
EXPECT_TRUE(pdu.empty());
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, ZeroEntitiesReturnsEmpty) {
|
|
auto pdu = create_client_pdu(FC::READ_HOLDING_REGISTERS, 0x0000, 0);
|
|
EXPECT_TRUE(pdu.empty());
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, WriteWithoutValuesReturnsEmpty) {
|
|
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 1, nullptr, 0);
|
|
EXPECT_TRUE(pdu.empty());
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, ReadHoldingOverMaxReturnsEmpty) {
|
|
auto pdu = create_client_pdu(FC::READ_HOLDING_REGISTERS, 0x0000, MAX_NUM_OF_REGISTERS_TO_READ + 1);
|
|
EXPECT_TRUE(pdu.empty());
|
|
}
|
|
|
|
// Regression: coils allow up to 2000 entities, well above the 125 register limit.
|
|
// A switch fall-through previously subjected coil/discrete reads to the register limit.
|
|
TEST(ModbusCreateClientPdu, ReadCoilsAboveRegisterLimitIsValid) {
|
|
const uint16_t quantity = MAX_NUM_OF_REGISTERS_TO_READ + 1; // 126: valid for coils, too many for registers
|
|
auto pdu = create_client_pdu(FC::READ_COILS, 0x0000, quantity);
|
|
const std::vector<uint8_t> expected{0x01, 0x00, 0x00, static_cast<uint8_t>(quantity >> 8),
|
|
static_cast<uint8_t>(quantity & 0xFF)};
|
|
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, ReadCoilsOverMaxReturnsEmpty) {
|
|
auto pdu = create_client_pdu(FC::READ_COILS, 0x0000, MAX_NUM_OF_COILS_TO_READ + 1);
|
|
EXPECT_TRUE(pdu.empty());
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, ReadDiscreteInputsOverMaxReturnsEmpty) {
|
|
auto pdu = create_client_pdu(FC::READ_DISCRETE_INPUTS, 0x0000, MAX_NUM_OF_DISCRETE_INPUTS_TO_READ + 1);
|
|
EXPECT_TRUE(pdu.empty());
|
|
}
|
|
|
|
TEST(ModbusCreateClientPdu, WriteMultipleOverEntityLimitReturnsEmpty) {
|
|
const uint8_t values[] = {0x00, 0x0B};
|
|
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, MAX_NUM_OF_REGISTERS_TO_WRITE + 1, values,
|
|
sizeof(values));
|
|
EXPECT_TRUE(pdu.empty());
|
|
}
|
|
|
|
TEST(ModbusHelpersTest, PayloadToNumberRejectsOffsetAtEndOfBuffer) {
|
|
const std::vector<uint8_t> data{0x12, 0x34};
|
|
EXPECT_FALSE(payload_to_number(std::span<const uint8_t>(data), SensorValueType::U_WORD, 2, 0xFFFFFFFF).has_value());
|
|
}
|
|
|
|
TEST(ModbusHelpersTest, PayloadToNumberRejectsTruncatedMultiRegisterValue) {
|
|
const std::vector<uint8_t> data{0x12, 0x34, 0x56};
|
|
EXPECT_FALSE(payload_to_number(std::span<const uint8_t>(data), SensorValueType::U_DWORD, 0, 0xFFFFFFFF).has_value());
|
|
}
|
|
|
|
TEST(ModbusHelpersTest, PayloadToNumberDecodesValidWord) {
|
|
const std::vector<uint8_t> data{0x12, 0x34};
|
|
EXPECT_EQ(payload_to_number(std::span<const uint8_t>(data), SensorValueType::U_WORD, 0, 0xFFFFFFFF), 0x1234);
|
|
}
|
|
|
|
// --- registers_to_number ---------------------------------------------------
|
|
// Register words are host byte order; results must match the byte-based payload_to_number.
|
|
|
|
TEST(ModbusHelpersTest, RegistersToNumberDecodesWord) {
|
|
const uint16_t registers[] = {0x1234};
|
|
EXPECT_EQ(registers_to_number(registers, 1, SensorValueType::U_WORD), 0x1234);
|
|
}
|
|
|
|
TEST(ModbusHelpersTest, RegistersToNumberDecodesDwordHighWordFirst) {
|
|
const uint16_t registers[] = {0x1234, 0x5678};
|
|
EXPECT_EQ(registers_to_number(registers, 2, SensorValueType::U_DWORD), 0x12345678);
|
|
}
|
|
|
|
TEST(ModbusHelpersTest, RegistersToNumberDecodesAtSpanStart) {
|
|
// The function decodes the value at the start of the span; the caller advances the pointer.
|
|
const uint16_t registers[] = {0xAAAA, 0x1234};
|
|
EXPECT_EQ(registers_to_number(registers + 1, 1, SensorValueType::U_WORD), 0x1234);
|
|
}
|
|
|
|
TEST(ModbusHelpersTest, RegistersToNumberMatchesPayloadToNumber) {
|
|
// Same value via both decoders: registers (host order) vs big-endian bytes.
|
|
const uint16_t registers[] = {0x8001, 0x0002};
|
|
const std::vector<uint8_t> bytes{0x80, 0x01, 0x00, 0x02};
|
|
for (auto value_type : {SensorValueType::S_DWORD, SensorValueType::U_DWORD, SensorValueType::S_DWORD_R}) {
|
|
EXPECT_EQ(registers_to_number(registers, 2, value_type),
|
|
payload_to_number(std::span<const uint8_t>(bytes), value_type, 0, 0xFFFFFFFF))
|
|
<< "value_type=" << static_cast<int>(value_type);
|
|
}
|
|
}
|
|
|
|
TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) {
|
|
const uint16_t registers[] = {0x1234};
|
|
EXPECT_FALSE(registers_to_number(registers, 1, SensorValueType::U_DWORD).has_value());
|
|
}
|
|
|
|
// server_pdu_payload() must never classify an exception PDU as a read: [fc|0x80, code] is 2 bytes, and a
|
|
// read-offset of 2 would return an empty span, losing the exception code. The payload of an exception PDU
|
|
// is the exception code byte, for reads and writes alike.
|
|
TEST(ModbusServerPduPayload, ExceptionOfReadYieldsExceptionCode) {
|
|
const uint8_t pdu[] = {0x83, 0x02}; // exception response to READ_HOLDING_REGISTERS
|
|
auto payload = server_pdu_payload(pdu);
|
|
ASSERT_EQ(payload.size(), 1u);
|
|
EXPECT_EQ(payload[0], 0x02);
|
|
}
|
|
|
|
TEST(ModbusServerPduPayload, ExceptionOfWriteYieldsExceptionCode) {
|
|
const uint8_t pdu[] = {0x86, 0x03}; // exception response to WRITE_SINGLE_REGISTER
|
|
auto payload = server_pdu_payload(pdu);
|
|
ASSERT_EQ(payload.size(), 1u);
|
|
EXPECT_EQ(payload[0], 0x03);
|
|
}
|
|
|
|
} // namespace esphome::modbus::helpers
|