rtu: implement modbus rtu inventory source service
Implement phosphor-modbus-rtu inventory source service based on [1].
[1]: https://gerrit.openbmc.org/c/openbmc/docs/+/77318
Tested: Unit test passes and tested on qemu with simulated modbus
server.
```
root@bmc:~# busctl tree xyz.openbmc_project.ModbusRTU
└─ /xyz
└─ /xyz/openbmc_project
└─ /xyz/openbmc_project/inventory_source
└─ /xyz/openbmc_project/inventory_source/modbus
├─ /xyz/openbmc_project/inventory_source/modbus/Heat_Exchanger_12_DevTTYUSB0
├─ /xyz/openbmc_project/inventory_source/modbus/Heat_Exchanger_12_DevTTYUSB1
├─ /xyz/openbmc_project/inventory_source/modbus/Reservoir_Pumping_Unit_12_DevTTYUSB0
└─ /xyz/openbmc_project/inventory_source/modbus/Reservoir_Pumping_Unit_12_DevTTYUSB1
root@bmc:~# busctl tree xyz.openbmc_project.EntityManager
└─ /xyz
└─ /xyz/openbmc_project
├─ /xyz/openbmc_project/EntityManager
└─ /xyz/openbmc_project/inventory
└─ /xyz/openbmc_project/inventory/system
├─ /xyz/openbmc_project/inventory/system/board
│ └─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus
│ ├─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus/DevTTYUSB0
│ ├─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus/DevTTYUSB1
│ ├─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus/Heat_Exchanger
│ └─ /xyz/openbmc_project/inventory/system/board/Ventura_Modbus/Reservoir_Pumping_Unit
└─ /xyz/openbmc_project/inventory/system/chassis
├─ /xyz/openbmc_project/inventory/system/chassis/Heat_Exchanger_12_DevTTYUSB0
├─ /xyz/openbmc_project/inventory/system/chassis/Heat_Exchanger_12_DevTTYUSB1
├─ /xyz/openbmc_project/inventory/system/chassis/Reservoir_Pumping_Unit_12_DevTTYUSB0
└─ /xyz/openbmc_project/inventory/system/chassis/Reservoir_Pumping_Unit_12_DevTTYUSB1
root@bmc:~# busctl introspect xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory/system/chassis/Heat_Exchanger_12_DevTTYUSB0
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.AddObject interface - - -
.AddObject method a{sv} - -
xyz.openbmc_project.Inventory.Decorator.Asset interface - - -
.BuildDate property s "Unknown" emits-change
.Manufacturer property s "Unknown" emits-change
.Model property s "Unknown" emits-change
.PartNumber property s "Unknown" emits-change
.SerialNumber property s "Unknown" emits-change
.SparePartNumber property s "ABABABAB" emits-change
xyz.openbmc_project.Inventory.Item.Chassis interface - - -
.Name property s "Heat Exchanger 12 DevTTYUSB0" emits-change
.Probe property s "xyz.openbmc_project.Inventory.Source.M… emits-change
.Type property s "Chassis" emits-change
root@bmc:~# busctl introspect xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory/system/chassis/Heat_Exchanger_12_DevTTYUSB1
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.AddObject interface - - -
.AddObject method a{sv} - -
xyz.openbmc_project.Inventory.Decorator.Asset interface - - -
.BuildDate property s "Unknown" emits-change
.Manufacturer property s "Unknown" emits-change
.Model property s "Unknown" emits-change
.PartNumber property s "Unknown" emits-change
.SerialNumber property s "Unknown" emits-change
.SparePartNumber property s "ABABABAB" emits-change
xyz.openbmc_project.Inventory.Item.Chassis interface - - -
.Name property s "Heat Exchanger 12 DevTTYUSB1" emits-change
.Probe property s "xyz.openbmc_project.Inventory.Source.M… emits-change
.Type property s "Chassis" emits-change
root@bmc:~# busctl introspect xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory/system/chassis/Reservoir_Pumping_Unit_12_DevTTYUSB0
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.AddObject interface - - -
.AddObject method a{sv} - -
xyz.openbmc_project.Inventory.Decorator.Asset interface - - -
.BuildDate property s "ABABABAB" emits-change
.Manufacturer property s "Unknown" emits-change
.Model property s "ABABABABABABABAB" emits-change
.PartNumber property s "Unknown" emits-change
.SerialNumber property s "ABABABABABABABAB" emits-change
.SparePartNumber property s "ABABABAB" emits-change
xyz.openbmc_project.Inventory.Item.Chassis interface - - -
.Name property s "Reservoir Pumping Unit 12 DevTTYUSB0" emits-change
.Probe property s "xyz.openbmc_project.Inventory.Source.M… emits-change
.Type property s "Chassis" emits-change
root@bmc:~# busctl introspect xyz.openbmc_project.EntityManager /xyz/openbmc_project/inventory/system/chassis/Reservoir_Pumping_Unit_12_DevTTYUSB1
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.AddObject interface - - -
.AddObject method a{sv} - -
xyz.openbmc_project.Inventory.Decorator.Asset interface - - -
.BuildDate property s "ABABABAB" emits-change
.Manufacturer property s "Unknown" emits-change
.Model property s "ABABABABABABABAB" emits-change
.PartNumber property s "Unknown" emits-change
.SerialNumber property s "ABABABABABABABAB" emits-change
.SparePartNumber property s "ABABABAB" emits-change
xyz.openbmc_project.Inventory.Item.Chassis interface - - -
.Name property s "Reservoir Pumping Unit 12 DevTTYUSB1" emits-change
.Probe property s "xyz.openbmc_project.Inventory.Source.M… emits-change
.Type property s "Chassis" emits-change
```
Change-Id: Ic0ea739de3833044c95da8164be1e2f3f8e6a063
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/rtu/device_manager.cpp b/rtu/device_manager.cpp
new file mode 100644
index 0000000..6d2fbd6
--- /dev/null
+++ b/rtu/device_manager.cpp
@@ -0,0 +1,126 @@
+#include "device_manager.hpp"
+
+#include "port/port_factory.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server/manager.hpp>
+#include <xyz/openbmc_project/Configuration/ModbusRTUDetect/client.hpp>
+
+PHOSPHOR_LOG2_USING;
+
+namespace phosphor::modbus::rtu
+{
+
+using ModbusRTUDetectIntf =
+ sdbusplus::client::xyz::openbmc_project::configuration::ModbusRTUDetect<>;
+
+static entity_manager::interface_list_t getInterfaces()
+{
+ entity_manager::interface_list_t interfaces;
+
+ auto portInterfaces = PortIntf::PortFactory::getInterfaces();
+ interfaces.insert(interfaces.end(), portInterfaces.begin(),
+ portInterfaces.end());
+ interfaces.emplace_back(ModbusRTUDetectIntf::interface);
+
+ return interfaces;
+}
+
+DeviceManager::DeviceManager(sdbusplus::async::context& ctx) :
+ ctx(ctx),
+ entityManager(ctx, getInterfaces(),
+ std::bind_front(&DeviceManager::processConfigAdded, this),
+ std::bind_front(&DeviceManager::processConfigRemoved, this))
+{
+ ctx.spawn(entityManager.handleInventoryGet());
+ info("DeviceManager created successfully");
+}
+
+auto DeviceManager::processConfigAdded(
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName) -> sdbusplus::async::task<>
+{
+ debug("Config added for {PATH} with {INTF}", "PATH", objectPath, "INTF",
+ interfaceName);
+ if (interfaceName == ModbusRTUDetectIntf::interface && ports.size() == 0)
+ {
+ warning(
+ "Skip processing ModbusRTUDetectIntf::interface as no serial ports detected yet");
+ co_return;
+ }
+
+ auto portInterfaces = PortIntf::PortFactory::getInterfaces();
+ 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;
+ }
+ }
+ else if (interfaceName == ModbusRTUDetectIntf::interface)
+ {
+ 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::processConfigRemoved(
+ const sdbusplus::message::object_path& /*unused*/,
+ const std::string& /*unused*/) -> sdbusplus::async::task<>
+{
+ // TODO: Implement this
+ co_return;
+}
+
+} // namespace phosphor::modbus::rtu
+
+auto main() -> int
+{
+ constexpr auto path = "/xyz/openbmc_project";
+ constexpr auto serviceName = "xyz.openbmc_project.ModbusRTU";
+ sdbusplus::async::context ctx;
+ sdbusplus::server::manager_t manager{ctx, path};
+
+ info("Creating Modbus device manager at {PATH}", "PATH", path);
+ phosphor::modbus::rtu::DeviceManager deviceManager{ctx};
+
+ ctx.request_name(serviceName);
+
+ ctx.run();
+ return 0;
+}
diff --git a/rtu/device_manager.hpp b/rtu/device_manager.hpp
new file mode 100644
index 0000000..68daa64
--- /dev/null
+++ b/rtu/device_manager.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "common/entity_manager_interface.hpp"
+#include "inventory/modbus_inventory.hpp"
+#include "port/base_port.hpp"
+
+#include <sdbusplus/async.hpp>
+
+namespace phosphor::modbus::rtu
+{
+
+namespace InventoryIntf = phosphor::modbus::rtu::inventory;
+namespace PortIntf = phosphor::modbus::rtu::port;
+
+class DeviceManager
+{
+ public:
+ DeviceManager() = delete;
+ DeviceManager(const DeviceManager&) = delete;
+ DeviceManager& operator=(const DeviceManager&) = delete;
+ DeviceManager(DeviceManager&&) = delete;
+ DeviceManager& operator=(DeviceManager&&) = delete;
+
+ explicit DeviceManager(sdbusplus::async::context& ctx);
+
+ 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>>;
+
+ auto processConfigAdded(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<>;
+
+ sdbusplus::async::context& ctx;
+ entity_manager::EntityManagerInterface entityManager;
+ inventory_device_map_t inventoryDevices;
+ port_map_t ports;
+};
+
+} // namespace phosphor::modbus::rtu
diff --git a/rtu/inventory/modbus_inventory.cpp b/rtu/inventory/modbus_inventory.cpp
new file mode 100644
index 0000000..16fa56a
--- /dev/null
+++ b/rtu/inventory/modbus_inventory.cpp
@@ -0,0 +1,435 @@
+#include "modbus_inventory.hpp"
+
+#include "common/entity_manager_interface.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/Configuration/ModbusRTUDetect/client.hpp>
+#include <xyz/openbmc_project/Inventory/Item/client.hpp>
+
+#include <flat_map>
+
+namespace phosphor::modbus::rtu::inventory
+{
+PHOSPHOR_LOG2_USING;
+
+namespace config
+{
+
+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}}};
+
+// TODO: This API will be dropped once EM supports non-indexed interfaces for
+// array objects.
+static auto processModbusAddressInterface(
+ Config& config, const InventoryBaseConfigMap& configMap) -> bool
+{
+ debug("Processing ModbusAddress {NAME}", "NAME", config.name);
+
+ auto rangeStartIter = configMap.find("RangeStart");
+ if (rangeStartIter == configMap.end())
+ {
+ error("Missing RangeStart for {NAME}", "NAME", config.name);
+ return false;
+ }
+ auto rangeStart = std::get<uint64_t>(rangeStartIter->second);
+
+ auto rangeEndIter = configMap.find("RangeEnd");
+ if (rangeEndIter == configMap.end())
+ {
+ error("Missing RangeEnd for {NAME}", "NAME", config.name);
+ return false;
+ }
+ auto rangeEnd = std::get<uint64_t>(rangeEndIter->second);
+
+ auto serialPortIter = configMap.find("SerialPort");
+ if (serialPortIter == configMap.end())
+ {
+ error("Missing SerialPort for {NAME}", "NAME", config.name);
+ return false;
+ }
+ auto serialPort = std::get<std::string>(serialPortIter->second);
+
+ config.addressMap[serialPort].push_back(AddressRange{
+ static_cast<uint8_t>(rangeStart), static_cast<uint8_t>(rangeEnd)});
+
+ debug("ModbusAddress {NAME} {PORT} {START} {END}", "NAME", config.name,
+ "PORT", serialPort, "START", rangeStart, "END", rangeEnd);
+
+ return true;
+}
+
+// TODO: This API will be dropped once EM supports non-indexed interfaces for
+// array objects.
+static auto processModbusRegistersInterface(
+ Config& config, const InventoryBaseConfigMap& configMap) -> bool
+{
+ debug("Processing ModbusRegisters {NAME}", "NAME", config.name);
+ Register registerConfig = {};
+
+ auto nameIter = configMap.find("Name");
+ if (nameIter == configMap.end())
+ {
+ error("Missing Name for {NAME}", "NAME", config.name);
+ return false;
+ }
+ registerConfig.name = std::get<std::string>(nameIter->second);
+
+ auto address = configMap.find("Address");
+ if (address == configMap.end())
+ {
+ error("Missing Address for {NAME}", "NAME", config.name);
+ return false;
+ }
+ registerConfig.offset = std::get<uint64_t>(address->second);
+
+ auto sizeIter = configMap.find("Size");
+ if (sizeIter == configMap.end())
+ {
+ error("Missing Size for {NAME}", "NAME", config.name);
+ return false;
+ }
+ registerConfig.size = std::get<uint64_t>(sizeIter->second);
+
+ config.registers.push_back(registerConfig);
+
+ debug("ModbusRegisters {NAME} {ADDRESS} {SIZE}", "NAME",
+ registerConfig.name, "ADDRESS", registerConfig.offset, "SIZE",
+ registerConfig.size);
+
+ return true;
+}
+
+static auto printConfig(const Config& config) -> void
+{
+ info("Inventory device config: {NAME} {BAUDRATE} {PARITY}", "NAME",
+ config.name, "BAUDRATE", config.baudRate, "PARITY", config.parity);
+
+ for (const auto& [port, addressRanges] : config.addressMap)
+ {
+ for (const auto& addressRange : addressRanges)
+ {
+ info(
+ "Inventory device config: {PORT} {ADDRESS_START} {ADDRESS_END}",
+ "PORT", port, "ADDRESS_START", addressRange.start,
+ "ADDRESS_END", addressRange.end);
+ }
+ }
+
+ for (const auto& registerConfig : config.registers)
+ {
+ info("Inventory device config: {NAME} {ADDRESS} {SIZE}", "NAME",
+ registerConfig.name, "ADDRESS", registerConfig.offset, "SIZE",
+ registerConfig.size);
+ }
+}
+
+auto getConfigSubInterfaces(sdbusplus::async::context& ctx,
+ sdbusplus::message::object_path objectPath,
+ Config& config) -> sdbusplus::async::task<bool>
+{
+ constexpr auto modbusAddressInterface =
+ "xyz.openbmc_project.Configuration.ModbusRTUDetect.Address";
+ constexpr auto modbusRegistersInterface =
+ "xyz.openbmc_project.Configuration.ModbusRTUDetect.Registers";
+
+ 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).starts_with(objectPath.str))
+ {
+ debug("Skipping device {PATH}", "PATH", path.str);
+ continue;
+ }
+ debug("Processing device {PATH}", "PATH", path.str);
+ for (const auto& [interfaceName, interfaceConfig] : deviceConfig)
+ {
+ if (interfaceName.starts_with(modbusAddressInterface))
+ {
+ if (!processModbusAddressInterface(config, interfaceConfig))
+ {
+ error("Failed to process {INTERFACE} for {NAME}",
+ "INTERFACE", modbusAddressInterface, "NAME",
+ config.name);
+ co_return false;
+ }
+ }
+ else if (interfaceName.starts_with(modbusRegistersInterface))
+ {
+ if (!processModbusRegistersInterface(config, interfaceConfig))
+ {
+ error("Failed to process {INTERFACE} for {NAME}",
+ "INTERFACE", modbusRegistersInterface, "NAME",
+ config.name);
+ co_return false;
+ }
+ }
+ }
+ }
+
+ co_return true;
+}
+
+auto getConfig(sdbusplus::async::context& ctx,
+ sdbusplus::message::object_path objectPath)
+ -> sdbusplus::async::task<std::optional<Config>>
+{
+ using ModbusRTUDetectIntf = sdbusplus::client::xyz::openbmc_project::
+ configuration::ModbusRTUDetect<>;
+
+ Config config = {};
+
+ auto properties =
+ co_await ModbusRTUDetectIntf(ctx)
+ .service(entity_manager::EntityManagerInterface::serviceName)
+ .path(objectPath.str)
+ .properties();
+
+ config.name = properties.name;
+ config.baudRate = properties.baud_rate;
+
+ for (const auto& [parityStr, parity] : config::validParities)
+ {
+ if (parityStr == properties.data_parity)
+ {
+ config.parity = parity;
+ break;
+ }
+ }
+ if (config.parity == Parity::unknown)
+ {
+ error("Invalid parity {PARITY} for {NAME}", "PARITY",
+ properties.data_parity, "NAME", properties.name);
+ co_return std::nullopt;
+ }
+
+ if (!co_await getConfigSubInterfaces(ctx, objectPath, config))
+ {
+ co_return std::nullopt;
+ }
+
+ printConfig(config);
+
+ co_return config;
+}
+
+} // namespace config
+
+Device::Device(sdbusplus::async::context& ctx, const config::Config& config,
+ serial_port_map_t& serialPorts) :
+ ctx(ctx), config(config), serialPorts(serialPorts)
+{
+ for (const auto& [serialPort, _] : config.addressMap)
+ {
+ if (serialPorts.find(serialPort) == serialPorts.end())
+ {
+ error("Serial port {PORT} not found for {NAME}", "PORT", serialPort,
+ "NAME", config.name);
+ continue;
+ }
+ }
+}
+
+auto Device::probePorts() -> sdbusplus::async::task<void>
+{
+ debug("Probing ports for {NAME}", "NAME", config.name);
+ while (!ctx.stop_requested())
+ {
+ for (const auto& [serialPort, _] : config.addressMap)
+ {
+ if (serialPorts.find(serialPort) == serialPorts.end())
+ {
+ continue;
+ }
+ ctx.spawn(probePort(serialPort));
+ }
+ constexpr auto probeInterval = 3;
+ co_await sdbusplus::async::sleep_for(
+ ctx, std::chrono::seconds(probeInterval));
+ debug("Probing ports for {NAME} in {INTERVAL} seconds", "NAME",
+ config.name, "INTERVAL", probeInterval);
+ }
+}
+
+auto Device::probePort(std::string portName) -> sdbusplus::async::task<void>
+{
+ debug("Probing port {PORT}", "PORT", portName);
+
+ auto portConfig = config.addressMap.find(portName);
+ if (portConfig == config.addressMap.end())
+ {
+ error("Serial port {PORT} address map not found for {NAME}", "PORT",
+ portName, "NAME", config.name);
+ co_return;
+ }
+ auto addressRanges = portConfig->second;
+
+ auto port = serialPorts.find(portName);
+ if (port == serialPorts.end())
+ {
+ error("Serial port {PORT} not found for {NAME}", "PORT", portName,
+ "NAME", config.name);
+ co_return;
+ }
+
+ for (const auto& addressRange : addressRanges)
+ {
+ for (auto address = addressRange.start; address <= addressRange.end;
+ address++)
+ {
+ co_await probeDevice(address, portName, *port->second);
+ }
+ }
+}
+
+auto Device::probeDevice(uint8_t address, const std::string& portName,
+ SerialPortIntf& port) -> sdbusplus::async::task<void>
+{
+ debug("Probing device at {ADDRESS} on port {PORT}", "ADDRESS", address,
+ "PORT", portName);
+
+ if (config.registers.size() == 0)
+ {
+ error("No registers configured for {NAME}", "NAME", config.name);
+ co_return;
+ }
+ auto probeRegister = config.registers[0].offset;
+ auto registers = std::vector<uint16_t>(config.registers[0].size);
+
+ auto sourceId = std::to_string(address) + "_" + portName;
+
+ auto ret = co_await port.readHoldingRegisters(
+ address, probeRegister, config.baudRate, config.parity, registers);
+ if (ret)
+ {
+ if (inventorySources.find(sourceId) == inventorySources.end())
+ {
+ debug("Device found at {ADDRESS}", "ADDRESS", address);
+ co_await addInventorySource(address, portName, port);
+ }
+ else
+ {
+ debug("Device already exists at {ADDRESS}", "ADDRESS", address);
+ }
+ }
+ else
+ {
+ if (inventorySources.find(sourceId) != inventorySources.end())
+ {
+ warning(
+ "Device removed at {ADDRESS} due to probe failure for {PROBE_REGISTER}",
+ "ADDRESS", address, "PROBE_REGISTER", probeRegister);
+ inventorySources[sourceId]->emit_removed();
+ inventorySources.erase(sourceId);
+ }
+ }
+}
+
+static auto fillInventorySourceProperties(
+ InventorySourceIntf::properties_t& properties, const std::string& regName,
+ std::string& strValue) -> void
+{
+ constexpr auto partNumber = "PartNumber";
+ constexpr auto sparePartNumber = "SparePartNumber";
+ constexpr auto serialNumber = "SerialNumber";
+ constexpr auto buildDate = "BuildDate";
+ constexpr auto model = "Model";
+ constexpr auto manufacturer = "Manufacturer";
+
+ if (regName == partNumber)
+ {
+ properties.part_number = strValue;
+ }
+ else if (regName == sparePartNumber)
+ {
+ properties.spare_part_number = strValue;
+ }
+ else if (regName == serialNumber)
+ {
+ properties.serial_number = strValue;
+ }
+ else if (regName == buildDate)
+ {
+ properties.build_date = strValue;
+ }
+ else if (regName == model)
+ {
+ properties.model = strValue;
+ }
+ else if (regName == manufacturer)
+ {
+ properties.manufacturer = strValue;
+ }
+}
+
+auto Device::addInventorySource(uint8_t address, const std::string& portName,
+ SerialPortIntf& port)
+ -> sdbusplus::async::task<void>
+{
+ InventorySourceIntf::properties_t properties;
+
+ for (const auto& reg : config.registers)
+ {
+ auto registers = std::vector<uint16_t>(reg.size);
+ auto ret = co_await port.readHoldingRegisters(
+ address, reg.offset, config.baudRate, config.parity, registers);
+ if (!ret)
+ {
+ error(
+ "Failed to read holding registers {NAME} for {DEVICE_ADDRESS}",
+ "NAME", reg.name, "DEVICE_ADDRESS", address);
+ continue;
+ }
+
+ std::string strValue = "";
+
+ // Reswap bytes in each register for string conversion
+ for (const auto& value : registers)
+ {
+ strValue += static_cast<char>((value >> 8) & 0xFF);
+ strValue += static_cast<char>(value & 0xFF);
+ }
+
+ fillInventorySourceProperties(properties, reg.name, strValue);
+ }
+
+ auto pathSuffix =
+ config.name + " " + std::to_string(address) + " " + portName;
+
+ properties.name = pathSuffix;
+ properties.address = address;
+ properties.link_tty = portName;
+
+ std::replace(pathSuffix.begin(), pathSuffix.end(), ' ', '_');
+
+ auto objectPath =
+ std::string(InventorySourceIntf::namespace_path) + "/" + pathSuffix;
+ auto sourceId = std::to_string(address) + "_" + portName;
+
+ inventorySources[sourceId] = std::make_unique<InventorySourceIntf>(
+ ctx, objectPath.c_str(), properties);
+ inventorySources[sourceId]->emit_added();
+
+ info("Added InventorySource at {PATH}", "PATH", objectPath);
+}
+
+} // namespace phosphor::modbus::rtu::inventory
diff --git a/rtu/inventory/modbus_inventory.hpp b/rtu/inventory/modbus_inventory.hpp
new file mode 100644
index 0000000..ed909c7
--- /dev/null
+++ b/rtu/inventory/modbus_inventory.hpp
@@ -0,0 +1,92 @@
+#pragma once
+
+#include "modbus/modbus.hpp"
+#include "port/base_port.hpp"
+
+#include <sdbusplus/async.hpp>
+#include <xyz/openbmc_project/Inventory/Source/Modbus/FRU/aserver.hpp>
+
+#include <cstdint>
+#include <map>
+#include <string>
+#include <tuple>
+#include <vector>
+
+namespace phosphor::modbus::rtu::inventory
+{
+
+class Device;
+
+namespace ModbusIntf = phosphor::modbus::rtu;
+using SerialPortIntf = phosphor::modbus::rtu::port::BasePort;
+using InventorySourceIntf =
+ sdbusplus::aserver::xyz::openbmc_project::inventory::source::modbus::FRU<
+ Device>;
+
+namespace config
+{
+
+struct Register
+{
+ std::string name = "unknown";
+ uint16_t offset = 0;
+ uint8_t size = 0;
+};
+
+struct AddressRange
+{
+ uint8_t start;
+ uint8_t end;
+};
+
+struct Config
+{
+ using address_range_arr_t = std::vector<AddressRange>;
+ using port_address_map_t =
+ std::map<std::string,
+ address_range_arr_t>; // <port name, device address range list>
+
+ std::string name = "unknown";
+ port_address_map_t addressMap = {};
+ std::vector<Register> registers = {};
+ ModbusIntf::Parity parity = ModbusIntf::Parity::unknown;
+ uint32_t baudRate = 0;
+};
+
+auto getConfig(sdbusplus::async::context& ctx,
+ sdbusplus::message::object_path objectPath)
+ -> sdbusplus::async::task<std::optional<Config>>;
+
+} // namespace config
+
+class Device
+{
+ public:
+ Device() = delete;
+ using serial_port_map_t =
+ std::unordered_map<std::string, std::unique_ptr<SerialPortIntf>>;
+
+ explicit Device(sdbusplus::async::context& ctx,
+ const config::Config& config,
+ serial_port_map_t& serialPorts);
+
+ auto probePorts() -> sdbusplus::async::task<void>;
+
+ auto probePort(std::string portName) -> sdbusplus::async::task<void>;
+
+ auto probeDevice(uint8_t address, const std::string& portName,
+ SerialPortIntf& port) -> sdbusplus::async::task<void>;
+
+ auto addInventorySource(uint8_t address, const std::string& portName,
+ SerialPortIntf& port)
+ -> sdbusplus::async::task<void>;
+
+ private:
+ sdbusplus::async::context& ctx;
+ const config::Config config;
+ serial_port_map_t& serialPorts;
+ std::map<std::string, std::unique_ptr<InventorySourceIntf>>
+ inventorySources;
+};
+
+} // namespace phosphor::modbus::rtu::inventory
diff --git a/rtu/meson.build b/rtu/meson.build
index 2c385b2..fa1bd9f 100644
--- a/rtu/meson.build
+++ b/rtu/meson.build
@@ -22,3 +22,25 @@
link_with: [modbus_rtu_lib, modbus_rtu_port_lib],
dependencies: [default_deps],
)
+
+inventory_src = files('inventory/modbus_inventory.cpp')
+
+executable(
+ 'phosphor-modbus-rtu',
+ ['device_manager.cpp', inventory_src],
+ include_directories: ['.', common_include],
+ dependencies: [default_deps],
+ link_with: [modbus_common_lib, modbus_rtu_lib, modbus_rtu_port_lib],
+ install: true,
+ install_dir: get_option('libexecdir') / 'phosphor-modbus',
+)
+
+systemd_system_unit_dir = dependency('systemd').get_variable(
+ 'systemdsystemunitdir',
+ pkgconfig_define: ['prefix', get_option('prefix')],
+)
+
+install_data(
+ 'xyz.openbmc_project.ModbusRTU.service',
+ install_dir: systemd_system_unit_dir,
+)
diff --git a/rtu/xyz.openbmc_project.ModbusRTU.service b/rtu/xyz.openbmc_project.ModbusRTU.service
new file mode 100644
index 0000000..564fc94
--- /dev/null
+++ b/rtu/xyz.openbmc_project.ModbusRTU.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=Modbus RTU Service
+Requires=xyz.openbmc_project.EntityManager.service
+After=xyz.openbmc_project.EntityManager.service
+
+[Service]
+Type=dbus
+BusName=xyz.openbmc_project.ModbusRTU
+Restart=always
+RestartSec=5
+ExecStart=/usr/libexec/phosphor-modbus/phosphor-modbus-rtu
+
+[Install]
+WantedBy=multi-user.target
diff --git a/tests/meson.build b/tests/meson.build
index 2be37a5..bcc4fad 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -50,3 +50,15 @@
include_directories: ['.', common_include],
),
)
+
+test(
+ 'test_inventory',
+ executable(
+ 'test_inventory',
+ 'test_inventory.cpp',
+ 'modbus_server_tester.cpp',
+ inventory_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 a08e51f..fe1b825 100644
--- a/tests/modbus_server_tester.cpp
+++ b/tests/modbus_server_tester.cpp
@@ -21,11 +21,15 @@
constexpr uint8_t readHoldingRegistersErrorFunctionCode = 0x83;
ServerTester::ServerTester(sdbusplus::async::context& ctx, int fd) :
- fd(fd), fdioInstance(ctx, fd)
+ fd(fd), fdioInstance(ctx, fd), mutex("TestMutex")
{}
auto ServerTester::processRequests() -> sdbusplus::async::task<void>
{
+ // Acquire lock to guard against concurrent access to fdioInstance
+ sdbusplus::async::lock_guard lg{mutex};
+ co_await lg.lock();
+
MessageIntf request;
co_await fdioInstance.next();
auto ret = read(fd, request.raw.data(), request.raw.size());
@@ -94,6 +98,11 @@
}
}
+static inline void checkRequestSize(size_t requestSize, size_t expectedSize)
+{
+ EXPECT_EQ(requestSize, expectedSize) << "Invalid request size";
+}
+
void ServerTester::processReadHoldingRegisters(
MessageIntf& request, size_t requestSize, MessageIntf& response,
bool& segmentedResponse)
@@ -113,11 +122,11 @@
uint16_t registerOffset = request.raw[2] << 8 | request.raw[3];
uint16_t registerCount = request.raw[4] << 8 | request.raw[5];
- EXPECT_EQ(registerCount, testSuccessReadHoldingRegisterCount);
-
if (registerOffset == testSuccessReadHoldingRegisterOffset ||
registerOffset == testSuccessReadHoldingRegisterSegmentedOffset)
{
+ checkRequestSize(registerCount, testSuccessReadHoldingRegisterCount);
+
response << request.raw[0] << request.raw[1]
<< uint8_t(2 * registerCount)
<< uint16_t(testSuccessReadHoldingRegisterResponse[0])
@@ -126,8 +135,22 @@
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);
diff --git a/tests/modbus_server_tester.hpp b/tests/modbus_server_tester.hpp
index 79955a0..c9b4416 100644
--- a/tests/modbus_server_tester.hpp
+++ b/tests/modbus_server_tester.hpp
@@ -14,6 +14,7 @@
friend class ServerTester;
};
+// Read Holding Registers Testing Constants
static constexpr uint8_t testDeviceAddress = 0xa;
constexpr uint16_t testSuccessReadHoldingRegisterOffset = 0x0102;
constexpr uint16_t testSuccessReadHoldingRegisterCount = 0x2;
@@ -22,6 +23,14 @@
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};
+constexpr std::string testReadHoldingRegisterModelStr = "RDF040DSS519";
+
class ServerTester
{
public:
@@ -39,5 +48,6 @@
int fd;
sdbusplus::async::fdio fdioInstance;
+ sdbusplus::async::mutex mutex;
};
} // namespace phosphor::modbus::test
diff --git a/tests/test_inventory.cpp b/tests/test_inventory.cpp
new file mode 100644
index 0000000..b54b3a7
--- /dev/null
+++ b/tests/test_inventory.cpp
@@ -0,0 +1,171 @@
+#include "inventory/modbus_inventory.hpp"
+#include "modbus_server_tester.hpp"
+#include "port/base_port.hpp"
+
+#include <fcntl.h>
+
+#include <xyz/openbmc_project/Inventory/Source/Modbus/FRU/client.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace std::literals;
+using namespace testing;
+using InventorySourceIntf =
+ sdbusplus::client::xyz::openbmc_project::inventory::source::modbus::FRU<>;
+
+namespace TestIntf = phosphor::modbus::test;
+namespace ModbusIntf = phosphor::modbus::rtu;
+namespace PortIntf = phosphor::modbus::rtu::port;
+namespace PortConfigIntf = PortIntf::config;
+namespace InventoryIntf = phosphor::modbus::rtu::inventory;
+namespace InventoryConfigIntf = InventoryIntf::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 InventoryTest : public ::testing::Test
+{
+ public:
+ PortConfigIntf::Config portConfig;
+ static constexpr const char* clientDevicePath =
+ "/tmp/ttyInventoryTestPort0";
+ static constexpr const char* serverDevicePath =
+ "/tmp/ttyInventoryTestPort1";
+ static constexpr const auto defaultBaudeRate = "b115200";
+ static constexpr const auto deviceName = "Test1";
+ static constexpr auto serviceName = "xyz.openbmc_project.TestModbusRTU";
+ int socat_pid = -1;
+ sdbusplus::async::context ctx;
+ int fdClient = -1;
+ std::unique_ptr<TestIntf::ServerTester> serverTester;
+ int fdServer = -1;
+
+ InventoryTest()
+ {
+ portConfig.name = "TestPort1";
+ portConfig.portMode = PortConfigIntf::PortMode::rs485;
+ portConfig.baudRate = 115200;
+ portConfig.rtsDelay = 1;
+
+ std::string socatCmd = std::format(
+ "socat -x -v -d -d pty,link={},rawer,echo=0,parenb,{} pty,link={},rawer,echo=0,parenb,{} & echo $!",
+ serverDevicePath, defaultBaudeRate, clientDevicePath,
+ defaultBaudeRate);
+
+ // 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);
+ }
+
+ ~InventoryTest() noexcept override
+ {
+ if (fdClient != -1)
+ {
+ close(fdClient);
+ fdClient = -1;
+ }
+ if (fdServer != -1)
+ {
+ close(fdServer);
+ fdServer = -1;
+ }
+ kill(socat_pid, SIGTERM);
+ }
+
+ auto testInventorySourceCreation(std::string objPath)
+ -> sdbusplus::async::task<void>
+ {
+ InventoryConfigIntf::Config::port_address_map_t addressMap;
+ addressMap[portConfig.name] = {{.start = TestIntf::testDeviceAddress,
+ .end = TestIntf::testDeviceAddress}};
+ InventoryConfigIntf::Config deviceConfig = {
+ .name = deviceName,
+ .addressMap = addressMap,
+ .registers = {{"Model",
+ TestIntf::testReadHoldingRegisterModelOffset,
+ TestIntf::testReadHoldingRegisterModelCount}},
+ .parity = ModbusIntf::Parity::none,
+ .baudRate = 115200};
+ InventoryIntf::Device::serial_port_map_t ports;
+ ports[portConfig.name] =
+ std::make_unique<MockPort>(ctx, portConfig, clientDevicePath);
+
+ auto inventoryDevice =
+ std::make_unique<InventoryIntf::Device>(ctx, deviceConfig, ports);
+
+ co_await inventoryDevice->probePorts();
+
+ // Create InventorySource client interface to read back D-Bus properties
+ auto properties = co_await InventorySourceIntf(ctx)
+ .service(serviceName)
+ .path(objPath)
+ .properties();
+
+ constexpr auto defaultInventoryValue = "Unknown";
+
+ EXPECT_EQ(properties.name,
+ std::format("{} {} {}", deviceName,
+ TestIntf::testDeviceAddress, portConfig.name))
+ << "Name mismatch";
+ EXPECT_EQ(properties.address, TestIntf::testDeviceAddress)
+ << "Address mismatch";
+ EXPECT_EQ(properties.link_tty, portConfig.name) << "Link TTY mismatch";
+ EXPECT_EQ(properties.model, TestIntf::testReadHoldingRegisterModelStr)
+ << "Model mismatch";
+ EXPECT_EQ(properties.serial_number, defaultInventoryValue)
+ << "Part Number mismatch";
+
+ co_return;
+ }
+
+ void SetUp() override
+ {
+ // Process request for probe device call
+ ctx.spawn(serverTester->processRequests());
+
+ // Process request to read `Model` holding register call
+ ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+ sdbusplus::async::execution::then([&]() {
+ ctx.spawn(serverTester->processRequests());
+ }));
+ }
+};
+
+TEST_F(InventoryTest, TestAddInventorySource)
+{
+ auto objPath =
+ std::format("{}/{}_{}_{}", InventorySourceIntf::namespace_path,
+ deviceName, TestIntf::testDeviceAddress, portConfig.name);
+
+ ctx.spawn(testInventorySourceCreation(objPath));
+
+ ctx.spawn(sdbusplus::async::sleep_for(ctx, 1s) |
+ sdbusplus::async::execution::then([&]() { ctx.request_stop(); }));
+
+ ctx.run();
+}