rtu: implement modbus sensor read

Read the modbus device config from the Entity Manager configuration and
create the sensor interfaces for related sensor register config.

Tested:
Added new Unit test named test_sensors -
```
> meson test -t 10 -C builddir/ --print-errorlogs --wrapper="valgrind --error-exitcode=1" test_sensors
ninja: Entering directory `/host/repos/Modbus/phosphor-modbus/builddir'
[2/2] Linking target tests/test_sensors
1/1 test_sensors        OK              13.98s

Ok:                1
Fail:              0
```

Tested on Qemu using Mock Modbus Device -
```
root@ventura:~# 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

busctl introspect xyz.openbmc_project.ModbusRTU /xyz/openbmc_project/sensors/temperature/Reservoir_Pumping_Unit_12_DevTTYUSB1_RPU_Coolant_Outlet_Temp_C
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.Sensor.Value    interface -         -                                        -
.MaxValue                           property  d         nan                                      emits-change writable
.MinValue                           property  d         nan                                      emits-change writable
.Unit                               property  s         "xyz.openbmc_project.Sensor.Value.Unit.… emits-change writable
.Value                              property  d         1670.6                                   emits-change writable
```

Change-Id: I1368e8df5999b5cee9ac19d185ee110a9ecc3021
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/tests/meson.build b/tests/meson.build
index bcc4fad..8b1df07 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -62,3 +62,15 @@
         include_directories: ['.', common_include],
     ),
 )
+
+test(
+    'test_sensors',
+    executable(
+        'test_sensors',
+        'test_sensors.cpp',
+        'modbus_server_tester.cpp',
+        device_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 fe1b825..1b09dc2 100644
--- a/tests/modbus_server_tester.cpp
+++ b/tests/modbus_server_tester.cpp
@@ -122,35 +122,8 @@
     uint16_t registerOffset = request.raw[2] << 8 | request.raw[3];
     uint16_t registerCount = request.raw[4] << 8 | request.raw[5];
 
-    if (registerOffset == testSuccessReadHoldingRegisterOffset ||
-        registerOffset == testSuccessReadHoldingRegisterSegmentedOffset)
+    if (registerOffset == testFailureReadHoldingRegister)
     {
-        checkRequestSize(registerCount, testSuccessReadHoldingRegisterCount);
-
-        response << request.raw[0] << request.raw[1]
-                 << uint8_t(2 * registerCount)
-                 << uint16_t(testSuccessReadHoldingRegisterResponse[0])
-                 << uint16_t(testSuccessReadHoldingRegisterResponse[1]);
-        response.appendCRC();
-        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);
@@ -158,7 +131,29 @@
     }
     else
     {
-        FAIL() << "Invalid register offset:" << registerOffset;
+        auto expectedResponseIter =
+            testReadHoldingRegisterMap.find(registerOffset);
+        if (expectedResponseIter == testReadHoldingRegisterMap.end())
+        {
+            FAIL() << "Invalid register offset:" << registerOffset;
+            return;
+        }
+
+        checkRequestSize(registerCount,
+                         std::get<0>(expectedResponseIter->second));
+
+        auto& expectedResponse = std::get<1>(expectedResponseIter->second);
+
+        response << request.raw[0] << request.raw[1]
+                 << uint8_t(2 * registerCount);
+        for (size_t i = 0; i < registerCount; i++)
+        {
+            response << uint16_t(expectedResponse[i]);
+        }
+        response.appendCRC();
+
+        segmentedResponse =
+            (registerOffset == testSuccessReadHoldingRegisterSegmentedOffset);
     }
 }
 
diff --git a/tests/modbus_server_tester.hpp b/tests/modbus_server_tester.hpp
index c9b4416..ccfa204 100644
--- a/tests/modbus_server_tester.hpp
+++ b/tests/modbus_server_tester.hpp
@@ -19,18 +19,43 @@
 constexpr uint16_t testSuccessReadHoldingRegisterOffset = 0x0102;
 constexpr uint16_t testSuccessReadHoldingRegisterCount = 0x2;
 constexpr uint16_t testSuccessReadHoldingRegisterSegmentedOffset = 0x0103;
-constexpr std::array<uint16_t, testSuccessReadHoldingRegisterCount>
-    testSuccessReadHoldingRegisterResponse = {0x1234, 0x5678};
+const std::vector<uint16_t> 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};
+const std::vector<uint16_t> testReadHoldingRegisterModel = {
+    0x5244, 0x4630, 0x3430, 0x4453, 0x5335, 0x3139, 0x0000, 0x3000};
 constexpr std::string testReadHoldingRegisterModelStr = "RDF040DSS519";
 
+// Device Sensors Testing Constants
+constexpr uint16_t testReadHoldingRegisterTempCount = 0x1;
+constexpr uint16_t testReadHoldingRegisterTempUnsignedOffset = 0x0113;
+const std::vector<uint16_t> testReadHoldingRegisterTempUnsigned = {
+    0x0050}; // 80.0
+constexpr uint16_t testReadHoldingRegisterTempSignedOffset = 0x0114;
+const std::vector<uint16_t> testReadHoldingRegisterTempSigned = {
+    0xFFB0}; // -80.0
+
+static const std::map<uint16_t, std::tuple<uint16_t, std::vector<uint16_t>>>
+    testReadHoldingRegisterMap = {
+        {testSuccessReadHoldingRegisterOffset,
+         {testSuccessReadHoldingRegisterCount,
+          testSuccessReadHoldingRegisterResponse}},
+        {testSuccessReadHoldingRegisterSegmentedOffset,
+         {testSuccessReadHoldingRegisterCount,
+          testSuccessReadHoldingRegisterResponse}},
+        {testReadHoldingRegisterModelOffset,
+         {testReadHoldingRegisterModelCount, testReadHoldingRegisterModel}},
+        {testReadHoldingRegisterTempUnsignedOffset,
+         {testReadHoldingRegisterTempCount,
+          testReadHoldingRegisterTempUnsigned}},
+        {testReadHoldingRegisterTempSignedOffset,
+         {testReadHoldingRegisterTempCount, testReadHoldingRegisterTempSigned}},
+};
+
 class ServerTester
 {
   public:
diff --git a/tests/test_sensors.cpp b/tests/test_sensors.cpp
new file mode 100644
index 0000000..a5046b0
--- /dev/null
+++ b/tests/test_sensors.cpp
@@ -0,0 +1,241 @@
+#include "device/device_factory.hpp"
+#include "modbus_server_tester.hpp"
+#include "port/base_port.hpp"
+
+#include <fcntl.h>
+
+#include <xyz/openbmc_project/Sensor/Value/client.hpp>
+
+#include <cmath>
+#include <string>
+
+#include <gtest/gtest.h>
+
+using namespace std::literals;
+using namespace testing;
+using SensorValueIntf =
+    sdbusplus::client::xyz::openbmc_project::sensor::Value<>;
+
+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 SensorsTest : public ::testing::Test
+{
+  public:
+    PortConfigIntf::Config portConfig;
+    static constexpr const char* clientDevicePath = "/tmp/ttySensorsTestPort0";
+    static constexpr const char* serverDevicePath = "/tmp/ttySensorsTestPort1";
+    static constexpr auto portName = "TestPort0";
+    static constexpr auto baudRate = 115200;
+    static constexpr const auto strBaudeRate = "b115200";
+    std::string deviceName;
+    std::string fullSensorName;
+    std::string objectPath;
+    static constexpr auto serviceName =
+        "xyz.openbmc_project.TestModbusRTUSensors";
+    static constexpr auto sensorName = "OutletTemperature";
+    int socat_pid = -1;
+    sdbusplus::async::context ctx;
+    int fdClient = -1;
+    std::unique_ptr<TestIntf::ServerTester> serverTester;
+    int fdServer = -1;
+
+    SensorsTest()
+    {
+        portConfig.name = portName;
+        portConfig.portMode = PortConfigIntf::PortMode::rs485;
+        portConfig.baudRate = baudRate;
+        portConfig.rtsDelay = 1;
+
+        deviceName = std::format("ResorviorPumpUnit_{}_{}",
+                                 TestIntf::testDeviceAddress, portName);
+
+        fullSensorName = std::format("{}_{}", deviceName, sensorName);
+
+        objectPath = std::format(
+            "{}/{}/{}", SensorValueIntf::namespace_path::value,
+            SensorValueIntf::namespace_path::temperature, fullSensorName);
+
+        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);
+
+        serverTester = std::make_unique<TestIntf::ServerTester>(ctx, fdServer);
+    }
+
+    ~SensorsTest() noexcept override
+    {
+        if (fdClient != -1)
+        {
+            close(fdClient);
+            fdClient = -1;
+        }
+        if (fdServer != -1)
+        {
+            close(fdServer);
+            fdServer = -1;
+        }
+        kill(socat_pid, SIGTERM);
+    }
+
+    auto testSensorCreation(std::string objectPath,
+                            DeviceConfigIntf::SensorRegister sensorRegister,
+                            double expectedValue)
+        -> 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 = {sensorRegister},
+                .statusRegisters = {},
+                .firmwareRegisters = {},
+            },
+            DeviceConfigIntf::DeviceType::reservoirPumpUnit,
+            DeviceConfigIntf::DeviceModel::RDF040DSS5193E0,
+        };
+
+        auto mockPort =
+            std::make_unique<MockPort>(ctx, portConfig, clientDevicePath);
+
+        auto device = DeviceIntf::DeviceFactory::create(
+            ctx, deviceFactoryConfig, *mockPort);
+
+        co_await device->readSensorRegisters();
+
+        auto properties = co_await SensorValueIntf(ctx)
+                              .service(serviceName)
+                              .path(objectPath)
+                              .properties();
+
+        EXPECT_EQ(properties.value, expectedValue) << "Sensor value mismatch";
+        EXPECT_EQ(properties.unit, sensorRegister.unit)
+            << "Sensor unit mismatch";
+        EXPECT_TRUE(std::isnan(properties.min_value)) << "Min value mismatch";
+        EXPECT_TRUE(std::isnan(properties.max_value)) << "Max value mismatch";
+
+        co_return;
+    }
+
+    void SetUp() override
+    {
+        // Process request for sensor poll
+        ctx.spawn(serverTester->processRequests());
+    }
+};
+
+TEST_F(SensorsTest, TestSensorValueUnsigned)
+{
+    const DeviceConfigIntf::SensorRegister sensorRegister = {
+        .name = sensorName,
+        .pathSuffix = SensorValueIntf::namespace_path::temperature,
+        .unit = SensorValueIntf::Unit::DegreesC,
+        .offset = TestIntf::testReadHoldingRegisterTempUnsignedOffset,
+        .size = TestIntf::testReadHoldingRegisterTempCount,
+        .format = DeviceConfigIntf::SensorFormat::floatingPoint,
+    };
+
+    ctx.spawn(
+        testSensorCreation(objectPath, sensorRegister,
+                           TestIntf::testReadHoldingRegisterTempUnsigned[0]));
+
+    ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+              sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+    ctx.run();
+}
+
+TEST_F(SensorsTest, TestSensorValueSigned)
+{
+    const DeviceConfigIntf::SensorRegister sensorRegister = {
+        .name = sensorName,
+        .pathSuffix = SensorValueIntf::namespace_path::temperature,
+        .unit = SensorValueIntf::Unit::DegreesC,
+        .offset = TestIntf::testReadHoldingRegisterTempSignedOffset,
+        .size = TestIntf::testReadHoldingRegisterTempCount,
+        .isSigned = true,
+        .format = DeviceConfigIntf::SensorFormat::floatingPoint,
+    };
+
+    // Convert expected hex value to a signed 16-bit integer for comparison
+    const int16_t expectedSigned =
+        static_cast<int16_t>(TestIntf::testReadHoldingRegisterTempSigned[0]);
+
+    ctx.spawn(testSensorCreation(objectPath, sensorRegister, expectedSigned));
+
+    ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+              sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+    ctx.run();
+}
+
+static auto applyValueSettings(double value, double shift, double scale,
+                               uint8_t precision)
+{
+    return (shift + (scale * (value / (1ULL << precision))));
+}
+
+TEST_F(SensorsTest, TestSensorValueWithSettings)
+{
+    const DeviceConfigIntf::SensorRegister sensorRegister = {
+        .name = sensorName,
+        .pathSuffix = SensorValueIntf::namespace_path::temperature,
+        .unit = SensorValueIntf::Unit::DegreesC,
+        .offset = TestIntf::testReadHoldingRegisterTempUnsignedOffset,
+        .size = TestIntf::testReadHoldingRegisterTempCount,
+        .precision = 2,
+        .scale = 0.1,
+        .shift = 50,
+        .format = DeviceConfigIntf::SensorFormat::floatingPoint,
+    };
+
+    ctx.spawn(testSensorCreation(
+        objectPath, sensorRegister,
+        applyValueSettings(TestIntf::testReadHoldingRegisterTempUnsigned[0],
+                           sensorRegister.shift, sensorRegister.scale,
+                           sensorRegister.precision)));
+
+    ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+              sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+    ctx.run();
+}