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/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