modbus_rtu_lib: define read holding register
Add the modbus rtu library commands with initial support for read
holding register. Define a base Message classes which all subsequent and
specific request & response messages can inherit from. Also add the
relevant unit testing for the added command set.
Tested:
```
> meson test -C builddir
ninja: Entering directory `/host/repos/Modbus/phosphor-modbus/builddir'
ninja: no work to do.
1/1 test_modbus_commands OK 0.01s
Ok: 1
Fail: 0
```
Change-Id: I331b0dee66a0829e9352ae0eac8ac82a9150904c
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/rtu/meson.build b/rtu/meson.build
index 8b13789..fb19994 100644
--- a/rtu/meson.build
+++ b/rtu/meson.build
@@ -1 +1,12 @@
+modbus_rtu_lib = static_library(
+ 'modbus_rtu_lib',
+ ['modbus/modbus_commands.cpp', 'modbus/modbus_message.cpp'],
+ include_directories: ['.'],
+ dependencies: [default_deps],
+)
+modbus_rtu_dep = declare_dependency(
+ include_directories: ['.'],
+ link_with: [modbus_rtu_lib],
+ dependencies: [default_deps],
+)
diff --git a/rtu/modbus/modbus_commands.cpp b/rtu/modbus/modbus_commands.cpp
new file mode 100644
index 0000000..64d541b
--- /dev/null
+++ b/rtu/modbus/modbus_commands.cpp
@@ -0,0 +1,59 @@
+#include "modbus_commands.hpp"
+
+#include "modbus_exception.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+namespace phosphor::modbus::rtu
+{
+
+ReadHoldingRegistersRequest::ReadHoldingRegistersRequest(
+ uint8_t deviceAddress, uint16_t registerOffset, uint16_t registerCount) :
+ deviceAddress(deviceAddress), registerOffset(registerOffset),
+ registerCount(registerCount)
+{}
+
+auto ReadHoldingRegistersRequest::encode() -> void
+{
+ *this << deviceAddress << commandCode << registerOffset << registerCount;
+ appendCRC();
+}
+
+ReadHoldingRegistersResponse::ReadHoldingRegistersResponse(
+ uint8_t deviceAddress, std::vector<uint16_t>& registers) :
+ expectedDeviceAddress(deviceAddress), registers(registers)
+{
+ if (registers.empty())
+ {
+ throw std::underflow_error("Response registers are empty");
+ }
+ // addr(1), func(1), bytecount(1), <2 * count regs>, crc(2)
+ len = 5 + (2 * registers.size());
+}
+
+auto Response::decode() -> void
+{
+ validate();
+
+ // Error response is structured as:
+ // addr(1), errorFunctionCode(1), exceptionCode(1)
+ // Where errorFunctionCode is the response function with
+ // MSB set to 1, hence mask of 0x80.
+ bool isError = (len == 3 && (functionCode & 0x80) != 0);
+ if (isError)
+ {
+ throw ModbusException(raw[2]);
+ }
+}
+
+auto ReadHoldingRegistersResponse::decode() -> void
+{
+ Response::decode();
+ uint8_t byteCount, responseCode, deviceAddress;
+ *this >> registers >> byteCount >> responseCode >> deviceAddress;
+ verifyValue("Device Address", deviceAddress, expectedDeviceAddress);
+ verifyValue("Response Function Code", responseCode, expectedCommandCode);
+ verifyValue("Byte Count", byteCount, registers.size() * 2);
+}
+
+} // namespace phosphor::modbus::rtu
diff --git a/rtu/modbus/modbus_commands.hpp b/rtu/modbus/modbus_commands.hpp
new file mode 100644
index 0000000..117408b
--- /dev/null
+++ b/rtu/modbus/modbus_commands.hpp
@@ -0,0 +1,65 @@
+#pragma once
+
+#include "modbus_message.hpp"
+
+#include <vector>
+
+namespace phosphor::modbus::rtu
+{
+
+static constexpr uint8_t ReadHoldingRegistersFunctionCode = 0x03;
+
+class ReadHoldingRegistersRequest : public Message
+{
+ public:
+ ReadHoldingRegistersRequest() = delete;
+ ReadHoldingRegistersRequest(const ReadHoldingRegistersRequest&) = delete;
+ ReadHoldingRegistersRequest& operator=(const ReadHoldingRegistersRequest&) =
+ delete;
+ ReadHoldingRegistersRequest(ReadHoldingRegistersRequest&&) = delete;
+ ReadHoldingRegistersRequest& operator=(ReadHoldingRegistersRequest&&) =
+ delete;
+
+ explicit ReadHoldingRegistersRequest(
+ uint8_t deviceAddress, uint16_t registerOffset, uint16_t registerCount);
+
+ auto encode() -> void;
+
+ private:
+ static constexpr uint8_t commandCode = ReadHoldingRegistersFunctionCode;
+ const uint8_t deviceAddress;
+ const uint16_t registerOffset;
+ const uint16_t registerCount;
+};
+
+class Response : public Message
+{
+ public:
+ auto decode() -> void;
+};
+
+class ReadHoldingRegistersResponse : public Response
+{
+ public:
+ ReadHoldingRegistersResponse() = delete;
+ ReadHoldingRegistersResponse(const ReadHoldingRegistersResponse&) = delete;
+ ReadHoldingRegistersResponse& operator=(
+ const ReadHoldingRegistersResponse&) = delete;
+ ReadHoldingRegistersResponse(ReadHoldingRegistersResponse&&) = delete;
+ ReadHoldingRegistersResponse& operator=(ReadHoldingRegistersResponse&&) =
+ delete;
+
+ explicit ReadHoldingRegistersResponse(uint8_t deviceAddress,
+ std::vector<uint16_t>& registers);
+
+ auto decode() -> void;
+
+ private:
+ static constexpr uint8_t expectedCommandCode =
+ ReadHoldingRegistersFunctionCode;
+ const uint8_t expectedDeviceAddress;
+ // The returned response is stored in the registers vector
+ std::vector<uint16_t>& registers;
+};
+
+} // namespace phosphor::modbus::rtu
diff --git a/rtu/modbus/modbus_exception.hpp b/rtu/modbus/modbus_exception.hpp
new file mode 100644
index 0000000..9498a1c
--- /dev/null
+++ b/rtu/modbus/modbus_exception.hpp
@@ -0,0 +1,107 @@
+#pragma once
+
+#include <format>
+#include <stdexcept>
+#include <string>
+
+namespace phosphor::modbus::rtu
+{
+
+enum class ModbusExceptionCode
+{
+ // The Modbus function code in the request is not supported or is an invalid
+ // action for the server.
+ illegalFunctionCode = 1,
+
+ // The requested data address is not valid or authorized for the server.
+ illegalDataAddress = 2,
+
+ // The value provided in the request is not an allowable value for the
+ // server, or the data field is structured incorrectly.
+ illegalDataValue = 3,
+
+ // The server encountered an internal error and cannot perform the requested
+ // operation.
+ slaveDeviceFailure = 4,
+
+ // The server has accepted the request but needs more time to process it.
+ acknowledge = 5,
+
+ // The server is currently busy and cannot respond to the request.
+ slaveDeviceBusy = 6,
+
+ // The server cannot perform the program function received in the query.
+ // This code is returned for an unsuccessful programming request using
+ // function code 13 or 14 decimal. The client should request diagnostic or
+ // error information from the server.
+ negativeAcknowledge = 7,
+
+ // The server attempted to read extended memory, but detected a parity error
+ // in the memory. The client can retry the request, but service may be
+ // required on the server device.
+ memoryParityError = 8,
+ unknownError = 255,
+};
+
+class ModbusException : public std::runtime_error
+{
+ public:
+ const ModbusExceptionCode code;
+
+ explicit ModbusException(uint8_t code, const std::string& message = "") :
+ std::runtime_error(std::format(
+ "{} ({})", toString(static_cast<ModbusExceptionCode>(code)),
+ message)),
+ code(static_cast<ModbusExceptionCode>(code))
+ {}
+
+ static auto toString(ModbusExceptionCode code) -> std::string
+ {
+ switch (code)
+ {
+ case ModbusExceptionCode::illegalFunctionCode:
+ return "Illegal Function Code";
+ case ModbusExceptionCode::illegalDataAddress:
+ return "Illegal Data Address";
+ case ModbusExceptionCode::illegalDataValue:
+ return "Illegal Data Value";
+ case ModbusExceptionCode::slaveDeviceFailure:
+ return "Slave Device Failure";
+ case ModbusExceptionCode::acknowledge:
+ return "Acknowledge";
+ case ModbusExceptionCode::slaveDeviceBusy:
+ return "Slave Device Busy";
+ case ModbusExceptionCode::negativeAcknowledge:
+ return "Negative Acknowledge";
+ case ModbusExceptionCode::memoryParityError:
+ return "Memory Parity Error";
+ default:
+ return "Unknown Modbus Error";
+ }
+ }
+};
+
+class ModbusCRCException : public std::runtime_error
+{
+ public:
+ explicit ModbusCRCException(uint16_t expectedCRC, uint16_t crc) :
+ std::runtime_error(
+ "CRC mismatch, expected: " + std::to_string(expectedCRC) +
+ " received: " + std::to_string(crc))
+ {}
+};
+
+class ModbusBadResponseException : public std::runtime_error
+{
+ public:
+ explicit ModbusBadResponseException(const std::string& fieldName,
+ uint32_t expectedValue,
+ uint32_t currentValue) :
+ std::runtime_error(
+ "Value mismatch for " + fieldName +
+ ", expected value: " + std::to_string(expectedValue) +
+ ", current value: " + std::to_string(currentValue))
+ {}
+};
+
+} // namespace phosphor::modbus::rtu
diff --git a/rtu/modbus/modbus_message.cpp b/rtu/modbus/modbus_message.cpp
new file mode 100644
index 0000000..2a663fe
--- /dev/null
+++ b/rtu/modbus/modbus_message.cpp
@@ -0,0 +1,137 @@
+#include "modbus_message.hpp"
+
+#include "modbus_exception.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+namespace phosphor::modbus::rtu
+{
+
+PHOSPHOR_LOG2_USING;
+
+Message& Message::operator<<(uint8_t d)
+{
+ if ((len + 1) > raw.size())
+ {
+ throw std::overflow_error("Encoding Failed");
+ }
+ raw[len++] = d;
+ return *this;
+}
+
+Message& Message::operator<<(uint16_t d)
+{
+ uint8_t upper = d >> 8, lower = d & 0xffff;
+ *this << upper; // Big-endian
+ *this << lower;
+ return *this;
+}
+
+Message& Message::operator<<(uint32_t d)
+{
+ uint16_t upper = d >> 16, lower = d & 0xffff;
+ *this << upper; // Big-endian
+ *this << lower;
+ return *this;
+}
+
+Message& Message::operator>>(uint8_t& d)
+{
+ if (len < 1)
+ {
+ throw std::underflow_error("Decoding Failed");
+ }
+ d = raw[--len];
+ return *this;
+}
+
+Message& Message::operator>>(uint16_t& d)
+{
+ uint8_t upper, lower;
+ *this >> lower; // Big-endian
+ *this >> upper;
+ d = upper << 8 | lower;
+ return *this;
+}
+
+Message& Message::operator>>(uint32_t& d)
+{
+ uint16_t upper, lower;
+ *this >> lower; // Big-endian
+ *this >> upper;
+ d = upper << 16 | lower;
+ return *this;
+}
+
+auto Message::appendCRC() -> void
+{
+ *this << generateCRC();
+}
+
+auto Message::validate() -> void
+{
+ uint16_t crc;
+ *this >> crc;
+ uint16_t expectedCRC = generateCRC();
+ if (expectedCRC != crc)
+ {
+ throw ModbusCRCException(expectedCRC, crc);
+ }
+}
+
+auto Message::verifyValue(const std::string& name, uint32_t currentValue,
+ uint32_t expectedValue) -> void
+{
+ if (currentValue != expectedValue)
+ {
+ throw ModbusBadResponseException(name, expectedValue, currentValue);
+ }
+}
+
+static const uint16_t crc16Table[] = {
+ 0x0, 0xc0c1, 0xc181, 0x140, 0xc301, 0x3c0, 0x280, 0xc241, 0xc601,
+ 0x6c0, 0x780, 0xc741, 0x500, 0xc5c1, 0xc481, 0x440, 0xcc01, 0xcc0,
+ 0xd80, 0xcd41, 0xf00, 0xcfc1, 0xce81, 0xe40, 0xa00, 0xcac1, 0xcb81,
+ 0xb40, 0xc901, 0x9c0, 0x880, 0xc841, 0xd801, 0x18c0, 0x1980, 0xd941,
+ 0x1b00, 0xdbc1, 0xda81, 0x1a40, 0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01,
+ 0x1dc0, 0x1c80, 0xdc41, 0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0,
+ 0x1680, 0xd641, 0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081,
+ 0x1040, 0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
+ 0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441, 0x3c00,
+ 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41, 0xfa01, 0x3ac0,
+ 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840, 0x2800, 0xe8c1, 0xe981,
+ 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41, 0xee01, 0x2ec0, 0x2f80, 0xef41,
+ 0x2d00, 0xedc1, 0xec81, 0x2c40, 0xe401, 0x24c0, 0x2580, 0xe541, 0x2700,
+ 0xe7c1, 0xe681, 0x2640, 0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0,
+ 0x2080, 0xe041, 0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281,
+ 0x6240, 0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
+ 0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41, 0xaa01,
+ 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840, 0x7800, 0xb8c1,
+ 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41, 0xbe01, 0x7ec0, 0x7f80,
+ 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40, 0xb401, 0x74c0, 0x7580, 0xb541,
+ 0x7700, 0xb7c1, 0xb681, 0x7640, 0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101,
+ 0x71c0, 0x7080, 0xb041, 0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0,
+ 0x5280, 0x9241, 0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481,
+ 0x5440, 0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
+ 0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841, 0x8801,
+ 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40, 0x4e00, 0x8ec1,
+ 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41, 0x4400, 0x84c1, 0x8581,
+ 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641, 0x8201, 0x42c0, 0x4380, 0x8341,
+ 0x4100, 0x81c1, 0x8081, 0x4040};
+
+auto Message::generateCRC() -> uint16_t
+{
+ uint16_t crc = 0xFFFF; // Initial value for Modbus CRC
+
+ for (const auto& data : *this)
+ {
+ // XOR the current byte with the low byte of the CRC,
+ // then use the result as an index into the table.
+ crc = (crc >> 8) ^ crc16Table[(crc ^ data) & 0xFF];
+ }
+
+ // Modbus CRC requires byte swapping of the final result
+ return ((crc & 0xFF) << 8) | ((crc >> 8) & 0xFF);
+}
+
+} // namespace phosphor::modbus::rtu
diff --git a/rtu/modbus/modbus_message.hpp b/rtu/modbus/modbus_message.hpp
new file mode 100644
index 0000000..6b26dab
--- /dev/null
+++ b/rtu/modbus/modbus_message.hpp
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace phosphor::modbus::rtu
+{
+
+class Message
+{
+ public:
+ static constexpr auto maxADUSize = 256;
+ std::array<uint8_t, maxADUSize> raw{};
+ size_t len = 0;
+
+ // Push to the end of raw message
+ Message& operator<<(uint8_t d);
+ Message& operator<<(uint16_t d);
+ Message& operator<<(uint32_t d);
+
+ // Pop from the end of raw message
+ Message& operator>>(uint8_t& d);
+ Message& operator>>(uint16_t& d);
+ Message& operator>>(uint32_t& d);
+
+ Message() = default;
+ Message(const Message&) = delete;
+ Message& operator=(const Message&) = delete;
+
+ uint8_t& address = raw[0];
+ uint8_t& functionCode = raw[1];
+
+ protected:
+ auto appendCRC() -> void;
+ auto validate() -> void;
+ auto verifyValue(const std::string& name, uint32_t currentValue,
+ uint32_t expectedValue) -> void;
+
+ template <typename T>
+ Message& operator>>(std::vector<T>& d)
+ {
+ for (auto it = d.rbegin(); it != d.rend(); it++)
+ {
+ *this >> *it;
+ }
+ return *this;
+ }
+
+ private:
+ auto generateCRC() -> uint16_t;
+
+ constexpr auto begin() noexcept
+ {
+ return raw.begin();
+ }
+ auto end() noexcept
+ {
+ return raw.begin() + len;
+ }
+};
+
+} // namespace phosphor::modbus::rtu
diff --git a/tests/meson.build b/tests/meson.build
index 8b13789..a02a690 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1 +1,30 @@
+gtest_dep = dependency('gtest', main: true, disabler: true, required: false)
+gmock_dep = dependency('gmock', disabler: true, required: false)
+if not gtest_dep.found() or not gmock_dep.found()
+ gtest_proj = import('cmake').subproject('googletest', required: false)
+ if gtest_proj.found()
+ gtest_dep = declare_dependency(
+ dependencies: [
+ dependency('threads'),
+ gtest_proj.dependency('gtest'),
+ gtest_proj.dependency('gtest_main'),
+ ],
+ )
+ gmock_dep = gtest_proj.dependency('gmock')
+ else
+ assert(
+ not get_option('tests').enabled(),
+ 'Googletest is required if tests are enabled',
+ )
+ endif
+endif
+test(
+ 'test_modbus_commands',
+ executable(
+ 'test_modbus_commands',
+ 'test_modbus_commands.cpp',
+ dependencies: [gtest_dep, gmock_dep, default_deps, modbus_rtu_dep],
+ include_directories: ['.'],
+ ),
+)
diff --git a/tests/test_modbus_commands.cpp b/tests/test_modbus_commands.cpp
new file mode 100644
index 0000000..547e0b2
--- /dev/null
+++ b/tests/test_modbus_commands.cpp
@@ -0,0 +1,177 @@
+#include "modbus/modbus_commands.hpp"
+#include "modbus/modbus_exception.hpp"
+
+#include <cstring>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace RTUIntf = phosphor::modbus::rtu;
+
+class ReadHoldingRegistersResponseTest :
+ public RTUIntf::ReadHoldingRegistersResponse
+{
+ public:
+ ReadHoldingRegistersResponseTest(uint8_t address,
+ std::vector<uint16_t>& registers) :
+ RTUIntf::ReadHoldingRegistersResponse(address, registers)
+ {}
+
+ template <std::size_t Length>
+ ReadHoldingRegistersResponseTest& operator=(
+ const std::array<uint8_t, Length>& expectedMessage)
+ {
+ len = Length;
+ std::copy(expectedMessage.begin(), expectedMessage.end(), raw.begin());
+ return *this;
+ }
+};
+
+class ModbusCommandTest : public ::testing::Test
+{
+ public:
+ ModbusCommandTest() = default;
+
+ static constexpr auto failureMessageLength = 11;
+
+ template <typename ExceptionType, std::size_t Length>
+ void TestReadHoldingRegisterResponseFailure(
+ const std::array<uint8_t, Length>& expectedMessage)
+ {
+ constexpr uint8_t expectedAddress = 0xa;
+ constexpr size_t expectedRegisterSize = 3;
+ std::vector<uint16_t> registers(expectedRegisterSize);
+ ReadHoldingRegistersResponseTest response(expectedAddress, registers);
+ response = expectedMessage;
+
+ EXPECT_THROW(response.decode(), ExceptionType);
+ }
+};
+
+TEST_F(ModbusCommandTest, TestReadHoldingRegistersRequestSucess)
+{
+ constexpr size_t expectedLength = 8;
+ constexpr uint8_t expectedAddress = 0x1;
+ constexpr std::array<uint8_t, expectedLength> expectedMessage = {
+ expectedAddress, // addr(1) = 0x01
+ 0x03, // func(1) = 0x03
+ 0x12, // reg_off(2) = 0x1234
+ 0x34,
+ 0x00, // reg_cnt(2) = 0x0020,
+ 0x20,
+ 0x00, // crc(2) (Pre-computed) = 0xa4
+ 0xa4};
+
+ RTUIntf::ReadHoldingRegistersRequest request(expectedAddress, 0x1234, 0x20);
+ request.encode();
+
+ std::array<uint8_t, expectedLength> actualMessage;
+ std::memcpy(actualMessage.data(), request.raw.data(), expectedLength);
+
+ EXPECT_EQ(actualMessage, expectedMessage);
+ EXPECT_EQ(request.address, expectedAddress);
+}
+
+TEST_F(ModbusCommandTest, TestReadHoldingRegistersResponseSucess)
+{
+ constexpr size_t expectedLength = 11;
+ constexpr uint8_t expectedAddress = 0xa;
+ constexpr size_t expectedRegisterSize = 3;
+ constexpr std::array<uint8_t, expectedLength> expectedMessage = {
+ expectedAddress, // addr(1) = 0x0a
+ 0x03, // func(1) = 0x03
+ 0x06, // bytes(1) = 0x06
+ 0x11, // regs(3*2) = 0x1122, 0x3344, 0x5566
+ 0x22,
+ 0x33,
+ 0x44,
+ 0x55,
+ 0x66,
+ 0x59, // crc(2) = 0x5928 (pre-computed)
+ 0x28};
+ const std::vector<uint16_t> expectedRegisters{0x1122, 0x3344, 0x5566};
+ std::vector<uint16_t> registers(expectedRegisterSize);
+
+ RTUIntf::ReadHoldingRegistersResponse response(expectedAddress, registers);
+ EXPECT_EQ(response.len, expectedLength);
+
+ std::copy(expectedMessage.begin(), expectedMessage.end(),
+ response.raw.begin());
+
+ EXPECT_EQ(response.len, expectedLength);
+ response.decode();
+ EXPECT_EQ(registers, expectedRegisters);
+}
+
+TEST_F(ModbusCommandTest, TestReadHoldingRegistersResponseError)
+{
+ constexpr std::array<uint8_t, 5> expectedMessage = {
+ 0xa, // addr(1) = 0x0a
+ 0x83, // func(1) = 0x83
+ 0x03, // exception code(1) = 0x03
+ 0x70, // crc(2) = 0x70f3 (pre-computed)
+ 0xf3};
+
+ TestReadHoldingRegisterResponseFailure<RTUIntf::ModbusException>(
+ expectedMessage);
+}
+
+TEST_F(ModbusCommandTest, TestReadHoldingRegistersResponseBadAddress)
+{
+ constexpr std::array<uint8_t, failureMessageLength> expectedMessage = {
+ 0x1, // Bad address(1), should be 0x0a
+ 0x03, // func(1) = 0x03
+ 0x06, // bytes(1) = 0x06
+ 0x11, // regs(3*2) = 0x1122, 0x3344, 0x5566
+ 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0x2a, // crc(2) = 0x2a18 (pre-computed)
+ 0x18};
+
+ TestReadHoldingRegisterResponseFailure<RTUIntf::ModbusBadResponseException>(
+ expectedMessage);
+}
+
+TEST_F(ModbusCommandTest, TestReadHoldingRegistersResponseBadCRC)
+{
+ constexpr std::array<uint8_t, failureMessageLength> expectedMessage = {
+ 0xa, // addr(1) = 0x0a
+ 0x03, // func(1) = 0x03
+ 0x06, // bytes(1) = 0x06
+ 0x11, // regs(3*2) = 0x1122, 0x3344, 0x5566
+ 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0x59, // Bad crc(2), should be 0x5928
+ 0x29};
+
+ TestReadHoldingRegisterResponseFailure<RTUIntf::ModbusCRCException>(
+ expectedMessage);
+}
+
+TEST_F(ModbusCommandTest, TestReadHoldingRegistersResponseBadFunctionCode)
+{
+ constexpr std::array<uint8_t, failureMessageLength> expectedMessage = {
+ 0xa, // addr(1) = 0x0a
+ 0x04, // Bad function code(1), should be 0x03
+ 0x06, // bytes(1) = 0x06
+ 0x11, // regs(3*2) = 0x1122, 0x3344, 0x5566
+ 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0x18, // crc(2) = 0x18ce (pre-computed)
+ 0xce};
+
+ TestReadHoldingRegisterResponseFailure<RTUIntf::ModbusBadResponseException>(
+ expectedMessage);
+}
+
+TEST_F(ModbusCommandTest, TestReadHoldingRegistersResponseBadByteCount)
+{
+ constexpr std::array<uint8_t, failureMessageLength> expectedMessage = {
+ 0xa, // addr(1) = 0x0a
+ 0x03, // func(1) = 0x03
+ 0x04, // Bad bytes(1), should be 0x06
+ 0x11, // regs(3*2) = 0x1122, 0x3344, 0x5566
+ 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0x7a, // crc(2) = 0x7ae8 (pre-computed)
+ 0xe8};
+
+ TestReadHoldingRegisterResponseFailure<RTUIntf::ModbusBadResponseException>(
+ expectedMessage);
+}