rtu: implement modbus rtu inventory source service
Implement phosphor-modbus-rtu inventory source service based on [1].
[1]: https://gerrit.openbmc.org/c/openbmc/docs/+/77318
Tested: Unit test passes and tested on qemu with simulated modbus
server.
```
root@bmc:~# busctl tree xyz.openbmc_project.ModbusRTU
└─ /xyz
└─ /xyz/openbmc_project
└─ /xyz/openbmc_project/inventory_source
└─ /xyz/openbmc_project/inventory_source/modbus
├─ /xyz/openbmc_project/inventory_source/modbus/Heat_Exchanger_12_DevTTYUSB0
├─ /xyz/openbmc_project/inventory_source/modbus/Heat_Exchanger_12_DevTTYUSB1
├─ /xyz/openbmc_project/inventory_source/modbus/Reservoir_Pumping_Unit_12_DevTTYUSB0
└─ /xyz/openbmc_project/inventory_source/modbus/Reservoir_Pumping_Unit_12_DevTTYUSB1
root@bmc:~# busctl tree xyz.openbmc_project.EntityManager
└─ /xyz
└─ /xyz/openbmc_project
├─ /xyz/openbmc_project/EntityManager
└─ /xyz/openbmc_project/inventory
└─ /xyz/openbmc_project/inventory/system
├─ /xyz/openbmc_project/inventory/system/board
│ └─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus
│ ├─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus/DevTTYUSB0
│ ├─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus/DevTTYUSB1
│ ├─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus/Heat_Exchanger
│ └─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus/Reservoir_Pumping_Unit
└─ /xyz/openbmc_project/inventory/system/chassis
├─ /xyz/openbmc_project/inventory/system/chassis/Heat_Exchanger_12_DevTTYUSB0
├─ /xyz/openbmc_project/inventory/system/chassis/Heat_Exchanger_12_DevTTYUSB1
├─ /xyz/openbmc_project/inventory/system/chassis/Reservoir_Pumping_Unit_12_DevTTYUSB0
└─ /xyz/openbmc_project/inventory/system/chassis/Reservoir_Pumping_Unit_12_DevTTYUSB1
root@bmc:~# busctl introspect xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory/system/chassis/Heat_Exchanger_12_DevTTYUSB0
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
org.freedesktop.DBus.Introspectable interface - - -
.Introspect method - s -
org.freedesktop.DBus.Peer interface - - -
.GetMachineId method - s -
.Ping method - - -
org.freedesktop.DBus.Properties interface - - -
.Get method ss v -
.GetAll method s a{sv} -
.Set method ssv - -
.PropertiesChanged signal sa{sv}as - -
xyz.openbmc_project.AddObject interface - - -
.AddObject method a{sv} - -
xyz.openbmc_project.Inventory.Decorator.Asset interface - - -
.BuildDate property s "Unknown" emits-change
.Manufacturer property s "Unknown" emits-change
.Model property s "Unknown" emits-change
.PartNumber property s "Unknown" emits-change
.SerialNumber property s "Unknown" emits-change
.SparePartNumber property s "ABABABAB" emits-change
xyz.openbmc_project.Inventory.Item.Chassis interface - - -
.Name property s "Heat Exchanger 12 DevTTYUSB0" emits-change
.Probe property s "xyz.openbmc_project.Inventory.Source.M… emits-change
.Type property s "Chassis" emits-change
root@bmc:~# busctl introspect xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory/system/chassis/Heat_Exchanger_12_DevTTYUSB1
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
org.freedesktop.DBus.Introspectable interface - - -
.Introspect method - s -
org.freedesktop.DBus.Peer interface - - -
.GetMachineId method - s -
.Ping method - - -
org.freedesktop.DBus.Properties interface - - -
.Get method ss v -
.GetAll method s a{sv} -
.Set method ssv - -
.PropertiesChanged signal sa{sv}as - -
xyz.openbmc_project.AddObject interface - - -
.AddObject method a{sv} - -
xyz.openbmc_project.Inventory.Decorator.Asset interface - - -
.BuildDate property s "Unknown" emits-change
.Manufacturer property s "Unknown" emits-change
.Model property s "Unknown" emits-change
.PartNumber property s "Unknown" emits-change
.SerialNumber property s "Unknown" emits-change
.SparePartNumber property s "ABABABAB" emits-change
xyz.openbmc_project.Inventory.Item.Chassis interface - - -
.Name property s "Heat Exchanger 12 DevTTYUSB1" emits-change
.Probe property s "xyz.openbmc_project.Inventory.Source.M… emits-change
.Type property s "Chassis" emits-change
root@bmc:~# busctl introspect xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory/system/chassis/Reservoir_Pumping_Unit_12_DevTTYUSB0
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
org.freedesktop.DBus.Introspectable interface - - -
.Introspect method - s -
org.freedesktop.DBus.Peer interface - - -
.GetMachineId method - s -
.Ping method - - -
org.freedesktop.DBus.Properties interface - - -
.Get method ss v -
.GetAll method s a{sv} -
.Set method ssv - -
.PropertiesChanged signal sa{sv}as - -
xyz.openbmc_project.AddObject interface - - -
.AddObject method a{sv} - -
xyz.openbmc_project.Inventory.Decorator.Asset interface - - -
.BuildDate property s "ABABABAB" emits-change
.Manufacturer property s "Unknown" emits-change
.Model property s "ABABABABABABABAB" emits-change
.PartNumber property s "Unknown" emits-change
.SerialNumber property s "ABABABABABABABAB" emits-change
.SparePartNumber property s "ABABABAB" emits-change
xyz.openbmc_project.Inventory.Item.Chassis interface - - -
.Name property s "Reservoir Pumping Unit 12 DevTTYUSB0" emits-change
.Probe property s "xyz.openbmc_project.Inventory.Source.M… emits-change
.Type property s "Chassis" emits-change
root@bmc:~# busctl introspect xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory/system/chassis/Reservoir_Pumping_Unit_12_DevTTYUSB1
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
org.freedesktop.DBus.Introspectable interface - - -
.Introspect method - s -
org.freedesktop.DBus.Peer interface - - -
.GetMachineId method - s -
.Ping method - - -
org.freedesktop.DBus.Properties interface - - -
.Get method ss v -
.GetAll method s a{sv} -
.Set method ssv - -
.PropertiesChanged signal sa{sv}as - -
xyz.openbmc_project.AddObject interface - - -
.AddObject method a{sv} - -
xyz.openbmc_project.Inventory.Decorator.Asset interface - - -
.BuildDate property s "ABABABAB" emits-change
.Manufacturer property s "Unknown" emits-change
.Model property s "ABABABABABABABAB" emits-change
.PartNumber property s "Unknown" emits-change
.SerialNumber property s "ABABABABABABABAB" emits-change
.SparePartNumber property s "ABABABAB" emits-change
xyz.openbmc_project.Inventory.Item.Chassis interface - - -
.Name property s "Reservoir Pumping Unit 12 DevTTYUSB1" emits-change
.Probe property s "xyz.openbmc_project.Inventory.Source.M… emits-change
.Type property s "Chassis" emits-change
```
Change-Id: Ic0ea739de3833044c95da8164be1e2f3f8e6a063
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/tests/meson.build b/tests/meson.build
index 2be37a5..bcc4fad 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -50,3 +50,15 @@
include_directories: ['.', common_include],
),
)
+
+test(
+ 'test_inventory',
+ executable(
+ 'test_inventory',
+ 'test_inventory.cpp',
+ 'modbus_server_tester.cpp',
+ inventory_src,
+ dependencies: [gtest_dep, gmock_dep, default_deps, modbus_rtu_dep],
+ include_directories: ['.', common_include],
+ ),
+)
diff --git a/tests/modbus_server_tester.cpp b/tests/modbus_server_tester.cpp
index a08e51f..fe1b825 100644
--- a/tests/modbus_server_tester.cpp
+++ b/tests/modbus_server_tester.cpp
@@ -21,11 +21,15 @@
constexpr uint8_t readHoldingRegistersErrorFunctionCode = 0x83;
ServerTester::ServerTester(sdbusplus::async::context& ctx, int fd) :
- fd(fd), fdioInstance(ctx, fd)
+ fd(fd), fdioInstance(ctx, fd), mutex("TestMutex")
{}
auto ServerTester::processRequests() -> sdbusplus::async::task<void>
{
+ // Acquire lock to guard against concurrent access to fdioInstance
+ sdbusplus::async::lock_guard lg{mutex};
+ co_await lg.lock();
+
MessageIntf request;
co_await fdioInstance.next();
auto ret = read(fd, request.raw.data(), request.raw.size());
@@ -94,6 +98,11 @@
}
}
+static inline void checkRequestSize(size_t requestSize, size_t expectedSize)
+{
+ EXPECT_EQ(requestSize, expectedSize) << "Invalid request size";
+}
+
void ServerTester::processReadHoldingRegisters(
MessageIntf& request, size_t requestSize, MessageIntf& response,
bool& segmentedResponse)
@@ -113,11 +122,11 @@
uint16_t registerOffset = request.raw[2] << 8 | request.raw[3];
uint16_t registerCount = request.raw[4] << 8 | request.raw[5];
- EXPECT_EQ(registerCount, testSuccessReadHoldingRegisterCount);
-
if (registerOffset == testSuccessReadHoldingRegisterOffset ||
registerOffset == testSuccessReadHoldingRegisterSegmentedOffset)
{
+ checkRequestSize(registerCount, testSuccessReadHoldingRegisterCount);
+
response << request.raw[0] << request.raw[1]
<< uint8_t(2 * registerCount)
<< uint16_t(testSuccessReadHoldingRegisterResponse[0])
@@ -126,8 +135,22 @@
segmentedResponse =
(registerOffset == testSuccessReadHoldingRegisterSegmentedOffset);
}
+ else if (registerOffset == testReadHoldingRegisterModelOffset)
+ {
+ checkRequestSize(registerCount, testReadHoldingRegisterModelCount);
+
+ response << request.raw[0] << request.raw[1]
+ << uint8_t(2 * testReadHoldingRegisterModelCount);
+ for (size_t i = 0; i < testReadHoldingRegisterModelCount; i++)
+ {
+ response << uint16_t(testReadHoldingRegisterModel[i]);
+ }
+ response.appendCRC();
+ }
else if (registerOffset == testFailureReadHoldingRegister)
{
+ checkRequestSize(registerCount, testSuccessReadHoldingRegisterCount);
+
response << request.raw[0]
<< (uint8_t)readHoldingRegistersErrorFunctionCode
<< uint8_t(RTUIntf::ModbusExceptionCode::illegalFunctionCode);
diff --git a/tests/modbus_server_tester.hpp b/tests/modbus_server_tester.hpp
index 79955a0..c9b4416 100644
--- a/tests/modbus_server_tester.hpp
+++ b/tests/modbus_server_tester.hpp
@@ -14,6 +14,7 @@
friend class ServerTester;
};
+// Read Holding Registers Testing Constants
static constexpr uint8_t testDeviceAddress = 0xa;
constexpr uint16_t testSuccessReadHoldingRegisterOffset = 0x0102;
constexpr uint16_t testSuccessReadHoldingRegisterCount = 0x2;
@@ -22,6 +23,14 @@
testSuccessReadHoldingRegisterResponse = {0x1234, 0x5678};
constexpr uint16_t testFailureReadHoldingRegister = 0x0105;
+// Device Inventory Testing Constants
+constexpr uint16_t testReadHoldingRegisterModelOffset = 0x0112;
+constexpr uint16_t testReadHoldingRegisterModelCount = 0x8;
+constexpr std::array<uint16_t, testReadHoldingRegisterModelCount>
+ testReadHoldingRegisterModel = {0x5244, 0x4630, 0x3430, 0x4453,
+ 0x5335, 0x3139, 0x0000, 0x3000};
+constexpr std::string testReadHoldingRegisterModelStr = "RDF040DSS519";
+
class ServerTester
{
public:
@@ -39,5 +48,6 @@
int fd;
sdbusplus::async::fdio fdioInstance;
+ sdbusplus::async::mutex mutex;
};
} // namespace phosphor::modbus::test
diff --git a/tests/test_inventory.cpp b/tests/test_inventory.cpp
new file mode 100644
index 0000000..b54b3a7
--- /dev/null
+++ b/tests/test_inventory.cpp
@@ -0,0 +1,171 @@
+#include "inventory/modbus_inventory.hpp"
+#include "modbus_server_tester.hpp"
+#include "port/base_port.hpp"
+
+#include <fcntl.h>
+
+#include <xyz/openbmc_project/Inventory/Source/Modbus/FRU/client.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace std::literals;
+using namespace testing;
+using InventorySourceIntf =
+ sdbusplus::client::xyz::openbmc_project::inventory::source::modbus::FRU<>;
+
+namespace TestIntf = phosphor::modbus::test;
+namespace ModbusIntf = phosphor::modbus::rtu;
+namespace PortIntf = phosphor::modbus::rtu::port;
+namespace PortConfigIntf = PortIntf::config;
+namespace InventoryIntf = phosphor::modbus::rtu::inventory;
+namespace InventoryConfigIntf = InventoryIntf::config;
+
+class MockPort : public PortIntf::BasePort
+{
+ public:
+ MockPort(sdbusplus::async::context& ctx,
+ const PortConfigIntf::Config& config,
+ const std::string& devicePath) : BasePort(ctx, config, devicePath)
+ {}
+};
+
+class InventoryTest : public ::testing::Test
+{
+ public:
+ PortConfigIntf::Config portConfig;
+ static constexpr const char* clientDevicePath =
+ "/tmp/ttyInventoryTestPort0";
+ static constexpr const char* serverDevicePath =
+ "/tmp/ttyInventoryTestPort1";
+ static constexpr const auto defaultBaudeRate = "b115200";
+ static constexpr const auto deviceName = "Test1";
+ static constexpr auto serviceName = "xyz.openbmc_project.TestModbusRTU";
+ int socat_pid = -1;
+ sdbusplus::async::context ctx;
+ int fdClient = -1;
+ std::unique_ptr<TestIntf::ServerTester> serverTester;
+ int fdServer = -1;
+
+ InventoryTest()
+ {
+ portConfig.name = "TestPort1";
+ portConfig.portMode = PortConfigIntf::PortMode::rs485;
+ portConfig.baudRate = 115200;
+ portConfig.rtsDelay = 1;
+
+ 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);
+
+ ctx.request_name(serviceName);
+
+ serverTester = std::make_unique<TestIntf::ServerTester>(ctx, fdServer);
+ }
+
+ ~InventoryTest() noexcept override
+ {
+ if (fdClient != -1)
+ {
+ close(fdClient);
+ fdClient = -1;
+ }
+ if (fdServer != -1)
+ {
+ close(fdServer);
+ fdServer = -1;
+ }
+ kill(socat_pid, SIGTERM);
+ }
+
+ auto testInventorySourceCreation(std::string objPath)
+ -> sdbusplus::async::task<void>
+ {
+ InventoryConfigIntf::Config::port_address_map_t addressMap;
+ addressMap[portConfig.name] = {{.start = TestIntf::testDeviceAddress,
+ .end = TestIntf::testDeviceAddress}};
+ InventoryConfigIntf::Config deviceConfig = {
+ .name = deviceName,
+ .addressMap = addressMap,
+ .registers = {{"Model",
+ TestIntf::testReadHoldingRegisterModelOffset,
+ TestIntf::testReadHoldingRegisterModelCount}},
+ .parity = ModbusIntf::Parity::none,
+ .baudRate = 115200};
+ InventoryIntf::Device::serial_port_map_t ports;
+ ports[portConfig.name] =
+ std::make_unique<MockPort>(ctx, portConfig, clientDevicePath);
+
+ auto inventoryDevice =
+ std::make_unique<InventoryIntf::Device>(ctx, deviceConfig, ports);
+
+ co_await inventoryDevice->probePorts();
+
+ // Create InventorySource client interface to read back D-Bus properties
+ auto properties = co_await InventorySourceIntf(ctx)
+ .service(serviceName)
+ .path(objPath)
+ .properties();
+
+ constexpr auto defaultInventoryValue = "Unknown";
+
+ EXPECT_EQ(properties.name,
+ std::format("{} {} {}", deviceName,
+ TestIntf::testDeviceAddress, portConfig.name))
+ << "Name mismatch";
+ EXPECT_EQ(properties.address, TestIntf::testDeviceAddress)
+ << "Address mismatch";
+ EXPECT_EQ(properties.link_tty, portConfig.name) << "Link TTY mismatch";
+ EXPECT_EQ(properties.model, TestIntf::testReadHoldingRegisterModelStr)
+ << "Model mismatch";
+ EXPECT_EQ(properties.serial_number, defaultInventoryValue)
+ << "Part Number mismatch";
+
+ co_return;
+ }
+
+ void SetUp() override
+ {
+ // Process request for probe device call
+ ctx.spawn(serverTester->processRequests());
+
+ // Process request to read `Model` holding register call
+ ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+ sdbusplus::async::execution::then([&]() {
+ ctx.spawn(serverTester->processRequests());
+ }));
+ }
+};
+
+TEST_F(InventoryTest, TestAddInventorySource)
+{
+ auto objPath =
+ std::format("{}/{}_{}_{}", InventorySourceIntf::namespace_path,
+ deviceName, TestIntf::testDeviceAddress, portConfig.name);
+
+ ctx.spawn(testInventorySourceCreation(objPath));
+
+ ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+ sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+ ctx.run();
+}