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