rtu: add serial port interface

Add the interface classes for serial port with Port Factory classes to
make the code extensible for future in case a new hardware port type is
introduced. This also makes the unit testing easy by creating a Mock
Port using socat.

Tested:
```
meson test -C builddir test_port
ninja: Entering directory `/host/repos/Modbus/phosphor-modbus/builddir'
ninja: no work to do.
1/1 test_port        OK              5.02s

Ok:                1
Fail:              0
```

Change-Id: Ic6bd982abf1ae993f76c39e3503d3a0402a692fe
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/rtu/meson.build b/rtu/meson.build
index e088f17..2c385b2 100644
--- a/rtu/meson.build
+++ b/rtu/meson.build
@@ -9,8 +9,16 @@
     dependencies: [default_deps],
 )
 
+modbus_rtu_port_lib = static_library(
+    'modbus_rtu_port_lib',
+    ['port/base_port.cpp', 'port/port_factory.cpp', 'port/usb_port.cpp'],
+    include_directories: ['.', common_include],
+    link_with: [modbus_rtu_lib, modbus_common_lib],
+    dependencies: [default_deps],
+)
+
 modbus_rtu_dep = declare_dependency(
     include_directories: ['.'],
-    link_with: [modbus_rtu_lib],
+    link_with: [modbus_rtu_lib, modbus_rtu_port_lib],
     dependencies: [default_deps],
 )
diff --git a/rtu/port/base_port.cpp b/rtu/port/base_port.cpp
new file mode 100644
index 0000000..42cef57
--- /dev/null
+++ b/rtu/port/base_port.cpp
@@ -0,0 +1,72 @@
+#include "base_port.hpp"
+
+#include "common/entity_manager_interface.hpp"
+
+#include <fcntl.h>
+
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/Configuration/USBPort/client.hpp>
+
+#include <filesystem>
+#include <format>
+#include <optional>
+#include <regex>
+
+namespace phosphor::modbus::rtu::port
+{
+
+PHOSPHOR_LOG2_USING;
+
+BasePort::BasePort(sdbusplus::async::context& ctx, const config::Config& config,
+                   const std::string& devicePath) :
+    name(config.name), mutex(config.name)
+{
+    fd = open(devicePath.c_str(), O_RDWR | O_NOCTTY);
+    if (fd == -1)
+    {
+        throw("Failed to open serial port " + devicePath +
+              " with error: " + strerror(errno));
+    }
+
+    modbus =
+        std::make_unique<ModbusIntf>(ctx, fd, config.baudRate, config.rtsDelay);
+    if (!modbus)
+    {
+        throw std::runtime_error("Failed to create Modbus interface");
+    }
+
+    info("Serial port {NAME} created successfully", "NAME", config.name);
+}
+
+auto BasePort::readHoldingRegisters(
+    uint8_t deviceAddress, uint16_t registerOffset, uint32_t baudRate,
+    Parity parity, std::vector<uint16_t>& registers)
+    -> sdbusplus::async::task<bool>
+{
+    sdbusplus::async::lock_guard lg{mutex};
+    co_await lg.lock();
+
+    if (!modbus->setProperties(baudRate, parity))
+    {
+        error("Failed to set serial port properties");
+        co_return false;
+    }
+
+    debug(
+        "Reading holding registers from device {ADDRESS} {PORT} at offset {OFFSET}",
+        "ADDRESS", deviceAddress, "PORT", name, "OFFSET", registerOffset);
+
+    auto ret = co_await modbus->readHoldingRegisters(deviceAddress,
+                                                     registerOffset, registers);
+    if (!ret)
+    {
+        error(
+            "Failed to read holding registers from device {ADDRESS} {PORT} at offset "
+            "{OFFSET}",
+            "ADDRESS", deviceAddress, "PORT", name, "OFFSET", registerOffset);
+    }
+
+    co_return ret;
+}
+
+} // namespace phosphor::modbus::rtu::port
diff --git a/rtu/port/base_port.hpp b/rtu/port/base_port.hpp
new file mode 100644
index 0000000..6a30dab
--- /dev/null
+++ b/rtu/port/base_port.hpp
@@ -0,0 +1,114 @@
+#pragma once
+
+#include "modbus/modbus.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <xyz/openbmc_project/Configuration/USBPort/client.hpp>
+
+#include <concepts>
+
+namespace phosphor::modbus::rtu::port
+{
+
+using ModbusIntf = phosphor::modbus::rtu::Modbus;
+
+namespace config
+{
+
+enum class PortMode
+{
+    rs232,
+    rs485,
+    unknown
+};
+
+static constexpr std::array<std::pair<std::string_view, PortMode>, 2>
+    validPortModes = {{{"RS232", PortMode::rs232}, {"RS485", PortMode::rs485}}};
+
+struct Config
+{
+    std::string name = "unknown";
+    PortMode portMode = PortMode::unknown;
+    uint32_t baudRate = 0;
+    uint16_t rtsDelay = 0;
+};
+
+template <typename T>
+concept HasPropertiesMembers = requires(T properties) {
+                                   {
+                                       properties.name
+                                   } -> std::same_as<std::string&>;
+                                   {
+                                       properties.mode
+                                   } -> std::same_as<std::string&>;
+                                   {
+                                       properties.baud_rate
+                                   } -> std::same_as<uint64_t&>;
+                                   {
+                                       properties.rts_delay
+                                   } -> std::same_as<uint64_t&>;
+                               };
+
+template <typename T>
+concept HasConfigMembers = requires(T config) {
+                               { config.name } -> std::same_as<std::string&>;
+                               { config.portMode } -> std::same_as<PortMode&>;
+                               { config.baudRate } -> std::same_as<uint32_t&>;
+                               { config.rtsDelay } -> std::same_as<uint16_t&>;
+                           };
+
+template <HasConfigMembers BaseConfig, HasPropertiesMembers BaseProperties>
+auto updateBaseConfig(BaseConfig& config, const BaseProperties& properties)
+    -> bool
+{
+    PHOSPHOR_LOG2_USING;
+
+    config.name = properties.name;
+    config.baudRate = static_cast<uint32_t>(properties.baud_rate);
+    config.rtsDelay = static_cast<uint16_t>(properties.rts_delay);
+
+    for (const auto& [modeStr, portMode] : config::validPortModes)
+    {
+        if (modeStr == properties.mode)
+        {
+            config.portMode = portMode;
+            break;
+        }
+    }
+    if (config.portMode == PortMode::unknown)
+    {
+        error("Invalid port mode {PORT_MODE} for {NAME}", "PORT_MODE",
+              properties.mode, "NAME", properties.name);
+        return false;
+    }
+
+    debug("Base Port config: {NAME} {PORT_MODE} {BAUD_RATE} {RTS_DELAY}",
+          "NAME", config.name, "PORT_MODE", config.portMode, "BAUD_RATE",
+          config.baudRate, "RTS_DELAY", config.rtsDelay);
+
+    return true;
+}
+
+} // namespace config
+
+class BasePort
+{
+  public:
+    explicit BasePort(sdbusplus::async::context& ctx,
+                      const config::Config& config,
+                      const std::string& devicePath);
+
+    auto readHoldingRegisters(uint8_t deviceAddress, uint16_t registerOffset,
+                              uint32_t baudRate, Parity parity,
+                              std::vector<uint16_t>& registers)
+        -> sdbusplus::async::task<bool>;
+
+  private:
+    std::string name;
+    int fd = -1;
+    std::unique_ptr<ModbusIntf> modbus;
+    sdbusplus::async::mutex mutex;
+};
+
+} // namespace phosphor::modbus::rtu::port
diff --git a/rtu/port/port_factory.cpp b/rtu/port/port_factory.cpp
new file mode 100644
index 0000000..b3b5d32
--- /dev/null
+++ b/rtu/port/port_factory.cpp
@@ -0,0 +1,50 @@
+#include "port_factory.hpp"
+
+#include "usb_port.hpp"
+
+#include <xyz/openbmc_project/Configuration/USBPort/client.hpp>
+
+namespace phosphor::modbus::rtu::port
+{
+
+using USBPortConfigIntf =
+    sdbusplus::client::xyz::openbmc_project::configuration::USBPort<>;
+
+auto PortFactory::getInterfaces() -> std::vector<std::string>
+{
+    return {USBPortConfigIntf::interface};
+}
+
+auto PortFactory::getConfig(sdbusplus::async::context& ctx,
+                            const sdbusplus::message::object_path& objectPath,
+                            const std::string& interfaceName)
+    -> sdbusplus::async::task<std::optional<config::PortFactoryConfig>>
+{
+    if (interfaceName == USBPortConfigIntf::interface)
+    {
+        auto res = co_await USBPort::getConfig(ctx, objectPath);
+        if (!res)
+        {
+            co_return std::nullopt;
+        }
+        config::PortFactoryConfig config = res.value();
+        config.portType = config::PortType::usb;
+        co_return config;
+    }
+
+    co_return std::nullopt;
+}
+
+auto PortFactory::create(sdbusplus::async::context& ctx,
+                         config::PortFactoryConfig& config)
+    -> std::unique_ptr<BasePort>
+{
+    if (config.portType == config::PortType::usb)
+    {
+        return std::make_unique<USBPort>(ctx, config);
+    }
+
+    return nullptr;
+}
+
+} // namespace phosphor::modbus::rtu::port
diff --git a/rtu/port/port_factory.hpp b/rtu/port/port_factory.hpp
new file mode 100644
index 0000000..b46ab7b
--- /dev/null
+++ b/rtu/port/port_factory.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "base_port.hpp"
+
+#include <sdbusplus/async.hpp>
+
+#include <string>
+#include <vector>
+
+namespace phosphor::modbus::rtu::port
+{
+
+namespace config
+{
+
+enum class PortType
+{
+    usb,
+    unknown
+};
+
+struct PortFactoryConfig : public Config
+{
+    PortType portType = PortType::unknown;
+};
+
+} // namespace config
+
+class PortFactory
+{
+  public:
+    static auto getInterfaces() -> std::vector<std::string>;
+
+    static auto getConfig(sdbusplus::async::context& ctx,
+                          const sdbusplus::message::object_path& objectPath,
+                          const std::string& interfaceName)
+        -> sdbusplus::async::task<std::optional<config::PortFactoryConfig>>;
+
+    static auto create(sdbusplus::async::context& ctx,
+                       config::PortFactoryConfig& config)
+        -> std::unique_ptr<BasePort>;
+};
+
+} // namespace phosphor::modbus::rtu::port
diff --git a/rtu/port/usb_port.cpp b/rtu/port/usb_port.cpp
new file mode 100644
index 0000000..57a7a8c
--- /dev/null
+++ b/rtu/port/usb_port.cpp
@@ -0,0 +1,99 @@
+#include "usb_port.hpp"
+
+#include "common/entity_manager_interface.hpp"
+#include "port_factory.hpp"
+
+#include <fcntl.h>
+
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/Configuration/USBPort/client.hpp>
+
+#include <filesystem>
+#include <format>
+#include <optional>
+#include <regex>
+
+namespace phosphor::modbus::rtu::port
+{
+
+PHOSPHOR_LOG2_USING;
+
+using USBPortConfigIntf =
+    sdbusplus::client::xyz::openbmc_project::configuration::USBPort<>;
+
+namespace config
+{
+
+struct USBPortConfig : public PortFactoryConfig
+{
+    std::string address = "unknown";
+    uint16_t port = 0;
+    uint16_t interface = 0;
+};
+
+} // namespace config
+
+static auto getDevicePath(const config::Config& inConfig) -> std::string
+{
+    namespace fs = std::filesystem;
+    auto config = static_cast<const config::USBPortConfig&>(inConfig);
+    std::regex pattern(
+        std::format("platform-{}\\.usb-usb.*{}-port{}", config.address,
+                    config.interface, config.port));
+    fs::path searchDir = "/dev/serial/by-path/";
+
+    for (const auto& entry : fs::recursive_directory_iterator(searchDir))
+    {
+        if (entry.is_symlink())
+        {
+            auto filePath = entry.path();
+            if (std::regex_search(filePath.filename().string(), pattern))
+            {
+                return ("/dev/" +
+                        fs::read_symlink(filePath).filename().string());
+            }
+        }
+    }
+
+    throw std::runtime_error("Failed to get device path");
+}
+
+USBPort::USBPort(sdbusplus::async::context& ctx,
+                 config::PortFactoryConfig& config) :
+    BasePort(ctx, config, getDevicePath(config))
+{
+    info("USB port {NAME} created successfully", "NAME", config.name);
+}
+
+auto USBPort::getConfig(sdbusplus::async::context& ctx,
+                        const sdbusplus::message::object_path& objectPath)
+    -> sdbusplus::async::task<std::optional<config::PortFactoryConfig>>
+{
+    config::USBPortConfig config = {};
+
+    auto properties =
+        co_await USBPortConfigIntf(ctx)
+            .service(entity_manager::EntityManagerInterface::serviceName)
+            .path(objectPath.str)
+            .properties();
+
+    auto res = updateBaseConfig(config, properties);
+    if (!res)
+    {
+        co_return std::nullopt;
+    }
+
+    config.address = properties.device_address;
+    config.port = properties.port;
+    config.interface = properties.device_interface;
+
+    debug(
+        "USB port config: {NAME} {PORT_TYPE} {PORT_MODE} {ADDRESS} {PORT} {INTERFACE} {BAUD_RATE} {RTS_DELAY}",
+        "NAME", config.name, "PORT_TYPE", config.portType, "PORT_MODE",
+        config.portMode, "ADDRESS", config.address, "PORT", config.port,
+        "INTERFACE", config.interface, "BAUD_RATE", config.baudRate,
+        "RTS_DELAY", config.rtsDelay);
+    co_return config;
+}
+
+} // namespace phosphor::modbus::rtu::port
diff --git a/rtu/port/usb_port.hpp b/rtu/port/usb_port.hpp
new file mode 100644
index 0000000..5f9cff5
--- /dev/null
+++ b/rtu/port/usb_port.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "base_port.hpp"
+
+#include <sdbusplus/async.hpp>
+
+namespace phosphor::modbus::rtu::port
+{
+
+namespace config
+{
+
+struct PortFactoryConfig;
+
+} // namespace config
+
+class USBPort : public BasePort
+{
+  public:
+    explicit USBPort(sdbusplus::async::context& ctx,
+                     config::PortFactoryConfig& config);
+
+    static auto getConfig(sdbusplus::async::context& ctx,
+                          const sdbusplus::message::object_path& objectPath)
+        -> sdbusplus::async::task<std::optional<config::PortFactoryConfig>>;
+};
+
+} // namespace phosphor::modbus::rtu::port
diff --git a/tests/meson.build b/tests/meson.build
index e6f5afc..2be37a5 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -39,3 +39,14 @@
         include_directories: ['.'],
     ),
 )
+
+test(
+    'test_port',
+    executable(
+        'test_port',
+        'test_port.cpp',
+        'modbus_server_tester.cpp',
+        dependencies: [gtest_dep, gmock_dep, default_deps, modbus_rtu_dep],
+        include_directories: ['.', common_include],
+    ),
+)
diff --git a/tests/test_port.cpp b/tests/test_port.cpp
new file mode 100644
index 0000000..32b9b49
--- /dev/null
+++ b/tests/test_port.cpp
@@ -0,0 +1,165 @@
+#include "modbus_server_tester.hpp"
+#include "port/base_port.hpp"
+
+#include <fcntl.h>
+
+#include <gtest/gtest.h>
+
+using namespace std::literals;
+
+namespace TestIntf = phosphor::modbus::test;
+namespace PortIntf = phosphor::modbus::rtu::port;
+namespace PortConfigIntf = PortIntf::config;
+namespace RTUIntf = phosphor::modbus::rtu;
+
+struct properties_t
+{
+    std::string name = {};
+    std::string mode = {};
+    uint64_t baud_rate = {};
+    uint64_t rts_delay = {};
+};
+
+class MockPort : public PortIntf::BasePort
+{
+  public:
+    MockPort(sdbusplus::async::context& ctx,
+             const PortConfigIntf::Config& config,
+             const std::string& devicePath) : BasePort(ctx, config, devicePath)
+    {}
+};
+
+class PortTest : public ::testing::Test
+{
+  public:
+    static constexpr properties_t properties = {"TestPort", "RS485", 115200, 1};
+    static constexpr const char* clientDevicePath = "/tmp/ttyPortV0";
+    static constexpr const char* serverDevicePath = "/tmp/ttyPortV1";
+    static constexpr const auto defaultBaudeRate = "b115200";
+    int socat_pid = -1;
+    sdbusplus::async::context ctx;
+    int fdClient = -1;
+    std::unique_ptr<TestIntf::ServerTester> serverTester;
+    int fdServer = -1;
+
+    PortTest()
+    {
+        std::string socatCmd = std::format(
+            "socat -x -v -d -d pty,link={},rawer,echo=0,parenb,{} pty,link={},rawer,echo=0,parenb,{} & echo $!",
+            serverDevicePath, defaultBaudeRate, clientDevicePath,
+            defaultBaudeRate);
+
+        // Start socat in the background and capture its PID
+        FILE* fp = popen(socatCmd.c_str(), "r");
+        EXPECT_NE(fp, nullptr) << "Failed to start socat: " << strerror(errno);
+        EXPECT_GT(fscanf(fp, "%d", &socat_pid), 0);
+        pclose(fp);
+
+        // Wait for socat to start up
+        sleep(1);
+
+        fdClient = open(clientDevicePath, O_RDWR | O_NOCTTY | O_NONBLOCK);
+        EXPECT_NE(fdClient, -1)
+            << "Failed to open serial port " << clientDevicePath
+            << " with error: " << strerror(errno);
+
+        fdServer = open(serverDevicePath, O_RDWR | O_NOCTTY | O_NONBLOCK);
+        EXPECT_NE(fdServer, -1)
+            << "Failed to open serial port " << serverDevicePath
+            << " with error: " << strerror(errno);
+
+        serverTester = std::make_unique<TestIntf::ServerTester>(ctx, fdServer);
+    }
+
+    ~PortTest() noexcept override
+    {
+        if (fdClient != -1)
+        {
+            close(fdClient);
+            fdClient = -1;
+        }
+        if (fdServer != -1)
+        {
+            close(fdServer);
+            fdServer = -1;
+        }
+        kill(socat_pid, SIGTERM);
+    }
+
+    auto TestHoldingRegisters(PortConfigIntf::Config& config, MockPort& port,
+                              uint16_t registerOffset, bool res)
+        -> sdbusplus::async::task<void>
+    {
+        std::vector<uint16_t> registers(
+            TestIntf::testSuccessReadHoldingRegisterCount);
+
+        auto ret = co_await port.readHoldingRegisters(
+            TestIntf::testDeviceAddress, registerOffset, config.baudRate,
+            RTUIntf::Parity::none, registers);
+
+        EXPECT_EQ(ret, res) << "Failed to read holding registers";
+
+        if (!res)
+        {
+            co_return;
+        }
+
+        for (auto i = 0; i < TestIntf::testSuccessReadHoldingRegisterCount; i++)
+        {
+            EXPECT_EQ(registers[i],
+                      TestIntf::testSuccessReadHoldingRegisterResponse[i]);
+        }
+
+        co_return;
+    }
+};
+
+TEST_F(PortTest, TestUpdateConfig)
+{
+    PortConfigIntf::Config config = {};
+    auto res = PortConfigIntf::updateBaseConfig(config, properties);
+    EXPECT_TRUE(res) << "Failed to update config";
+
+    EXPECT_EQ(config.name, properties.name);
+    EXPECT_EQ(config.portMode, PortConfigIntf::PortMode::rs485);
+    EXPECT_EQ(config.baudRate, properties.baud_rate);
+    EXPECT_EQ(config.rtsDelay, properties.rts_delay);
+}
+
+TEST_F(PortTest, TestReadHoldingRegisterSuccess)
+{
+    PortConfigIntf::Config config = {};
+    auto res = PortConfigIntf::updateBaseConfig(config, properties);
+    EXPECT_TRUE(res) << "Failed to update config";
+
+    MockPort port(ctx, config, clientDevicePath);
+
+    ctx.spawn(serverTester->processRequests());
+
+    ctx.spawn(TestHoldingRegisters(
+        config, port, TestIntf::testSuccessReadHoldingRegisterOffset, true));
+
+    ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+              sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+    ctx.run();
+}
+
+TEST_F(PortTest, TestReadHoldingRegisterFailure)
+{
+    PortConfigIntf::Config config = {};
+    auto res = PortConfigIntf::updateBaseConfig(config, properties);
+    EXPECT_TRUE(res) << "Failed to update config";
+
+    MockPort port(ctx, config, clientDevicePath);
+
+    ctx.spawn(serverTester->processRequests());
+
+    ctx.spawn(TestHoldingRegisters(
+        config, port, TestIntf::testFailureReadHoldingRegister, false));
+
+    ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+              sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+    ctx.run();
+}