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