add firmware inventory support

Add support to display the firmware version using the Firmware version
register from EM configuration. The version will be exposed as
xyz.openbmc_project.Software.Version interface on the Dbus.

Tested:
Unit Test -
```
> meson test -t 10 -C builddir/ --print-errorlogs --wrapper="valgrind --error-exitcode=1" test_firmware
ninja: Entering directory `/host/repos/Modbus/phosphor-modbus/builddir'
ninja: no work to do.
1/1 test_firmware        OK               3.37s

Ok:                1
Fail:              0
```

Tested on Qemu -
```
> busctl tree xyz.openbmc_project.ModbusRTU
`- /xyz
  `- /xyz/openbmc_project
    |- /xyz/openbmc_project/inventory_source
    | |- /xyz/openbmc_project/inventory_source/Heat_Exchanger_12_DevTTYUSB0
    | |- /xyz/openbmc_project/inventory_source/Heat_Exchanger_12_DevTTYUSB1
    | |- /xyz/openbmc_project/inventory_source/Reservoir_Pumping_Unit_12_DevTTYUSB0
    | `- /xyz/openbmc_project/inventory_source/Reservoir_Pumping_Unit_12_DevTTYUSB1
    |- /xyz/openbmc_project/sensors
    | `- /xyz/openbmc_project/sensors/temperature
    |   |- /xyz/openbmc_project/sensors/temperature/Reservoir_Pumping_Unit_12_DevTTYUSB0_RPU_Coolant_Inlet_Temp_C
    |   |- /xyz/openbmc_project/sensors/temperature/Reservoir_Pumping_Unit_12_DevTTYUSB0_RPU_Coolant_Outlet_Temp_C
    |   |- /xyz/openbmc_project/sensors/temperature/Reservoir_Pumping_Unit_12_DevTTYUSB1_RPU_Coolant_Inlet_Temp_C
    |   `- /xyz/openbmc_project/sensors/temperature/Reservoir_Pumping_Unit_12_DevTTYUSB1_RPU_Coolant_Outlet_Temp_C
    `- /xyz/openbmc_project/software
      |- /xyz/openbmc_project/software/Reservoir_Pumping_Unit_12_DevTTYUSB0_RPU_PLC_FW_Revision_9071
      `- /xyz/openbmc_project/software/Reservoir_Pumping_Unit_12_DevTTYUSB1_RPU_PLC_FW_Revision_8053

> busctl  introspect xyz.openbmc_project.ModbusRTU /xyz/openbmc_project/software/Reservoir_Pumping_Unit_12_DevTTYUSB0_RPU_PLC_FW_Revision_9071
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.Association.Definitions interface -         -                                        -
.Associations                               property  a(sss)    1 "running" "ran_on" "/xyz/openbmc_pr... emits-change writable
xyz.openbmc_project.Software.Activation     interface -         -                                        -
.Activation                                 property  s         "xyz.openbmc_project.Software.Activat... emits-change writable
.RequestedActivation                        property  s         "xyz.openbmc_project.Software.Activat... emits-change writable
xyz.openbmc_project.Software.Version        interface -         -                                        -
.Purpose                                    property  s         "xyz.openbmc_project.Software.Version... emits-change writable
.Version                                    property  s         "ABABABAB"                               emits-change writable
```

Change-Id: I985e12ef88547585cca93569b083f347e74a8695
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/rtu/device/base_device.cpp b/rtu/device/base_device.cpp
index c075d55..80b6748 100644
--- a/rtu/device/base_device.cpp
+++ b/rtu/device/base_device.cpp
@@ -17,6 +17,13 @@
 {
     createSensors();
 
+    if (!config.firmwareRegisters.empty())
+    {
+        currentFirmware =
+            std::make_unique<DeviceFirmware>(ctx, config, serialPort);
+        ctx.spawn(currentFirmware->readVersionRegister());
+    }
+
     info("Successfully created device {NAME}", "NAME", config.name);
 }
 
