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