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();
+}