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