diff --git a/rtu/device/base_device.hpp b/rtu/device/base_device.hpp
index 3a2f0a8..2310f1e 100644
--- a/rtu/device/base_device.hpp
+++ b/rtu/device/base_device.hpp
@@ -2,7 +2,7 @@
 
 #include "base_config.hpp"
 #include "common/events.hpp"
-#include "modbus/modbus.hpp"
+#include "firmware/device_firmware.hpp"
 #include "port/base_port.hpp"
 
 #include <sdbusplus/async.hpp>
@@ -45,6 +45,7 @@
     const config::Config config;
     PortIntf& serialPort;
     EventIntf::Events& events;
+    std::unique_ptr<DeviceFirmware> currentFirmware;
     sensors_map_t sensors;
 };
 
diff --git a/rtu/firmware/device_firmware.cpp b/rtu/firmware/device_firmware.cpp
new file mode 100644
index 0000000..7626076
--- /dev/null
+++ b/rtu/firmware/device_firmware.cpp
@@ -0,0 +1,119 @@
+#include "device_firmware.hpp"
+
+#include "device/base_device.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+namespace phosphor::modbus::rtu::device
+{
+
+PHOSPHOR_LOG2_USING;
+
+static auto getRandomId() -> long int
+{
+    struct timespec ts;
+    clock_gettime(CLOCK_REALTIME, &ts);
+    unsigned int seed = ts.tv_nsec ^ getpid();
+    srandom(seed);
+    return random() % 10000;
+}
+
+static auto getObjectPath(const config_intf::Config& config)
+    -> sdbusplus::message::object_path
+{
+    for (const auto& firmwareRegister : config.firmwareRegisters)
+    {
+        if (firmwareRegister.type == config_intf::FirmwareRegisterType::version)
+        {
+            if (firmwareRegister.name.empty())
+            {
+                return sdbusplus::message::object_path(
+                           FirmwareIntf::namespace_path) /
+                       std::format("{}_{}", config.name, getRandomId());
+            }
+            else
+            {
+                return sdbusplus::message::object_path(
+                           FirmwareIntf::namespace_path) /
+                       std::format("{}_{}_{}", config.name,
+                                   firmwareRegister.name, getRandomId());
+            }
+        }
+    }
+
+    throw std::runtime_error(
+        "No firmware version register found for " + config.name);
+}
+
+constexpr FirmwareIntf::Version::properties_t initVersion{
+    "Unknown", FirmwareIntf::VersionPurpose::Other};
+constexpr FirmwareIntf::Activation::properties_t initActivation{
+    FirmwareIntf::Activations::NotReady,
+    FirmwareIntf::RequestedActivations::None};
+constexpr FirmwareIntf::Definitions::properties_t initAssociations{};
+
+DeviceFirmware::DeviceFirmware(sdbusplus::async::context& ctx,
+                               const config_intf::Config& config,
+                               PortIntf& serialPort) :
+    objectPath(getObjectPath(config)),
+    currentFirmware(
+        std::make_unique<FirmwareIntf>(ctx, objectPath.str.c_str(), initVersion,
+                                       initActivation, initAssociations)),
+    config(config), serialPort(serialPort)
+{
+    currentFirmware->Version::emit_added();
+    currentFirmware->Activation::emit_added();
+    currentFirmware->Definitions::emit_added();
+
+    info("Device firmware {NAME} created successfully", "NAME", config.name);
+}
+
+auto DeviceFirmware::readVersionRegister() -> sdbusplus::async::task<void>
+{
+    const auto it = std::find_if(
+        config.firmwareRegisters.begin(), config.firmwareRegisters.end(),
+        [](const config_intf::FirmwareRegister& firmwareRegister) {
+            return firmwareRegister.type ==
+                   config_intf::FirmwareRegisterType::version;
+        });
+
+    if (it == config.firmwareRegisters.end())
+    {
+        error("No firmware version register found for {NAME}", "NAME",
+              config.name);
+        co_return;
+    }
+
+    const config_intf::FirmwareRegister& versionRegister = *it;
+
+    auto registers = std::vector<uint16_t>(versionRegister.size);
+    auto ret = co_await serialPort.readHoldingRegisters(
+        config.address, versionRegister.offset, config.baudRate, config.parity,
+        registers);
+    if (!ret)
+    {
+        error("Failed to read holding registers {NAME} for {DEVICE_ADDRESS}",
+              "NAME", versionRegister.name, "DEVICE_ADDRESS", config.address);
+        co_return;
+    }
+
+    std::string strValue = "";
+
+    for (const auto& value : registers)
+    {
+        strValue += static_cast<char>((value >> 8) & 0xFF);
+        strValue += static_cast<char>(value & 0xFF);
+    }
+
+    currentFirmware->version(strValue);
+    currentFirmware->activation(FirmwareIntf::Activation::Activations::Active);
+    auto associationList =
+        std::vector<std::tuple<std::string, std::string, std::string>>{
+            {"running", "ran_on", config.inventoryPath}};
+    currentFirmware->associations(associationList);
+
+    info("Firmware version {VERSION} for {NAME} at {DEVICE_ADDRESS}", "VERSION",
+         strValue, "NAME", config.name, "DEVICE_ADDRESS", config.address);
+}
+
+} // namespace phosphor::modbus::rtu::device
diff --git a/rtu/firmware/device_firmware.hpp b/rtu/firmware/device_firmware.hpp
new file mode 100644
index 0000000..c3839f4
--- /dev/null
+++ b/rtu/firmware/device_firmware.hpp
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "device/base_config.hpp"
+#include "port/base_port.hpp"
+
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/aserver.hpp>
+#include <xyz/openbmc_project/Software/Activation/aserver.hpp>
+#include <xyz/openbmc_project/Software/Version/aserver.hpp>
+
+namespace phosphor::modbus::rtu::device
+{
+
+namespace config
+{
+
+struct Config;
+
+} // namespace config
+
+namespace config_intf = phosphor::modbus::rtu::device::config;
+using PortIntf = phosphor::modbus::rtu::port::BasePort;
+
+class DeviceFirmware;
+
+using FirmwareIntf = sdbusplus::async::server_t<
+    DeviceFirmware, sdbusplus::aserver::xyz::openbmc_project::software::Version,
+    sdbusplus::aserver::xyz::openbmc_project::software::Activation,
+    sdbusplus::aserver::xyz::openbmc_project::association::Definitions>;
+
+class DeviceFirmware
+{
+  public:
+    DeviceFirmware() = delete;
+
+    explicit DeviceFirmware(sdbusplus::async::context& ctx,
+                            const config_intf::Config& config,
+                            PortIntf& serialPort);
+
+    auto readVersionRegister() -> sdbusplus::async::task<void>;
+
+  protected:
+    // Object path of current firmware object
+    // TODO: check if its possible to get rid off this via mocking since its
+    // only used in tests
+    const sdbusplus::message::object_path objectPath;
+
+  private:
+    std::unique_ptr<FirmwareIntf> currentFirmware;
+    const config_intf::Config config;
+    PortIntf& serialPort;
+};
+
+} // namespace phosphor::modbus::rtu::device
diff --git a/rtu/meson.build b/rtu/meson.build
index a8b9101..e95c7bc 100644
--- a/rtu/meson.build
+++ b/rtu/meson.build
@@ -32,9 +32,11 @@
     'device/reservoir_pump_unit.cpp',
 )
 
