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