1 #include "common/events.hpp" 2 #include "device/device_factory.hpp" 3 #include "modbus_server_tester.hpp" 4 #include "port/base_port.hpp" 5 6 #include <fcntl.h> 7 8 #include <xyz/openbmc_project/Sensor/Value/client.hpp> 9 10 #include <cmath> 11 #include <string> 12 13 #include <gtest/gtest.h> 14 15 using namespace std::literals; 16 using namespace testing; 17 using SensorValueIntf = 18 sdbusplus::client::xyz::openbmc_project::sensor::Value<>; 19 20 namespace TestIntf = phosphor::modbus::test; 21 namespace ModbusIntf = phosphor::modbus::rtu; 22 namespace PortIntf = phosphor::modbus::rtu::port; 23 namespace PortConfigIntf = PortIntf::config; 24 namespace DeviceIntf = phosphor::modbus::rtu::device; 25 namespace DeviceConfigIntf = DeviceIntf::config; 26 namespace EventIntf = phosphor::modbus::events; 27 28 class MockPort : public PortIntf::BasePort 29 { 30 public: 31 MockPort(sdbusplus::async::context& ctx, 32 const PortConfigIntf::Config& config, 33 const std::string& devicePath) : BasePort(ctx, config, devicePath) 34 {} 35 }; 36 37 class SensorsTest : public ::testing::Test 38 { 39 public: 40 PortConfigIntf::Config portConfig; 41 static constexpr const char* clientDevicePath = "/tmp/ttySensorsTestPort0"; 42 static constexpr const char* serverDevicePath = "/tmp/ttySensorsTestPort1"; 43 static constexpr auto portName = "TestPort0"; 44 static constexpr auto baudRate = 115200; 45 static constexpr const auto strBaudeRate = "b115200"; 46 std::string deviceName; 47 std::string fullSensorName; 48 std::string objectPath; 49 static constexpr auto serviceName = 50 "xyz.openbmc_project.TestModbusRTUSensors"; 51 static constexpr auto sensorName = "OutletTemperature"; 52 int socat_pid = -1; 53 sdbusplus::async::context ctx; 54 int fdClient = -1; 55 std::unique_ptr<TestIntf::ServerTester> serverTester; 56 int fdServer = -1; 57 58 SensorsTest() 59 { 60 portConfig.name = portName; 61 portConfig.portMode = PortConfigIntf::PortMode::rs485; 62 portConfig.baudRate = baudRate; 63 portConfig.rtsDelay = 1; 64 65 deviceName = std::format("ResorviorPumpUnit_{}_{}", 66 TestIntf::testDeviceAddress, portName); 67 68 fullSensorName = std::format("{}_{}", deviceName, sensorName); 69 70 objectPath = std::format( 71 "{}/{}/{}", SensorValueIntf::namespace_path::value, 72 SensorValueIntf::namespace_path::temperature, fullSensorName); 73 74 std::string socatCmd = std::format( 75 "socat -x -v -d -d pty,link={},rawer,echo=0,parenb,{} pty,link={},rawer,echo=0,parenb,{} & echo $!", 76 serverDevicePath, strBaudeRate, clientDevicePath, strBaudeRate); 77 78 // Start socat in the background and capture its PID 79 FILE* fp = popen(socatCmd.c_str(), "r"); 80 EXPECT_NE(fp, nullptr) << "Failed to start socat: " << strerror(errno); 81 EXPECT_GT(fscanf(fp, "%d", &socat_pid), 0); 82 pclose(fp); 83 84 // Wait for socat to start up 85 sleep(1); 86 87 fdClient = open(clientDevicePath, O_RDWR | O_NOCTTY | O_NONBLOCK); 88 EXPECT_NE(fdClient, -1) 89 << "Failed to open serial port " << clientDevicePath 90 << " with error: " << strerror(errno); 91 92 fdServer = open(serverDevicePath, O_RDWR | O_NOCTTY | O_NONBLOCK); 93 EXPECT_NE(fdServer, -1) 94 << "Failed to open serial port " << serverDevicePath 95 << " with error: " << strerror(errno); 96 97 ctx.request_name(serviceName); 98 99 serverTester = std::make_unique<TestIntf::ServerTester>(ctx, fdServer); 100 } 101 102 ~SensorsTest() noexcept override 103 { 104 if (fdClient != -1) 105 { 106 close(fdClient); 107 fdClient = -1; 108 } 109 if (fdServer != -1) 110 { 111 close(fdServer); 112 fdServer = -1; 113 } 114 kill(socat_pid, SIGTERM); 115 } 116 117 auto testSensorCreation(std::string objectPath, 118 DeviceConfigIntf::SensorRegister sensorRegister, 119 double expectedValue) 120 -> sdbusplus::async::task<void> 121 { 122 DeviceConfigIntf::DeviceFactoryConfig deviceFactoryConfig = { 123 { 124 .address = TestIntf::testDeviceAddress, 125 .parity = ModbusIntf::Parity::none, 126 .baudRate = baudRate, 127 .name = deviceName, 128 .portName = portConfig.name, 129 .inventoryPath = sdbusplus::message::object_path( 130 "xyz/openbmc_project/Inventory/ResorviorPumpUnit"), 131 .sensorRegisters = {sensorRegister}, 132 .statusRegisters = {}, 133 .firmwareRegisters = {}, 134 }, 135 DeviceConfigIntf::DeviceType::reservoirPumpUnit, 136 DeviceConfigIntf::DeviceModel::RDF040DSS5193E0, 137 }; 138 139 EventIntf::Events events{ctx}; 140 141 MockPort mockPort(ctx, portConfig, clientDevicePath); 142 143 auto device = DeviceIntf::DeviceFactory::create( 144 ctx, deviceFactoryConfig, mockPort, events); 145 146 co_await device->readSensorRegisters(); 147 148 auto properties = co_await SensorValueIntf(ctx) 149 .service(serviceName) 150 .path(objectPath) 151 .properties(); 152 153 EXPECT_EQ(properties.value, expectedValue) << "Sensor value mismatch"; 154 EXPECT_EQ(properties.unit, sensorRegister.unit) 155 << "Sensor unit mismatch"; 156 EXPECT_TRUE(std::isnan(properties.min_value)) << "Min value mismatch"; 157 EXPECT_TRUE(std::isnan(properties.max_value)) << "Max value mismatch"; 158 159 co_return; 160 } 161 162 void SetUp() override 163 { 164 // Process request for sensor poll 165 ctx.spawn(serverTester->processRequests()); 166 } 167 }; 168 169 TEST_F(SensorsTest, TestSensorValueUnsigned) 170 { 171 const DeviceConfigIntf::SensorRegister sensorRegister = { 172 .name = sensorName, 173 .pathSuffix = SensorValueIntf::namespace_path::temperature, 174 .unit = SensorValueIntf::Unit::DegreesC, 175 .offset = TestIntf::testReadHoldingRegisterTempUnsignedOffset, 176 .size = TestIntf::testReadHoldingRegisterTempCount, 177 .format = DeviceConfigIntf::SensorFormat::floatingPoint, 178 }; 179 180 ctx.spawn( 181 testSensorCreation(objectPath, sensorRegister, 182 TestIntf::testReadHoldingRegisterTempUnsigned[0])); 183 184 ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) | 185 sdbusplus::async::execution::then([&]() { ctx.request_stop(); })); 186 187 ctx.run(); 188 } 189 190 TEST_F(SensorsTest, TestSensorValueSigned) 191 { 192 const DeviceConfigIntf::SensorRegister sensorRegister = { 193 .name = sensorName, 194 .pathSuffix = SensorValueIntf::namespace_path::temperature, 195 .unit = SensorValueIntf::Unit::DegreesC, 196 .offset = TestIntf::testReadHoldingRegisterTempSignedOffset, 197 .size = TestIntf::testReadHoldingRegisterTempCount, 198 .isSigned = true, 199 .format = DeviceConfigIntf::SensorFormat::floatingPoint, 200 }; 201 202 // Convert expected hex value to a signed 16-bit integer for comparison 203 const int16_t expectedSigned = 204 static_cast<int16_t>(TestIntf::testReadHoldingRegisterTempSigned[0]); 205 206 ctx.spawn(testSensorCreation(objectPath, sensorRegister, expectedSigned)); 207 208 ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) | 209 sdbusplus::async::execution::then([&]() { ctx.request_stop(); })); 210 211 ctx.run(); 212 } 213 214 static auto applyValueSettings(double value, double shift, double scale, 215 uint8_t precision) 216 { 217 return (shift + (scale * (value / (1ULL << precision)))); 218 } 219 220 TEST_F(SensorsTest, TestSensorValueWithSettings) 221 { 222 const DeviceConfigIntf::SensorRegister sensorRegister = { 223 .name = sensorName, 224 .pathSuffix = SensorValueIntf::namespace_path::temperature, 225 .unit = SensorValueIntf::Unit::DegreesC, 226 .offset = TestIntf::testReadHoldingRegisterTempUnsignedOffset, 227 .size = TestIntf::testReadHoldingRegisterTempCount, 228 .precision = 2, 229 .scale = 0.1, 230 .shift = 50, 231 .format = DeviceConfigIntf::SensorFormat::floatingPoint, 232 }; 233 234 ctx.spawn(testSensorCreation( 235 objectPath, sensorRegister, 236 applyValueSettings(TestIntf::testReadHoldingRegisterTempUnsigned[0], 237 sensorRegister.shift, sensorRegister.scale, 238 sensorRegister.precision))); 239 240 ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) | 241 sdbusplus::async::execution::then([&]() { ctx.request_stop(); })); 242 243 ctx.run(); 244 } 245