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