+firmware_src = files('firmware/device_firmware.cpp')
+
 executable(
     'phosphor-modbus-rtu',
-    ['device_manager.cpp', inventory_src, device_src],
+    ['device_manager.cpp', inventory_src, device_src, firmware_src],
     include_directories: ['.', common_include],
     dependencies: [default_deps],
     link_with: [modbus_common_lib, modbus_rtu_lib, modbus_rtu_port_lib],
diff --git a/tests/meson.build b/tests/meson.build
index c457029..373378c 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -70,6 +70,7 @@
         'test_sensors.cpp',
         'modbus_server_tester.cpp',
         device_src,
+        firmware_src,
         link_with: [modbus_common_lib],
         dependencies: [gtest_dep, gmock_dep, default_deps, modbus_rtu_dep],
         include_directories: ['.', common_include],
@@ -86,3 +87,16 @@
         include_directories: ['.', common_include],
     ),
 )
+
+test(
+    'test_firmware',
+    executable(
+        'test_firmware',
+        'test_firmware.cpp',
+        'modbus_server_tester.cpp',
+        firmware_src,
+        link_with: [modbus_common_lib],
+        dependencies: [gtest_dep, gmock_dep, default_deps, modbus_rtu_dep],
+        include_directories: ['.', common_include],
+    ),
+)
diff --git a/tests/modbus_server_tester.hpp b/tests/modbus_server_tester.hpp
index ccfa204..cd3f62b 100644
--- a/tests/modbus_server_tester.hpp
+++ b/tests/modbus_server_tester.hpp
@@ -39,6 +39,13 @@
 const std::vector<uint16_t> testReadHoldingRegisterTempSigned = {
     0xFFB0}; // -80.0
 
