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);
+}