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/rtu/inventory/modbus_inventory.cpp b/rtu/inventory/modbus_inventory.cpp
new file mode 100644
index 0000000..16fa56a
--- /dev/null
+++ b/rtu/inventory/modbus_inventory.cpp
@@ -0,0 +1,435 @@
+#include "modbus_inventory.hpp"
+
+#include "common/entity_manager_interface.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/Configuration/ModbusRTUDetect/client.hpp>
+#include <xyz/openbmc_project/Inventory/Item/client.hpp>
+
+#include <flat_map>
+
+namespace phosphor::modbus::rtu::inventory
+{
+PHOSPHOR_LOG2_USING;
+
+namespace config
+{
+
+using BasicVariantType =
+    std::variant<std::vector<std::string>, std::vector<uint8_t>, std::string,
+                 int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
+                 uint16_t, uint8_t, bool>;
+using InventoryBaseConfigMap = std::flat_map<std::string, BasicVariantType>;
+using InventoryData = std::flat_map<std::string, InventoryBaseConfigMap>;
+using ManagedObjectType =
+    std::flat_map<sdbusplus::message::object_path, InventoryData>;
+
+static constexpr std::array<std::pair<std::string_view, Parity>, 3>
+    validParities = {
+        {{"Odd", Parity::odd}, {"Even", Parity::even}, {"None", Parity::none}}};
+
+// TODO: This API will be dropped once EM supports non-indexed interfaces for
+// array objects.
+static auto processModbusAddressInterface(
+    Config& config, const InventoryBaseConfigMap& configMap) -> bool
+{
+    debug("Processing ModbusAddress {NAME}", "NAME", config.name);
+
+    auto rangeStartIter = configMap.find("RangeStart");
+    if (rangeStartIter == configMap.end())
+    {
+        error("Missing RangeStart for {NAME}", "NAME", config.name);
+        return false;
+    }
+    auto rangeStart = std::get<uint64_t>(rangeStartIter->second);
+
+    auto rangeEndIter = configMap.find("RangeEnd");
+    if (rangeEndIter == configMap.end())
+    {
+        error("Missing RangeEnd for {NAME}", "NAME", config.name);
+        return false;
+    }
+    auto rangeEnd = std::get<uint64_t>(rangeEndIter->second);
+
+    auto serialPortIter = configMap.find("SerialPort");
+    if (serialPortIter == configMap.end())
+    {
+        error("Missing SerialPort for {NAME}", "NAME", config.name);
+        return false;
+    }
+    auto serialPort = std::get<std::string>(serialPortIter->second);
+
+    config.addressMap[serialPort].push_back(AddressRange{
+        static_cast<uint8_t>(rangeStart), static_cast<uint8_t>(rangeEnd)});
+
+    debug("ModbusAddress {NAME} {PORT} {START} {END}", "NAME", config.name,
+          "PORT", serialPort, "START", rangeStart, "END", rangeEnd);
+
+    return true;
+}
+
+// TODO: This API will be dropped once EM supports non-indexed interfaces for
+// array objects.
+static auto processModbusRegistersInterface(
+    Config& config, const InventoryBaseConfigMap& configMap) -> bool
+{
+    debug("Processing ModbusRegisters {NAME}", "NAME", config.name);
+    Register registerConfig = {};
+
+    auto nameIter = configMap.find("Name");
+    if (nameIter == configMap.end())
+    {
+        error("Missing Name for {NAME}", "NAME", config.name);
+        return false;
+    }
+    registerConfig.name = std::get<std::string>(nameIter->second);
+
+    auto address = configMap.find("Address");
+    if (address == configMap.end())
+    {
+        error("Missing Address for {NAME}", "NAME", config.name);
+        return false;
+    }
+    registerConfig.offset = std::get<uint64_t>(address->second);
+
+    auto sizeIter = configMap.find("Size");
+    if (sizeIter == configMap.end())
+    {
+        error("Missing Size for {NAME}", "NAME", config.name);
+        return false;
+    }
+    registerConfig.size = std::get<uint64_t>(sizeIter->second);
+
+    config.registers.push_back(registerConfig);
+
+    debug("ModbusRegisters {NAME} {ADDRESS} {SIZE}", "NAME",
+          registerConfig.name, "ADDRESS", registerConfig.offset, "SIZE",
+          registerConfig.size);
+
+    return true;
+}
+
+static auto printConfig(const Config& config) -> void
+{
+    info("Inventory device config: {NAME} {BAUDRATE} {PARITY}", "NAME",
+         config.name, "BAUDRATE", config.baudRate, "PARITY", config.parity);
+
+    for (const auto& [port, addressRanges] : config.addressMap)
+    {
+        for (const auto& addressRange : addressRanges)
+        {
+            info(
+                "Inventory device config: {PORT} {ADDRESS_START} {ADDRESS_END}",
+                "PORT", port, "ADDRESS_START", addressRange.start,
+                "ADDRESS_END", addressRange.end);
+        }
+    }
+
+    for (const auto& registerConfig : config.registers)
+    {
+        info("Inventory device config: {NAME} {ADDRESS} {SIZE}", "NAME",
+             registerConfig.name, "ADDRESS", registerConfig.offset, "SIZE",
+             registerConfig.size);
+    }
+}
+
+auto getConfigSubInterfaces(sdbusplus::async::context& ctx,
+                            sdbusplus::message::object_path objectPath,
+                            Config& config) -> sdbusplus::async::task<bool>
+{
+    constexpr auto modbusAddressInterface =
+        "xyz.openbmc_project.Configuration.ModbusRTUDetect.Address";
+    constexpr auto modbusRegistersInterface =
+        "xyz.openbmc_project.Configuration.ModbusRTUDetect.Registers";
+
+    using InventoryIntf =
+        sdbusplus::client::xyz::openbmc_project::inventory::Item<>;
+
+    constexpr auto entityManager =
+        sdbusplus::async::proxy()
+            .service(entity_manager::EntityManagerInterface::serviceName)
+            .path(InventoryIntf::namespace_path)
+            .interface("org.freedesktop.DBus.ObjectManager");
+
+    for (const auto& [path, deviceConfig] :
+         co_await entityManager.call<ManagedObjectType>(ctx,
+                                                        "GetManagedObjects"))
+    {
+        if (!(path.str).starts_with(objectPath.str))
+        {
+            debug("Skipping device {PATH}", "PATH", path.str);
+            continue;
+        }
+        debug("Processing device {PATH}", "PATH", path.str);
+        for (const auto& [interfaceName, interfaceConfig] : deviceConfig)
+        {
+            if (interfaceName.starts_with(modbusAddressInterface))
+            {
+                if (!processModbusAddressInterface(config, interfaceConfig))
+                {
+                    error("Failed to process {INTERFACE} for {NAME}",
+                          "INTERFACE", modbusAddressInterface, "NAME",
+                          config.name);
+                    co_return false;
+                }
+            }
+            else if (interfaceName.starts_with(modbusRegistersInterface))
+            {
+                if (!processModbusRegistersInterface(config, interfaceConfig))
+                {
+                    error("Failed to process {INTERFACE} for {NAME}",
+                          "INTERFACE", modbusRegistersInterface, "NAME",
+                          config.name);
+                    co_return false;
+                }
+            }
+        }
+    }
+
+    co_return true;
+}
+
+auto getConfig(sdbusplus::async::context& ctx,
+               sdbusplus::message::object_path objectPath)
+    -> sdbusplus::async::task<std::optional<Config>>
+{
+    using ModbusRTUDetectIntf = sdbusplus::client::xyz::openbmc_project::
+        configuration::ModbusRTUDetect<>;
+
+    Config config = {};
+
+    auto properties =
+        co_await ModbusRTUDetectIntf(ctx)
+            .service(entity_manager::EntityManagerInterface::serviceName)
+            .path(objectPath.str)
+            .properties();
+
+    config.name = properties.name;
+    config.baudRate = properties.baud_rate;
+
+    for (const auto& [parityStr, parity] : config::validParities)
+    {
+        if (parityStr == properties.data_parity)
+        {
+            config.parity = parity;
+            break;
+        }
+    }
+    if (config.parity == Parity::unknown)
+    {
+        error("Invalid parity {PARITY} for {NAME}", "PARITY",
+              properties.data_parity, "NAME", properties.name);
+        co_return std::nullopt;
+    }
+
+    if (!co_await getConfigSubInterfaces(ctx, objectPath, config))
+    {
+        co_return std::nullopt;
+    }
+
+    printConfig(config);
+
+    co_return config;
+}
+
+} // namespace config
+
+Device::Device(sdbusplus::async::context& ctx, const config::Config& config,
+               serial_port_map_t& serialPorts) :
+    ctx(ctx), config(config), serialPorts(serialPorts)
+{
+    for (const auto& [serialPort, _] : config.addressMap)
+    {
+        if (serialPorts.find(serialPort) == serialPorts.end())
+        {
+            error("Serial port {PORT} not found for {NAME}", "PORT", serialPort,
+                  "NAME", config.name);
+            continue;
+        }
+    }
+}
+
+auto Device::probePorts() -> sdbusplus::async::task<void>
+{
+    debug("Probing ports for {NAME}", "NAME", config.name);
+    while (!ctx.stop_requested())
+    {
+        for (const auto& [serialPort, _] : config.addressMap)
+        {
+            if (serialPorts.find(serialPort) == serialPorts.end())
+            {
+                continue;
+            }
+            ctx.spawn(probePort(serialPort));
+        }
+        constexpr auto probeInterval = 3;
+        co_await sdbusplus::async::sleep_for(
+            ctx, std::chrono::seconds(probeInterval));
+        debug("Probing ports for {NAME} in {INTERVAL} seconds", "NAME",
+              config.name, "INTERVAL", probeInterval);
+    }
+}
+
+auto Device::probePort(std::string portName) -> sdbusplus::async::task<void>
+{
+    debug("Probing port {PORT}", "PORT", portName);
+
+    auto portConfig = config.addressMap.find(portName);
+    if (portConfig == config.addressMap.end())
+    {
+        error("Serial port {PORT} address map not found for {NAME}", "PORT",
+              portName, "NAME", config.name);
+        co_return;
+    }
+    auto addressRanges = portConfig->second;
+
+    auto port = serialPorts.find(portName);
+    if (port == serialPorts.end())
+    {
+        error("Serial port {PORT} not found for {NAME}", "PORT", portName,
+              "NAME", config.name);
+        co_return;
+    }
+
+    for (const auto& addressRange : addressRanges)
+    {
+        for (auto address = addressRange.start; address <= addressRange.end;
+             address++)
+        {
+            co_await probeDevice(address, portName, *port->second);
+        }
+    }
+}
+
+auto Device::probeDevice(uint8_t address, const std::string& portName,
+                         SerialPortIntf& port) -> sdbusplus::async::task<void>
+{
+    debug("Probing device at {ADDRESS} on port {PORT}", "ADDRESS", address,
+          "PORT", portName);
+
+    if (config.registers.size() == 0)
+    {
+        error("No registers configured for {NAME}", "NAME", config.name);
+        co_return;
+    }
+    auto probeRegister = config.registers[0].offset;
+    auto registers = std::vector<uint16_t>(config.registers[0].size);
+
+    auto sourceId = std::to_string(address) + "_" + portName;
+
+    auto ret = co_await port.readHoldingRegisters(
+        address, probeRegister, config.baudRate, config.parity, registers);
+    if (ret)
+    {
+        if (inventorySources.find(sourceId) == inventorySources.end())
+        {
+            debug("Device found at {ADDRESS}", "ADDRESS", address);
+            co_await addInventorySource(address, portName, port);
+        }
+        else
+        {
+            debug("Device already exists at {ADDRESS}", "ADDRESS", address);
+        }
+    }
+    else
+    {
+        if (inventorySources.find(sourceId) != inventorySources.end())
+        {
+            warning(
+                "Device removed at {ADDRESS} due to probe failure for {PROBE_REGISTER}",
+                "ADDRESS", address, "PROBE_REGISTER", probeRegister);
+            inventorySources[sourceId]->emit_removed();
+            inventorySources.erase(sourceId);
+        }
+    }
+}
+
+static auto fillInventorySourceProperties(
+    InventorySourceIntf::properties_t& properties, const std::string& regName,
+    std::string& strValue) -> void
+{
+    constexpr auto partNumber = "PartNumber";
+    constexpr auto sparePartNumber = "SparePartNumber";
+    constexpr auto serialNumber = "SerialNumber";
+    constexpr auto buildDate = "BuildDate";
+    constexpr auto model = "Model";
+    constexpr auto manufacturer = "Manufacturer";
+
+    if (regName == partNumber)
+    {
+        properties.part_number = strValue;
+    }
+    else if (regName == sparePartNumber)
+    {
+        properties.spare_part_number = strValue;
+    }
+    else if (regName == serialNumber)
+    {
+        properties.serial_number = strValue;
+    }
+    else if (regName == buildDate)
+    {
+        properties.build_date = strValue;
+    }
+    else if (regName == model)
+    {
+        properties.model = strValue;
+    }
+    else if (regName == manufacturer)
+    {
+        properties.manufacturer = strValue;
+    }
+}
+
+auto Device::addInventorySource(uint8_t address, const std::string& portName,
+                                SerialPortIntf& port)
+    -> sdbusplus::async::task<void>
+{
+    InventorySourceIntf::properties_t properties;
+
+    for (const auto& reg : config.registers)
+    {
+        auto registers = std::vector<uint16_t>(reg.size);
+        auto ret = co_await port.readHoldingRegisters(
+            address, reg.offset, config.baudRate, config.parity, registers);
+        if (!ret)
+        {
+            error(
+                "Failed to read holding registers {NAME} for {DEVICE_ADDRESS}",
+                "NAME", reg.name, "DEVICE_ADDRESS", address);
+            continue;
+        }
+
+        std::string strValue = "";
+
+        // Reswap bytes in each register for string conversion
+        for (const auto& value : registers)
+        {
+            strValue += static_cast<char>((value >> 8) & 0xFF);
+            strValue += static_cast<char>(value & 0xFF);
+        }
+
+        fillInventorySourceProperties(properties, reg.name, strValue);
+    }
+
+    auto pathSuffix =
+        config.name + " " + std::to_string(address) + " " + portName;
+
+    properties.name = pathSuffix;
+    properties.address = address;
+    properties.link_tty = portName;
+
+    std::replace(pathSuffix.begin(), pathSuffix.end(), ' ', '_');
+
+    auto objectPath =
+        std::string(InventorySourceIntf::namespace_path) + "/" + pathSuffix;
+    auto sourceId = std::to_string(address) + "_" + portName;
+
+    inventorySources[sourceId] = std::make_unique<InventorySourceIntf>(
+        ctx, objectPath.c_str(), properties);
+    inventorySources[sourceId]->emit_added();
+
+    info("Added InventorySource at {PATH}", "PATH", objectPath);
+}
+
+} // namespace phosphor::modbus::rtu::inventory