+// Device Firmware Testing Constants
+constexpr uint16_t testReadHoldingRegisterFirmwareVersionOffset = 0x0115;
+constexpr uint16_t testReadHoldingRegisterFirmwareVersionCount = 0x2;
+const std::vector<uint16_t> testReadHoldingRegisterFirmwareVersion = {
+    0x5244, 0x4630};
+constexpr std::string testReadHoldingRegisterFirmwareVersionStr = "RDF0";
+
 static const std::map<uint16_t, std::tuple<uint16_t, std::vector<uint16_t>>>
     testReadHoldingRegisterMap = {
         {testSuccessReadHoldingRegisterOffset,
@@ -54,6 +61,9 @@
           testReadHoldingRegisterTempUnsigned}},
         {testReadHoldingRegisterTempSignedOffset,
          {testReadHoldingRegisterTempCount, testReadHoldingRegisterTempSigned}},
+        {testReadHoldingRegisterFirmwareVersionOffset,
+         {testReadHoldingRegisterFirmwareVersionCount,
+          testReadHoldingRegisterFirmwareVersion}},
 };
 
 class ServerTester
diff --git a/tests/test_firmware.cpp b/tests/test_firmware.cpp
new file mode 100644
index 0000000..a0ce5ec
--- /dev/null
+++ b/tests/test_firmware.cpp
@@ -0,0 +1,192 @@
+#include "device/device_factory.hpp"
+#include "modbus_server_tester.hpp"
+#include "port/base_port.hpp"
+
+#include <fcntl.h>
+
+#include <xyz/openbmc_project/Software/Version/client.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace std::literals;
+using namespace testing;
+using SoftwareIntf =
+    sdbusplus::client::xyz::openbmc_project::software::Version<>;
+
+namespace TestIntf = phosphor::modbus::test;
+namespace ModbusIntf = phosphor::modbus::rtu;
+namespace PortIntf = phosphor::modbus::rtu::port;
+namespace PortConfigIntf = PortIntf::config;
+namespace DeviceIntf = phosphor::modbus::rtu::device;
+namespace DeviceConfigIntf = DeviceIntf::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 TestFirmware : public DeviceIntf::DeviceFirmware
+{
+  public:
+    TestFirmware(sdbusplus::async::context& ctx,
+                 const DeviceConfigIntf::Config& config,
+                 PortIntf::BasePort& serialPort) :
+        DeviceIntf::DeviceFirmware(ctx, config, serialPort)
+    {}
+
+    auto getObjectPath() -> sdbusplus::message::object_path
+    {
+        return objectPath;
+    }
+};
+
+class FirmwareTest : public ::testing::Test
+{
+  public:
+    PortConfigIntf::Config portConfig;
+    static constexpr const char* clientDevicePath = "/tmp/ttyFirmwareTestPort0";
+    static constexpr const char* serverDevicePath = "/tmp/ttyFirmwareTestPort1";
+    static constexpr auto portName = "TestPort0";
+    static constexpr auto baudRate = 115200;
+    static constexpr const auto strBaudeRate = "b115200";
+    std::string deviceName;
+    std::string objectPath;
+    static constexpr auto serviceName =
+        "xyz.openbmc_project.TestModbusRTUFirmware";
+    static constexpr auto firmwareName = "TestVersion";
+    int socat_pid = -1;
+    sdbusplus::async::context ctx;
+    int fdClient = -1;
+    std::unique_ptr<TestIntf::ServerTester> serverTester;
+    int fdServer = -1;
+    std::unique_ptr<MockPort> mockPort;
+
+    FirmwareTest()
+    {
+        portConfig.name = portName;
+        portConfig.portMode = PortConfigIntf::PortMode::rs485;
+        portConfig.baudRate = baudRate;
+        portConfig.rtsDelay = 1;
+
+        deviceName = std::format("ResorviorPumpUnit_{}_{}",
+                                 TestIntf::testDeviceAddress, portName);
+        objectPath =
+            std::format("{}/{}", SoftwareIntf::namespace_path, deviceName);
+
+        std::string socatCmd = std::format(
+            "socat -x -v -d -d pty,link={},rawer,echo=0,parenb,{} pty,link={},rawer,echo=0,parenb,{} & echo $!",
+            serverDevicePath, strBaudeRate, clientDevicePath, strBaudeRate);
+
+        // 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);
+
+        mockPort =
+            std::make_unique<MockPort>(ctx, portConfig, clientDevicePath);
+
+        serverTester = std::make_unique<TestIntf::ServerTester>(ctx, fdServer);
+    }
+
+    ~FirmwareTest() noexcept override
+    {
+        if (fdClient != -1)
+        {
+            close(fdClient);
+            fdClient = -1;
+        }
+        if (fdServer != -1)
+        {
+            close(fdServer);
+            fdServer = -1;
+        }
+        kill(socat_pid, SIGTERM);
+    }
+
+    auto testFirmwareVersion(
+        std::string objectPath,
+        DeviceConfigIntf::FirmwareRegister firmwareRegister,
+        std::string expectedVersion) -> sdbusplus::async::task<void>
+    {
+        DeviceConfigIntf::DeviceFactoryConfig deviceFactoryConfig = {
+            {
+                .address = TestIntf::testDeviceAddress,
+                .parity = ModbusIntf::Parity::none,
+                .baudRate = baudRate,
+                .name = deviceName,
+                .portName = portConfig.name,
+                .inventoryPath = sdbusplus::message::object_path(
+                    "xyz/openbmc_project/Inventory/ResorviorPumpUnit"),
+                .sensorRegisters = {},
+                .statusRegisters = {},
+                .firmwareRegisters = {firmwareRegister},
+            },
+            DeviceConfigIntf::DeviceType::reservoirPumpUnit,
+            DeviceConfigIntf::DeviceModel::RDF040DSS5193E0,
+        };
+
+        auto deviceFirmware =
+            std::make_unique<TestFirmware>(ctx, deviceFactoryConfig, *mockPort);
+
+        co_await deviceFirmware->readVersionRegister();
+
+        EXPECT_TRUE(deviceFirmware->getObjectPath().str.starts_with(objectPath))
+            << "Invalid ObjectPath";
+
+        auto softwarePath = deviceFirmware->getObjectPath().str;
+
+        auto properties = co_await SoftwareIntf(ctx)
+                              .service(serviceName)
+                              .path(softwarePath)
+                              .properties();
+
+        EXPECT_EQ(properties.version, expectedVersion)
+            << "Firmware version mismatch";
+
+        co_return;
+    }
+
+    void SetUp() override
+    {
+        // Process request to read firmware version
+        ctx.spawn(serverTester->processRequests());
+    }
+};
+
+TEST_F(FirmwareTest, TestFirmwareVersion)
+{
+    const DeviceConfigIntf::FirmwareRegister firmwareRegister = {
+        .name = "",
+        .type = DeviceConfigIntf::FirmwareRegisterType::version,
+        .offset = TestIntf::testReadHoldingRegisterFirmwareVersionOffset,
+        .size = TestIntf::testReadHoldingRegisterFirmwareVersionCount};
+
+    ctx.spawn(testFirmwareVersion(
+        objectPath, firmwareRegister,
+        TestIntf::testReadHoldingRegisterFirmwareVersionStr));
+
+    ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+              sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+    ctx.run();
+}