rtu: add mock modbus test device

Add the mocked modbus test device/server. The purpose of this mocked
device is to help facilitate testing on Qemu VM. More details are
captured in Readme.md file.

Change-Id: I3b09f4057ea5cc03db101b81f435ae772d51d601
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/meson.build b/meson.build
index 643b7ec..060e6f0 100644
--- a/meson.build
+++ b/meson.build
@@ -25,6 +25,10 @@
     subdir('rtu')
 endif
 
+if get_option('mocked-test-device').allowed()
+    subdir('mocked_test_device')
+endif
+
 if get_option('tests').allowed()
     subdir('tests')
 endif
diff --git a/meson.options b/meson.options
index 5346c2b..eee0609 100644
--- a/meson.options
+++ b/meson.options
@@ -5,4 +5,11 @@
     description: 'Enable modbus RTU.',
 )
 
-option('tests', type: 'feature', description: 'Build tests')
+option(
+    'mocked-test-device',
+    type: 'feature',
+    value: 'enabled',
+    description: 'Build mocked modbus devuce.',
+)
+
+option('tests', type: 'feature', description: 'Build tests.')
diff --git a/mocked_test_device/Readme.md b/mocked_test_device/Readme.md
new file mode 100644
index 0000000..52ae240
--- /dev/null
+++ b/mocked_test_device/Readme.md
@@ -0,0 +1,23 @@
+# Mock Modbus Test Device
+
+## mock-modbus-device
+
+The `mock-modbus-device` daemon launches a simulated Modbus server on a
+specified PTY port ID. This server listens for Modbus requests and returns
+static data. At present, it only supports the ReadHoldingRegisters command, with
+plans to add more command support in the future.
+
+## start_mock_server.sh
+
+The `start_mock_server.sh` script acts as a wrapper for `mock-modbus-device`. It
+accepts a count parameter and starts a separate mocked Modbus server for each
+PTY port. The script also configures the necessary environment variables to
+enable client communication with the mocked servers. It utilizes socat to create
+pseudo-terminals (PTYs) and initiates a `mock-modbus-device` instance for each
+PTY.
+
+This approach enables clients to interact with simulated Modbus devices,
+facilitating testing across different scenarios without requiring physical
+hardware. It is particularly suited for Qemu-based testing. To use this setup,
+users must manually copy the required artifacts to the Qemu VM and execute the
+script to start the mocked Modbus servers.
diff --git a/mocked_test_device/meson.build b/mocked_test_device/meson.build
new file mode 100644
index 0000000..116129b
--- /dev/null
+++ b/mocked_test_device/meson.build
@@ -0,0 +1,9 @@
+executable(
+    'mock-modbus-server',
+    ['mock_modbus_server.cpp'],
+    include_directories: ['.', common_include],
+    dependencies: [default_deps, modbus_rtu_dep],
+    link_with: [modbus_rtu_lib],
+    install: true,
+    install_dir: get_option('libexecdir') / 'phosphor-modbus',
+)
diff --git a/mocked_test_device/mock_modbus_server.cpp b/mocked_test_device/mock_modbus_server.cpp
new file mode 100644
index 0000000..e89ef5a
--- /dev/null
+++ b/mocked_test_device/mock_modbus_server.cpp
@@ -0,0 +1,156 @@
+#include "modbus/modbus.hpp"
+#include "modbus/modbus_commands.hpp"
+#include "modbus/modbus_message.hpp"
+
+#include <fcntl.h>
+
+#include <sdbusplus/async.hpp>
+
+#include <iostream>
+#include <string>
+
+namespace phosphor::modbus::test
+{
+
+class MessageIntf : public phosphor::modbus::rtu::Message
+{
+    friend class TestServer;
+};
+
+class TestServer
+{
+  public:
+    explicit TestServer(sdbusplus::async::context& ctx, int fd);
+
+  private:
+    auto processRequests() -> sdbusplus::async::task<void>;
+    void processMessage(MessageIntf& request, size_t requestSize,
+                        MessageIntf& response);
+
+    void processReadHoldingRegisters(MessageIntf& request, size_t requestSize,
+                                     MessageIntf& response);
+
+    sdbusplus::async::context& ctx;
+    int fd;
+    sdbusplus::async::fdio fdioInstance;
+};
+
+TestServer::TestServer(sdbusplus::async::context& ctx, int fd) :
+    ctx(ctx), fd(fd), fdioInstance(ctx, fd)
+{
+    ctx.spawn(processRequests());
+}
+
+auto TestServer::processRequests() -> sdbusplus::async::task<void>
+{
+    while (!ctx.stop_requested())
+    {
+        MessageIntf request;
+        co_await fdioInstance.next();
+        auto ret = read(fd, request.raw.data(), request.raw.size());
+        // Request message need to be at least 4 bytes long - address(1),
+        // function code(1), ..., CRC(2)
+        if (ret < 4)
+        {
+            std::cerr << "Invalid Server message size:" << ret << ", drop it"
+                      << std::endl;
+            continue;
+        }
+
+        MessageIntf response;
+        processMessage(request, ret, response);
+
+        ret = write(fd, response.raw.data(), response.len);
+        if (ret < 0)
+        {
+            std::cerr << "Failed to send response" << std::endl;
+        }
+    }
+}
+
+void TestServer::processMessage(MessageIntf& request, size_t requestSize,
+                                MessageIntf& response)
+{
+    constexpr uint8_t readHoldingRegistersFunctionCode = 0x3;
+
+    switch (request.functionCode)
+    {
+        case readHoldingRegistersFunctionCode:
+            processReadHoldingRegisters(request, requestSize, response);
+            break;
+        default:
+            std::cerr << "Server received unknown request" << std::endl;
+            break;
+    }
+}
+
+void TestServer::processReadHoldingRegisters(
+    MessageIntf& request, size_t requestSize, MessageIntf& response)
+{
+    uint16_t registerOffset = request.raw[2] << 8 | request.raw[3];
+    uint16_t registerCount = request.raw[4] << 8 | request.raw[5];
+
+    std::cout << "Received readHoldingRegisters request with size:"
+              << requestSize << ", registerOffset:" << registerOffset
+              << ", registerCount:" << registerCount << std::endl;
+
+    response << request.raw[0] << request.raw[1] << uint8_t(2 * registerCount);
+    for (int i = 0; i < registerCount; i++)
+    {
+        constexpr uint16_t readHoldingRegisterResponse = 0x4142;
+        response << uint16_t(readHoldingRegisterResponse);
+    }
+    response.appendCRC();
+}
+
+} // namespace phosphor::modbus::test
+
+int main(int argc, char* argv[])
+{
+    if (argc < 2)
+    {
+        std::cerr << "Usage: " << argv[0] << " <integer_value>" << std::endl;
+        return 1;
+    }
+
+    int interfaceValue = 1;
+    try
+    {
+        interfaceValue = std::stoi(argv[1]);
+    }
+    catch (const std::invalid_argument& e)
+    {
+        std::cerr << "Invalid argument: " << e.what() << std::endl;
+        return 1;
+    }
+    catch (const std::out_of_range& e)
+    {
+        std::cerr << "Argument out of range: " << e.what() << std::endl;
+        return 1;
+    }
+
+    using TestServerIntf = phosphor::modbus::test::TestServer;
+    std::string devicePathStr =
+        std::string("/dev/ttyV") + std::to_string(interfaceValue);
+    const char* serverDevicePath = devicePathStr.c_str();
+    std::cout << "Starting at device path" << serverDevicePath << std::endl;
+    constexpr auto path = "/xyz/openbmc_project";
+    constexpr auto serviceName = "xyz.openbmc_project.ModbusRTUTestServer";
+    sdbusplus::async::context ctx;
+
+    auto fdServer = open(serverDevicePath, O_RDWR | O_NOCTTY | O_NONBLOCK);
+    if (fdServer == -1)
+    {
+        std::cerr << "Failed to open serial port " << serverDevicePath
+                  << " with error: " << strerror(errno) << std::endl;
+        return 1;
+    }
+
+    std::cout << "Creating Modbus RTU test server at " << path << std::endl;
+    TestServerIntf server{ctx, fdServer};
+
+    ctx.request_name(serviceName);
+
+    ctx.run();
+    return 0;
+}
diff --git a/mocked_test_device/start_mock_server.sh b/mocked_test_device/start_mock_server.sh
new file mode 100644
index 0000000..d3b8fd9
--- /dev/null
+++ b/mocked_test_device/start_mock_server.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+
+set -e
+set -u
+set -o pipefail
+
+function cleanup() {
+    echo -e "\nCaught EXIT signal. Terminating background processes."
+    jobs -p | xargs -r kill 2>/dev/null
+    echo "Cleanup complete."
+}
+trap cleanup EXIT
+
+DEFAULT_COUNT=2
+MODBUS_SERVER="/usr/libexec/phosphor-modbus/mock-modbus-server"
+
+if [ "$#" -eq 0 ]; then
+    echo "No count provided. Starting with default count of $DEFAULT_COUNT servers and serial port pairs."
+    SERVER_COUNT=$DEFAULT_COUNT
+elif [ "$#" -eq 1 ]; then
+    SERVER_COUNT=$1
+    if ! [[ "$SERVER_COUNT" =~ ^[0-9]+$ ]]; then
+        echo "Error: Count must be a non-negative integer." >&2
+        exit 1
+    fi
+    echo "Starting $SERVER_COUNT $MODBUS_SERVER instances and virtual port pairs."
+else
+    echo "Error: Too many arguments." >&2
+    echo "Usage: $0 [<count>]" >&2
+    exit 1
+fi
+
+# Create the necessary directory structure for serial ports
+echo "Creating directory /dev/serial/by-path"
+mkdir -p /dev/serial/by-path
+
+# Remove old symlinks to prevent conflicts
+echo "Removing old symlinks from /dev/serial/by-path/..."
+rm -f /dev/serial/by-path/platform-1e6a1000.usb-usb-0:1:1.*-port0
+
+# Loop to create virtual serial port pairs and start modbus servers
+echo "Starting $SERVER_COUNT virtual serial port pairs and $MODBUS_SERVER instances."
+for (( i=0; i<SERVER_COUNT; i++ )); do
+    TTY_USB="/dev/ttyUSB$i"
+    TTY_V="/dev/ttyV$i"
+
+    # Start the socat process for this pair
+    echo "  - Starting socat for $TTY_USB and $TTY_V."
+    socat -v -x -d -d pty,link="$TTY_USB",rawer,echo=0,b115200 pty,rawer,echo=0,link="$TTY_V",b115200 &
+
+    # Wait a moment for socat to create the devices
+    sleep 0.5
+
+    echo "  - Creating symlink for $TTY_USB..."
+    ln -sf $TTY_USB "/dev/serial/by-path/platform-1e6a1000.usb-usb-0:1:1.$i-port0"
+
+    # Start the $MODBUS_SERVER instance
+    echo "  - Starting $MODBUS_SERVER instance $i."
+    $MODBUS_SERVER $i &
+done
+
+echo "All background processes have been started."
+echo "Press Ctrl+C to terminate all processes gracefully."
+
+# Keep the script running to manage background jobs
+wait