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/rtu/device/base_config.cpp b/rtu/device/base_config.cpp
new file mode 100644
index 0000000..d9e7e4f
--- /dev/null
+++ b/rtu/device/base_config.cpp
@@ -0,0 +1,362 @@
+#include "base_config.hpp"
+
+#include "common/entity_manager_interface.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/Inventory/Item/client.hpp>
+#include <xyz/openbmc_project/State/Leak/Detector/aserver.hpp>
+
+#include <flat_map>
+
+namespace phosphor::modbus::rtu::device::config
+{
+
+PHOSPHOR_LOG2_USING;
+
+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}}};
+
+template <typename T>
+auto getValue(const InventoryBaseConfigMap& configMap, const std::string& key,
+ const std::string& contextName) -> T
+{
+ auto iter = configMap.find(key);
+ if (iter == configMap.end())
+ {
+ throw std::runtime_error(
+ "Missing property " + key + " for " + contextName);
+ }
+
+ try
+ {
+ return std::get<T>(iter->second);
+ }
+ catch (const std::bad_variant_access& ex)
+ {
+ throw std::runtime_error(
+ "Incorrect type for property " + key + " in " + contextName);
+ }
+}
+
+static inline auto getDataParity(Config& config,
+ const InventoryBaseConfigMap& configMap)
+ -> void
+{
+ auto receivedParity =
+ getValue<std::string>(configMap, "DataParity", config.name);
+
+ for (const auto& [parityStr, parity] : validParities)
+ {
+ if (parityStr == receivedParity)
+ {
+ config.parity = parity;
+ break;
+ }
+ }
+
+ if (config.parity == Parity::unknown)
+ {
+ throw std::runtime_error(
+ "Invalid parity " + receivedParity + " for " + config.name);
+ }
+}
+
+static auto processDeviceInterface(Config& config,
+ const InventoryBaseConfigMap& configMap)
+ -> void
+{
+ debug("Processing device config");
+
+ config.name = getValue<std::string>(configMap, "Name", config.name);
+
+ std::replace(config.name.begin(), config.name.end(), ' ', '_');
+
+ config.address = getValue<uint64_t>(configMap, "Address", config.name);
+
+ getDataParity(config, configMap);
+
+ config.baudRate = getValue<uint64_t>(configMap, "BaudRate", config.name);
+
+ config.portName =
+ getValue<std::string>(configMap, "SerialPort", config.name);
+
+ getValue<std::string>(configMap, "Type", config.name);
+}
+
+static const auto sensorTypes = std::unordered_map<
+ std::string_view, std::pair<std::string_view, SensorValueIntf::Unit>>{
+ {"FanTach",
+ {SensorValueIntf::namespace_path::fan_tach, SensorValueIntf::Unit::RPMS}},
+ {"LiquidFlow",
+ {SensorValueIntf::namespace_path::liquidflow, SensorValueIntf::Unit::LPM}},
+ {"Power",
+ {SensorValueIntf::namespace_path::power, SensorValueIntf::Unit::Watts}},
+ {"Pressure",
+ {SensorValueIntf::namespace_path::pressure,
+ SensorValueIntf::Unit::Pascals}},
+ {"Temperature",
+ {SensorValueIntf::namespace_path::temperature,
+ SensorValueIntf::Unit::DegreesC}},
+};
+
+static const auto formatTypes =
+ std::unordered_map<std::string_view, SensorFormat>{
+ {"Integer", SensorFormat::integer},
+ {"Float", SensorFormat::floatingPoint}};
+
+static auto processRegisterType(SensorRegister& sensorRegister,
+ const InventoryBaseConfigMap& configMap) -> void
+{
+ auto registerType =
+ getValue<std::string>(configMap, "RegisterType", sensorRegister.name);
+
+ auto type = sensorTypes.find(registerType);
+ if (type == sensorTypes.end())
+ {
+ throw std::runtime_error("Invalid RegisterType " + registerType +
+ " for " + sensorRegister.name);
+ }
+ sensorRegister.pathSuffix = type->second.first;
+ sensorRegister.unit = type->second.second;
+}
+
+static auto processRegisterFormat(SensorRegister& sensorRegister,
+ const InventoryBaseConfigMap& configMap)
+ -> void
+{
+ auto format =
+ getValue<std::string>(configMap, "Format", sensorRegister.name);
+
+ auto formatIter = formatTypes.find(format);
+ if (formatIter == formatTypes.end())
+ {
+ throw std::runtime_error(
+ "Invalid Format " + format + " for " + sensorRegister.name);
+ }
+ sensorRegister.format = formatIter->second;
+}
+
+static auto processSensorRegistersInterface(
+ Config& config, const InventoryBaseConfigMap& configMap) -> void
+{
+ SensorRegister sensorRegister = {};
+
+ sensorRegister.name = getValue<std::string>(configMap, "Name", config.name);
+
+ processRegisterType(sensorRegister, configMap);
+
+ sensorRegister.offset =
+ getValue<uint64_t>(configMap, "Address", config.name);
+
+ sensorRegister.size = getValue<uint64_t>(configMap, "Size", config.name);
+
+ sensorRegister.precision =
+ getValue<uint64_t>(configMap, "Precision", config.name);
+
+ sensorRegister.shift = getValue<double>(configMap, "Shift", config.name);
+
+ sensorRegister.scale = getValue<double>(configMap, "Scale", config.name);
+
+ sensorRegister.isSigned = getValue<bool>(configMap, "Signed", config.name);
+
+ processRegisterFormat(sensorRegister, configMap);
+
+ config.sensorRegisters.emplace_back(sensorRegister);
+}
+
+static const auto statusBitTypes =
+ std::unordered_map<std::string_view, StatusType>{
+ {"ControllerFailure", StatusType::controllerFailure},
+ {"FanFailure", StatusType::fanFailure},
+ {"FilterFailure", StatusType::filterFailure},
+ {"PowerFault", StatusType::powerFault},
+ {"PumpFailure", StatusType::pumpFailure},
+ {"LeakDetectedCritical", StatusType::leakDetectedCritical},
+ {"LeakDetectedWarning", StatusType::leakDetectedWarning},
+ {"SensorFailure", StatusType::sensorFailure},
+ {"SensorReadingCritical", StatusType::sensorReadingCritical},
+ {"SensorReadingWarning", StatusType::sensorReadingWarning}};
+
+static auto processStatusBitsInterface(Config& config,
+ const InventoryBaseConfigMap& configMap)
+ -> void
+{
+ debug("Processing StatusBits for {NAME}", "NAME", config.name);
+
+ StatusBit statusBit = {};
+
+ statusBit.name = getValue<std::string>(configMap, "Name", config.name);
+
+ auto type = getValue<std::string>(configMap, "StatusType", config.name);
+ auto typeIter = statusBitTypes.find(type);
+ if (typeIter == statusBitTypes.end())
+ {
+ throw std::runtime_error(
+ "Invalid StatusType " + type + " for " + statusBit.name);
+ }
+ statusBit.type = typeIter->second;
+
+ statusBit.bitPosition =
+ getValue<uint64_t>(configMap, "BitPosition", config.name);
+
+ statusBit.value = getValue<bool>(configMap, "Value", config.name);
+
+ auto address = getValue<uint64_t>(configMap, "Address", config.name);
+
+ config.statusRegisters[address].emplace_back(statusBit);
+}
+
+static const auto firmwareRegisterTypes =
+ std::unordered_map<std::string_view, FirmwareRegisterType>{
+ {"Version", FirmwareRegisterType::version},
+ {"Update", FirmwareRegisterType::update}};
+
+static auto processFirmwareRegistersInterface(
+ Config& config, const InventoryBaseConfigMap& configMap) -> void
+{
+ debug("Processing FirmwareRegisters for {NAME}", "NAME", config.name);
+
+ FirmwareRegister firmwareRegister = {};
+
+ firmwareRegister.name =
+ getValue<std::string>(configMap, "Name", config.name);
+
+ firmwareRegister.offset =
+ getValue<uint64_t>(configMap, "Address", firmwareRegister.name);
+
+ firmwareRegister.size =
+ getValue<uint64_t>(configMap, "Size", firmwareRegister.name);
+
+ auto registerType =
+ getValue<std::string>(configMap, "RegisterType", firmwareRegister.name);
+ auto registerTypeIter = firmwareRegisterTypes.find(registerType);
+ if (registerTypeIter == firmwareRegisterTypes.end())
+ {
+ throw std::runtime_error("Invalid RegisterType " + registerType +
+ " for " + firmwareRegister.name);
+ }
+ firmwareRegister.type = registerTypeIter->second;
+
+ config.firmwareRegisters.emplace_back(firmwareRegister);
+}
+
+static auto printConfig(const Config& config) -> void
+{
+ info("Device Config for {NAME}: {ADDRESS} {PORT} {INV_PATH}", "NAME",
+ config.name, "ADDRESS", config.address, "PORT", config.portName,
+ "INV_PATH", config.inventoryPath);
+
+ for (const auto& sensorRegister : config.sensorRegisters)
+ {
+ info(
+ "Sensor Register {NAME} {ADDRESS} {SIZE} {PRECISION} {SCALE} {SIGNED} {FORMAT} {UNIT} {PATH_SUFFIX}",
+ "NAME", sensorRegister.name, "ADDRESS", sensorRegister.offset,
+ "SIZE", sensorRegister.size, "PRECISION", sensorRegister.precision,
+ "SCALE", sensorRegister.scale, "SIGNED", sensorRegister.isSigned,
+ "FORMAT", sensorRegister.format, "UNIT", sensorRegister.unit,
+ "PATH_SUFFIX", sensorRegister.pathSuffix);
+ }
+
+ for (const auto& [address, statusBits] : config.statusRegisters)
+ {
+ for (const auto& statusBit : statusBits)
+ {
+ info("Status Bit {NAME} {ADDRESS} {BIT_POSITION} {VALUE} {TYPE}",
+ "NAME", statusBit.name, "ADDRESS", address, "BIT_POSITION",
+ statusBit.bitPosition, "VALUE", statusBit.value, "TYPE",
+ statusBit.type);
+ }
+ }
+
+ for (const auto& firmwareRegister : config.firmwareRegisters)
+ {
+ info("Firmware Register {NAME} {ADDRESS} {SIZE} {TYPE}", "NAME",
+ firmwareRegister.name, "ADDRESS", firmwareRegister.offset, "SIZE",
+ firmwareRegister.size, "TYPE", firmwareRegister.type);
+ }
+}
+
+static auto getConfigSubTree(Config& config, const std::string& interfaceName,
+ const InventoryData& deviceConfig) -> void
+{
+ std::string firmwareRegistersInterface =
+ interfaceName + ".FirmwareRegisters";
+ std::string sensorRegistersInterface = interfaceName + ".SensorRegisters";
+ std::string statusBitsInterface = interfaceName + ".StatusBits";
+
+ for (const auto& [curInterface, interfaceConfig] : deviceConfig)
+ {
+ if (curInterface == interfaceName)
+ {
+ processDeviceInterface(config, interfaceConfig);
+ }
+ else if (curInterface.starts_with(sensorRegistersInterface))
+ {
+ processSensorRegistersInterface(config, interfaceConfig);
+ }
+ else if (curInterface.starts_with(statusBitsInterface))
+ {
+ processStatusBitsInterface(config, interfaceConfig);
+ }
+ else if (curInterface.starts_with(firmwareRegistersInterface))
+ {
+ processFirmwareRegistersInterface(config, interfaceConfig);
+ }
+ }
+}
+
+auto updateBaseConfig(sdbusplus::async::context& ctx,
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName, Config& config)
+ -> sdbusplus::async::task<bool>
+{
+ config.inventoryPath = objectPath.parent_path();
+
+ 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 != objectPath.str)
+ {
+ debug("Skipping device {PATH}", "PATH", path.str);
+ continue;
+ }
+ debug("Processing device {PATH}", "PATH", path.str);
+
+ try
+ {
+ getConfigSubTree(config, interfaceName, deviceConfig);
+ }
+ catch (std::exception& e)
+ {
+ error("Failed to process device {PATH} with {ERROR}", "PATH",
+ path.str, "ERROR", e);
+ co_return false;
+ }
+ }
+
+ printConfig(config);
+
+ co_return true;
+}
+
+} // namespace phosphor::modbus::rtu::device::config
diff --git a/rtu/device/base_config.hpp b/rtu/device/base_config.hpp
new file mode 100644
index 0000000..595c0df
--- /dev/null
+++ b/rtu/device/base_config.hpp
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "modbus/modbus.hpp"
+
+#include <xyz/openbmc_project/Sensor/Value/client.hpp>
+
+namespace phosphor::modbus::rtu::device
+{
+
+namespace ModbusIntf = phosphor::modbus::rtu;
+
+namespace config
+{
+
+using SensorValueIntf =
+ sdbusplus::client::xyz::openbmc_project::sensor::Value<>;
+
+enum class SensorFormat
+{
+ floatingPoint,
+ integer,
+ unknown
+};
+
+struct SensorRegister
+{
+ std::string name = "unknown";
+ std::string pathSuffix = "unknown";
+ SensorValueIntf::Unit unit;
+ uint16_t offset = 0;
+ uint8_t size = 0;
+ uint8_t precision = 0;
+ double scale = 1.0;
+ double shift = 0.0;
+ bool isSigned = false;
+ SensorFormat format = SensorFormat::unknown;
+};
+
+enum class StatusType
+{
+ controllerFailure,
+ fanFailure,
+ filterFailure,
+ powerFault,
+ pumpFailure,
+ leakDetectedCritical,
+ leakDetectedWarning,
+ sensorFailure,
+ sensorReadingCritical,
+ sensorReadingWarning,
+ unknown
+};
+
+struct StatusBit
+{
+ std::string name = "unknown";
+ StatusType type = StatusType::unknown;
+ uint8_t bitPosition = 0;
+ bool value = false;
+};
+
+enum class FirmwareRegisterType
+{
+ version,
+ update,
+ unknown
+};
+
+struct FirmwareRegister
+{
+ std::string name = "unknown";
+ FirmwareRegisterType type = FirmwareRegisterType::unknown;
+ uint16_t offset = 0;
+ uint8_t size = 0;
+};
+
+struct Config
+{
+ using sensor_registers_t = std::vector<SensorRegister>;
+ using status_registers_t =
+ std::unordered_map<uint16_t, std::vector<StatusBit>>;
+ using firmware_registers_t = std::vector<FirmwareRegister>;
+
+ uint8_t address = 0;
+ ModbusIntf::Parity parity = ModbusIntf::Parity::unknown;
+ uint32_t baudRate = 0;
+ std::string name = "unknown";
+ std::string portName = "unknown";
+ sdbusplus::message::object_path inventoryPath;
+ sensor_registers_t sensorRegisters;
+ status_registers_t statusRegisters;
+ firmware_registers_t firmwareRegisters;
+};
+
+auto updateBaseConfig(sdbusplus::async::context& ctx,
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName, Config& config)
+ -> sdbusplus::async::task<bool>;
+
+} // namespace config
+
+} // namespace phosphor::modbus::rtu::device
diff --git a/rtu/device/base_device.cpp b/rtu/device/base_device.cpp
new file mode 100644
index 0000000..73d1ca6
--- /dev/null
+++ b/rtu/device/base_device.cpp
@@ -0,0 +1,159 @@
+#include "base_device.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+#include <numeric>
+
+namespace phosphor::modbus::rtu::device
+{
+
+PHOSPHOR_LOG2_USING;
+
+BaseDevice::BaseDevice(sdbusplus::async::context& ctx,
+ const config::Config& config, PortIntf& serialPort) :
+ ctx(ctx), config(config), serialPort(serialPort)
+{
+ createSensors();
+
+ info("Successfully created device {NAME}", "NAME", config.name);
+}
+
+static auto getObjectPath(const std::string& sensorType,
+ const std::string& sensorName)
+ -> sdbusplus::message::object_path
+{
+ return sdbusplus::message::object_path(
+ std::string(SensorValueIntf::namespace_path::value) + "/" + sensorType +
+ "/" + sensorName);
+}
+
+auto BaseDevice::createSensors() -> void
+{
+ for (const auto& sensorRegister : config.sensorRegisters)
+ {
+ SensorValueIntf::properties_t initProperties = {
+ std::numeric_limits<double>::quiet_NaN(),
+ std::numeric_limits<double>::quiet_NaN(),
+ std::numeric_limits<double>::quiet_NaN(), sensorRegister.unit};
+
+ auto sensorPath = getObjectPath(
+ sensorRegister.pathSuffix, config.name + "_" + sensorRegister.name);
+
+ auto sensor = std::make_unique<SensorValueIntf>(
+ ctx, sensorPath.str.c_str(), initProperties);
+
+ sensor->emit_added();
+
+ sensors.emplace(sensorRegister.name, std::move(sensor));
+ }
+
+ return;
+}
+
+static auto getRawIntegerFromRegister(const std::vector<uint16_t>& reg,
+ bool sign) -> int64_t
+{
+ if (reg.empty())
+ {
+ return 0;
+ }
+
+ uint64_t accumulator = 0;
+ for (auto val : reg)
+ {
+ accumulator = (accumulator << 16) | val;
+ }
+
+ int64_t result = 0;
+
+ if (sign)
+ {
+ if (reg.size() == 1)
+ {
+ result = static_cast<int16_t>(accumulator);
+ }
+ else if (reg.size() == 2)
+ {
+ result = static_cast<int32_t>(accumulator);
+ }
+ else
+ {
+ result = static_cast<int64_t>(accumulator);
+ }
+ }
+ else
+ {
+ if (reg.size() == 1)
+ {
+ result = static_cast<uint16_t>(accumulator);
+ }
+ else if (reg.size() == 2)
+ {
+ result = static_cast<uint32_t>(accumulator);
+ }
+ else
+ {
+ result = static_cast<int64_t>(accumulator);
+ }
+ }
+
+ return result;
+}
+
+auto BaseDevice::readSensorRegisters() -> sdbusplus::async::task<void>
+{
+ while (!ctx.stop_requested())
+ {
+ for (const auto& sensorRegister : config.sensorRegisters)
+ {
+ auto sensor = sensors.find(sensorRegister.name);
+ if (sensor == sensors.end())
+ {
+ error("Sensor not found for {NAME}", "NAME",
+ sensorRegister.name);
+ continue;
+ }
+
+ if (sensorRegister.size > 4)
+ {
+ error("Unsupported size for register {NAME}", "NAME",
+ sensorRegister.name);
+ continue;
+ }
+
+ auto registers = std::vector<uint16_t>(sensorRegister.size);
+ auto ret = co_await serialPort.readHoldingRegisters(
+ config.address, sensorRegister.offset, config.baudRate,
+ config.parity, registers);
+ if (!ret)
+ {
+ error(
+ "Failed to read holding registers {NAME} for {DEVICE_ADDRESS}",
+ "NAME", sensorRegister.name, "DEVICE_ADDRESS",
+ config.address);
+ continue;
+ }
+
+ double regVal = static_cast<double>(
+ getRawIntegerFromRegister(registers, sensorRegister.isSigned));
+ if (sensorRegister.format == config::SensorFormat::floatingPoint)
+ {
+ regVal = sensorRegister.shift +
+ (sensorRegister.scale *
+ (regVal / (1ULL << sensorRegister.precision)));
+ }
+
+ sensor->second->value(regVal);
+ }
+
+ constexpr auto pollInterval = 3;
+ co_await sdbusplus::async::sleep_for(
+ ctx, std::chrono::seconds(pollInterval));
+ debug("Polling sensors for {NAME} in {INTERVAL} seconds", "NAME",
+ config.name, "INTERVAL", pollInterval);
+ }
+
+ co_return;
+}
+
+} // namespace phosphor::modbus::rtu::device
diff --git a/rtu/device/base_device.hpp b/rtu/device/base_device.hpp
new file mode 100644
index 0000000..a64d708
--- /dev/null
+++ b/rtu/device/base_device.hpp
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "base_config.hpp"
+#include "modbus/modbus.hpp"
+#include "port/base_port.hpp"
+
+#include <sdbusplus/async.hpp>
+#include <xyz/openbmc_project/Sensor/Value/aserver.hpp>
+
+namespace phosphor::modbus::rtu::device
+{
+
+class Device;
+
+using SensorValueIntf =
+ sdbusplus::aserver::xyz::openbmc_project::sensor::Value<Device>;
+using PortIntf = phosphor::modbus::rtu::port::BasePort;
+
+class BaseDevice
+{
+ public:
+ BaseDevice() = delete;
+
+ explicit BaseDevice(sdbusplus::async::context& ctx,
+ const config::Config& config, PortIntf& serialPort);
+
+ auto readSensorRegisters() -> sdbusplus::async::task<void>;
+
+ private:
+ auto createSensors() -> void;
+
+ using sensors_map_t =
+ std::unordered_map<std::string, std::unique_ptr<SensorValueIntf>>;
+ sdbusplus::async::context& ctx;
+ const config::Config config;
+ PortIntf& serialPort;
+ sensors_map_t sensors;
+};
+
+} // namespace phosphor::modbus::rtu::device
diff --git a/rtu/device/device_factory.cpp b/rtu/device/device_factory.cpp
new file mode 100644
index 0000000..d707952
--- /dev/null
+++ b/rtu/device/device_factory.cpp
@@ -0,0 +1,54 @@
+#include "device_factory.hpp"
+
+#include "reservoir_pump_unit.hpp"
+
+#include <string>
+#include <vector>
+
+namespace phosphor::modbus::rtu::device
+{
+
+using ReservoirPumpUnitIntf = phosphor::modbus::rtu::device::ReservoirPumpUnit;
+
+auto DeviceFactory::getInterfaces() -> std::vector<std::string>
+{
+ std::vector<std::string> interfaces{};
+
+ auto rpuInterfaces = ReservoirPumpUnitIntf::getInterfaces();
+ interfaces.insert(interfaces.end(), rpuInterfaces.begin(),
+ rpuInterfaces.end());
+
+ return interfaces;
+}
+
+auto DeviceFactory::getConfig(sdbusplus::async::context& ctx,
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName)
+ -> sdbusplus::async::task<std::optional<config::DeviceFactoryConfig>>
+{
+ auto rpuInterfaces = ReservoirPumpUnitIntf::getInterfaces();
+ if (rpuInterfaces.find(interfaceName) != rpuInterfaces.end())
+ {
+ co_return co_await ReservoirPumpUnitIntf::getConfig(ctx, objectPath,
+ interfaceName);
+ }
+
+ co_return std::nullopt;
+}
+
+auto DeviceFactory::create(sdbusplus::async::context& ctx,
+ const config::DeviceFactoryConfig& config,
+ PortIntf& serialPort) -> std::unique_ptr<BaseDevice>
+{
+ switch (config.deviceType)
+ {
+ case config::DeviceType::reservoirPumpUnit:
+ return std::make_unique<ReservoirPumpUnit>(ctx, config, serialPort);
+ default:
+ break;
+ }
+
+ return nullptr;
+}
+
+} // namespace phosphor::modbus::rtu::device
diff --git a/rtu/device/device_factory.hpp b/rtu/device/device_factory.hpp
new file mode 100644
index 0000000..5dca705
--- /dev/null
+++ b/rtu/device/device_factory.hpp
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "base_device.hpp"
+
+namespace phosphor::modbus::rtu::device
+{
+
+namespace config
+{
+
+enum class DeviceType
+{
+ reservoirPumpUnit,
+ heatExchanger,
+ flowMeter,
+ unknown
+};
+
+enum class DeviceModel
+{
+ RDF040DSS5193E0,
+ unknown
+};
+
+struct DeviceFactoryConfig : public Config
+{
+ DeviceType deviceType = DeviceType::unknown;
+ DeviceModel deviceModel = DeviceModel::unknown;
+};
+
+} // namespace config
+
+class DeviceFactory
+{
+ public:
+ DeviceFactory() = delete;
+
+ static auto getInterfaces() -> std::vector<std::string>;
+
+ static auto getConfig(sdbusplus::async::context& ctx,
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName)
+ -> sdbusplus::async::task<std::optional<config::DeviceFactoryConfig>>;
+
+ static auto create(sdbusplus::async::context& ctx,
+ const config::DeviceFactoryConfig& config,
+ PortIntf& serialPort) -> std::unique_ptr<BaseDevice>;
+};
+
+} // namespace phosphor::modbus::rtu::device
diff --git a/rtu/device/reservoir_pump_unit.cpp b/rtu/device/reservoir_pump_unit.cpp
new file mode 100644
index 0000000..58debee
--- /dev/null
+++ b/rtu/device/reservoir_pump_unit.cpp
@@ -0,0 +1,68 @@
+#include "reservoir_pump_unit.hpp"
+
+#include "device_factory.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+
+namespace phosphor::modbus::rtu::device
+{
+
+PHOSPHOR_LOG2_USING;
+
+static constexpr auto ModbusRDF040DSS5193E0ReservoirPumpUnitInterface =
+ "xyz.openbmc_project.Configuration.ModbusRDF040DSS5193E0ReservoirPumpUnit";
+
+static const std::unordered_map<std::string_view, config::DeviceModel>
+ validDevices = {{ModbusRDF040DSS5193E0ReservoirPumpUnitInterface,
+ config::DeviceModel::RDF040DSS5193E0}};
+
+ReservoirPumpUnit::ReservoirPumpUnit(sdbusplus::async::context& ctx,
+ const config::Config& config,
+ PortIntf& serialPort) :
+ BaseDevice(ctx, config, serialPort)
+{
+ info("Reservoir pump unit {NAME} created successfully", "NAME",
+ config.name);
+}
+
+auto ReservoirPumpUnit::getInterfaces() -> std::unordered_set<std::string>
+{
+ return {ModbusRDF040DSS5193E0ReservoirPumpUnitInterface};
+}
+
+auto ReservoirPumpUnit::getConfig(
+ sdbusplus::async::context& ctx,
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName)
+ -> sdbusplus::async::task<std::optional<config::DeviceFactoryConfig>>
+{
+ config::DeviceFactoryConfig config{};
+
+ auto res = co_await config::updateBaseConfig(ctx, objectPath, interfaceName,
+ config);
+ if (!res)
+ {
+ co_return std::nullopt;
+ }
+
+ for (const auto& [deviceInterface, deviceModel] : validDevices)
+ {
+ if (interfaceName == deviceInterface)
+ {
+ config.deviceModel = deviceModel;
+ }
+ }
+
+ if (config.deviceModel == config::DeviceModel::unknown)
+ {
+ error("Invalid device model {MODEL} for {NAME}", "MODEL", interfaceName,
+ "NAME", config.name);
+ co_return std::nullopt;
+ }
+
+ config.deviceType = config::DeviceType::reservoirPumpUnit;
+
+ co_return config;
+}
+
+} // namespace phosphor::modbus::rtu::device
diff --git a/rtu/device/reservoir_pump_unit.hpp b/rtu/device/reservoir_pump_unit.hpp
new file mode 100644
index 0000000..19240f0
--- /dev/null
+++ b/rtu/device/reservoir_pump_unit.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "base_device.hpp"
+
+#include <unordered_set>
+
+namespace phosphor::modbus::rtu::device
+{
+
+namespace config
+{
+
+struct DeviceFactoryConfig;
+
+} // namespace config
+
+class ReservoirPumpUnit : public BaseDevice
+{
+ public:
+ explicit ReservoirPumpUnit(sdbusplus::async::context& ctx,
+ const config::Config& config,
+ PortIntf& serialPort);
+
+ static auto getInterfaces() -> std::unordered_set<std::string>;
+
+ static auto getConfig(sdbusplus::async::context& ctx,
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName)
+ -> sdbusplus::async::task<std::optional<config::DeviceFactoryConfig>>;
+};
+
+} // namespace phosphor::modbus::rtu::device
diff --git a/rtu/device_manager.cpp b/rtu/device_manager.cpp
index 6d2fbd6..804f6eb 100644
--- a/rtu/device_manager.cpp
+++ b/rtu/device_manager.cpp
@@ -1,5 +1,6 @@
#include "device_manager.hpp"
+#include "device/device_factory.hpp"
#include "port/port_factory.hpp"
#include <phosphor-logging/lg2.hpp>
@@ -14,6 +15,7 @@
using ModbusRTUDetectIntf =
sdbusplus::client::xyz::openbmc_project::configuration::ModbusRTUDetect<>;
+using DeviceFactoryIntf = phosphor::modbus::rtu::device::DeviceFactory;
static entity_manager::interface_list_t getInterfaces()
{
@@ -22,8 +24,13 @@
auto portInterfaces = PortIntf::PortFactory::getInterfaces();
interfaces.insert(interfaces.end(), portInterfaces.begin(),
portInterfaces.end());
+
interfaces.emplace_back(ModbusRTUDetectIntf::interface);
+ auto deviceInterfaces = DeviceFactoryIntf::getInterfaces();
+ interfaces.insert(interfaces.end(), deviceInterfaces.begin(),
+ deviceInterfaces.end());
+
return interfaces;
}
@@ -54,48 +61,106 @@
if (std::find(portInterfaces.begin(), portInterfaces.end(),
interfaceName) != portInterfaces.end())
{
- auto config = co_await PortIntf::PortFactory::getConfig(
- ctx, objectPath, interfaceName);
- if (!config)
- {
- error("Failed to get Port config for {PATH}", "PATH", objectPath);
- co_return;
- }
-
- try
- {
- ports[config->name] = PortIntf::PortFactory::create(ctx, *config);
- }
- catch (const std::exception& e)
- {
- error("Failed to create Port for {PATH} with {ERROR}", "PATH",
- objectPath, "ERROR", e);
- co_return;
- }
+ co_return co_await processPortAdded(objectPath, interfaceName);
}
- else if (interfaceName == ModbusRTUDetectIntf::interface)
+
+ if (interfaceName == ModbusRTUDetectIntf::interface)
{
- auto res = co_await InventoryIntf::config::getConfig(ctx, objectPath);
- if (!res)
+ co_return co_await processInventoryAdded(objectPath);
+ }
+
+ auto deviceInterfaces = DeviceFactoryIntf::getInterfaces();
+ if (std::find(deviceInterfaces.begin(), deviceInterfaces.end(),
+ interfaceName) != deviceInterfaces.end())
+ {
+ co_return co_await processDeviceAdded(objectPath, interfaceName);
+ }
+}
+
+auto DeviceManager::processPortAdded(
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName) -> sdbusplus::async::task<>
+{
+ auto config = co_await PortIntf::PortFactory::getConfig(
+ ctx, objectPath, interfaceName);
+ if (!config)
+ {
+ error("Failed to get Port config for {PATH}", "PATH", objectPath);
+ co_return;
+ }
+
+ try
+ {
+ ports[config->name] = PortIntf::PortFactory::create(ctx, *config);
+ }
+ catch (const std::exception& e)
+ {
+ error("Failed to create Port for {PATH} with {ERROR}", "PATH",
+ objectPath, "ERROR", e);
+ co_return;
+ }
+}
+
+auto DeviceManager::processInventoryAdded(
+ const sdbusplus::message::object_path& objectPath)
+ -> sdbusplus::async::task<>
+{
+ auto res = co_await InventoryIntf::config::getConfig(ctx, objectPath);
+ if (!res)
+ {
+ error("Failed to get Inventory Device config for {PATH}", "PATH",
+ objectPath);
+ co_return;
+ }
+ auto config = res.value();
+ try
+ {
+ auto inventoryDevice =
+ std::make_unique<InventoryIntf::Device>(ctx, config, ports);
+ ctx.spawn(inventoryDevice->probePorts());
+ inventoryDevices[config.name] = std::move(inventoryDevice);
+ }
+ catch (const std::exception& e)
+ {
+ error("Failed to create Inventory Device for {PATH} with {ERROR}",
+ "PATH", objectPath, "ERROR", e);
+ co_return;
+ }
+}
+
+auto DeviceManager::processDeviceAdded(
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName) -> sdbusplus::async::task<>
+{
+ auto res =
+ co_await DeviceFactoryIntf::getConfig(ctx, objectPath, interfaceName);
+ if (!res)
+ {
+ error("Failed to get Device config for {PATH}", "PATH", objectPath);
+ co_return;
+ }
+ auto config = res.value();
+
+ try
+ {
+ auto serialPort = ports.find(config.portName);
+ if (serialPort == ports.end())
{
error("Failed to get Inventory Device config for {PATH}", "PATH",
objectPath);
co_return;
}
- auto config = res.value();
- try
- {
- auto inventoryDevice =
- std::make_unique<InventoryIntf::Device>(ctx, config, ports);
- ctx.spawn(inventoryDevice->probePorts());
- inventoryDevices[config.name] = std::move(inventoryDevice);
- }
- catch (const std::exception& e)
- {
- error("Failed to create Inventory Device for {PATH} with {ERROR}",
- "PATH", objectPath, "ERROR", e);
- co_return;
- }
+
+ auto device =
+ DeviceFactoryIntf::create(ctx, config, *(serialPort->second));
+ ctx.spawn(device->readSensorRegisters());
+ devices[config.name] = std::move(device);
+ }
+ catch (const std::exception& e)
+ {
+ error("Failed to create Device for {PATH} with {ERROR}", "PATH",
+ objectPath, "ERROR", e);
+ co_return;
}
}
diff --git a/rtu/device_manager.hpp b/rtu/device_manager.hpp
index 68daa64..75fe75b 100644
--- a/rtu/device_manager.hpp
+++ b/rtu/device_manager.hpp
@@ -1,6 +1,7 @@
#pragma once
#include "common/entity_manager_interface.hpp"
+#include "device/base_device.hpp"
#include "inventory/modbus_inventory.hpp"
#include "port/base_port.hpp"
@@ -11,6 +12,8 @@
namespace InventoryIntf = phosphor::modbus::rtu::inventory;
namespace PortIntf = phosphor::modbus::rtu::port;
+namespace ModbusIntf = phosphor::modbus::rtu;
+namespace DeviceIntf = phosphor::modbus::rtu::device;
class DeviceManager
{
@@ -26,13 +29,30 @@
private:
using inventory_device_map_t =
std::unordered_map<std::string, std::unique_ptr<InventoryIntf::Device>>;
+
using port_map_t =
std::unordered_map<std::string, std::unique_ptr<PortIntf::BasePort>>;
+ using device_map_t =
+ std::unordered_map<std::string,
+ std::unique_ptr<DeviceIntf::BaseDevice>>;
+
auto processConfigAdded(const sdbusplus::message::object_path& objectPath,
const std::string& interfaceName)
-> sdbusplus::async::task<>;
+ auto processPortAdded(const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName)
+ -> sdbusplus::async::task<>;
+
+ auto processInventoryAdded(
+ const sdbusplus::message::object_path& objectPath)
+ -> sdbusplus::async::task<>;
+
+ auto processDeviceAdded(const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName)
+ -> sdbusplus::async::task<>;
+
auto processConfigRemoved(const sdbusplus::message::object_path& objectPath,
const std::string& interfaceName)
-> sdbusplus::async::task<>;
@@ -41,6 +61,7 @@
entity_manager::EntityManagerInterface entityManager;
inventory_device_map_t inventoryDevices;
port_map_t ports;
+ device_map_t devices; // Modbus devices
};
} // namespace phosphor::modbus::rtu
diff --git a/rtu/inventory/modbus_inventory.cpp b/rtu/inventory/modbus_inventory.cpp
index 16fa56a..22afa2a 100644
--- a/rtu/inventory/modbus_inventory.cpp
+++ b/rtu/inventory/modbus_inventory.cpp
@@ -236,7 +236,7 @@
Device::Device(sdbusplus::async::context& ctx, const config::Config& config,
serial_port_map_t& serialPorts) :
- ctx(ctx), config(config), serialPorts(serialPorts)
+ config(config), ctx(ctx), serialPorts(serialPorts)
{
for (const auto& [serialPort, _] : config.addressMap)
{
diff --git a/rtu/inventory/modbus_inventory.hpp b/rtu/inventory/modbus_inventory.hpp
index ed909c7..829024a 100644
--- a/rtu/inventory/modbus_inventory.hpp
+++ b/rtu/inventory/modbus_inventory.hpp
@@ -81,9 +81,10 @@
SerialPortIntf& port)
-> sdbusplus::async::task<void>;
+ const config::Config config;
+
private:
sdbusplus::async::context& ctx;
- const config::Config config;
serial_port_map_t& serialPorts;
std::map<std::string, std::unique_ptr<InventorySourceIntf>>
inventorySources;
diff --git a/rtu/meson.build b/rtu/meson.build
index fa1bd9f..a8b9101 100644
--- a/rtu/meson.build
+++ b/rtu/meson.build
@@ -25,9 +25,16 @@
inventory_src = files('inventory/modbus_inventory.cpp')
+device_src = files(
+ 'device/base_config.cpp',
+ 'device/base_device.cpp',
+ 'device/device_factory.cpp',
+ 'device/reservoir_pump_unit.cpp',
+)
+
executable(
'phosphor-modbus-rtu',
- ['device_manager.cpp', inventory_src],
+ ['device_manager.cpp', inventory_src, device_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 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();
+}