blob: 22afa2a570b595aee391c7407b7869f0ea24fddc [file] [log] [blame]
Jagpal Singh Gillcad9ecf2025-10-22 19:53:16 -07001#include "modbus_inventory.hpp"
2
3#include "common/entity_manager_interface.hpp"
4
5#include <phosphor-logging/lg2.hpp>
6#include <xyz/openbmc_project/Configuration/ModbusRTUDetect/client.hpp>
7#include <xyz/openbmc_project/Inventory/Item/client.hpp>
8
9#include <flat_map>
10
11namespace phosphor::modbus::rtu::inventory
12{
13PHOSPHOR_LOG2_USING;
14
15namespace config
16{
17
18using BasicVariantType =
19 std::variant<std::vector<std::string>, std::vector<uint8_t>, std::string,
20 int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
21 uint16_t, uint8_t, bool>;
22using InventoryBaseConfigMap = std::flat_map<std::string, BasicVariantType>;
23using InventoryData = std::flat_map<std::string, InventoryBaseConfigMap>;
24using ManagedObjectType =
25 std::flat_map<sdbusplus::message::object_path, InventoryData>;
26
27static constexpr std::array<std::pair<std::string_view, Parity>, 3>
28 validParities = {
29 {{"Odd", Parity::odd}, {"Even", Parity::even}, {"None", Parity::none}}};
30
31// TODO: This API will be dropped once EM supports non-indexed interfaces for
32// array objects.
33static auto processModbusAddressInterface(
34 Config& config, const InventoryBaseConfigMap& configMap) -> bool
35{
36 debug("Processing ModbusAddress {NAME}", "NAME", config.name);
37
38 auto rangeStartIter = configMap.find("RangeStart");
39 if (rangeStartIter == configMap.end())
40 {
41 error("Missing RangeStart for {NAME}", "NAME", config.name);
42 return false;
43 }
44 auto rangeStart = std::get<uint64_t>(rangeStartIter->second);
45
46 auto rangeEndIter = configMap.find("RangeEnd");
47 if (rangeEndIter == configMap.end())
48 {
49 error("Missing RangeEnd for {NAME}", "NAME", config.name);
50 return false;
51 }
52 auto rangeEnd = std::get<uint64_t>(rangeEndIter->second);
53
54 auto serialPortIter = configMap.find("SerialPort");
55 if (serialPortIter == configMap.end())
56 {
57 error("Missing SerialPort for {NAME}", "NAME", config.name);
58 return false;
59 }
60 auto serialPort = std::get<std::string>(serialPortIter->second);
61
62 config.addressMap[serialPort].push_back(AddressRange{
63 static_cast<uint8_t>(rangeStart), static_cast<uint8_t>(rangeEnd)});
64
65 debug("ModbusAddress {NAME} {PORT} {START} {END}", "NAME", config.name,
66 "PORT", serialPort, "START", rangeStart, "END", rangeEnd);
67
68 return true;
69}
70
71// TODO: This API will be dropped once EM supports non-indexed interfaces for
72// array objects.
73static auto processModbusRegistersInterface(
74 Config& config, const InventoryBaseConfigMap& configMap) -> bool
75{
76 debug("Processing ModbusRegisters {NAME}", "NAME", config.name);
77 Register registerConfig = {};
78
79 auto nameIter = configMap.find("Name");
80 if (nameIter == configMap.end())
81 {
82 error("Missing Name for {NAME}", "NAME", config.name);
83 return false;
84 }
85 registerConfig.name = std::get<std::string>(nameIter->second);
86
87 auto address = configMap.find("Address");
88 if (address == configMap.end())
89 {
90 error("Missing Address for {NAME}", "NAME", config.name);
91 return false;
92 }
93 registerConfig.offset = std::get<uint64_t>(address->second);
94
95 auto sizeIter = configMap.find("Size");
96 if (sizeIter == configMap.end())
97 {
98 error("Missing Size for {NAME}", "NAME", config.name);
99 return false;
100 }
101 registerConfig.size = std::get<uint64_t>(sizeIter->second);
102
103 config.registers.push_back(registerConfig);
104
105 debug("ModbusRegisters {NAME} {ADDRESS} {SIZE}", "NAME",
106 registerConfig.name, "ADDRESS", registerConfig.offset, "SIZE",
107 registerConfig.size);
108
109 return true;
110}
111
112static auto printConfig(const Config& config) -> void
113{
114 info("Inventory device config: {NAME} {BAUDRATE} {PARITY}", "NAME",
115 config.name, "BAUDRATE", config.baudRate, "PARITY", config.parity);
116
117 for (const auto& [port, addressRanges] : config.addressMap)
118 {
119 for (const auto& addressRange : addressRanges)
120 {
121 info(
122 "Inventory device config: {PORT} {ADDRESS_START} {ADDRESS_END}",
123 "PORT", port, "ADDRESS_START", addressRange.start,
124 "ADDRESS_END", addressRange.end);
125 }
126 }
127
128 for (const auto& registerConfig : config.registers)
129 {
130 info("Inventory device config: {NAME} {ADDRESS} {SIZE}", "NAME",
131 registerConfig.name, "ADDRESS", registerConfig.offset, "SIZE",
132 registerConfig.size);
133 }
134}
135
136auto getConfigSubInterfaces(sdbusplus::async::context& ctx,
137 sdbusplus::message::object_path objectPath,
138 Config& config) -> sdbusplus::async::task<bool>
139{
140 constexpr auto modbusAddressInterface =
141 "xyz.openbmc_project.Configuration.ModbusRTUDetect.Address";
142 constexpr auto modbusRegistersInterface =
143 "xyz.openbmc_project.Configuration.ModbusRTUDetect.Registers";
144
145 using InventoryIntf =
146 sdbusplus::client::xyz::openbmc_project::inventory::Item<>;
147
148 constexpr auto entityManager =
149 sdbusplus::async::proxy()
150 .service(entity_manager::EntityManagerInterface::serviceName)
151 .path(InventoryIntf::namespace_path)
152 .interface("org.freedesktop.DBus.ObjectManager");
153
154 for (const auto& [path, deviceConfig] :
155 co_await entityManager.call<ManagedObjectType>(ctx,
156 "GetManagedObjects"))
157 {
158 if (!(path.str).starts_with(objectPath.str))
159 {
160 debug("Skipping device {PATH}", "PATH", path.str);
161 continue;
162 }
163 debug("Processing device {PATH}", "PATH", path.str);
164 for (const auto& [interfaceName, interfaceConfig] : deviceConfig)
165 {
166 if (interfaceName.starts_with(modbusAddressInterface))
167 {
168 if (!processModbusAddressInterface(config, interfaceConfig))
169 {
170 error("Failed to process {INTERFACE} for {NAME}",
171 "INTERFACE", modbusAddressInterface, "NAME",
172 config.name);
173 co_return false;
174 }
175 }
176 else if (interfaceName.starts_with(modbusRegistersInterface))
177 {
178 if (!processModbusRegistersInterface(config, interfaceConfig))
179 {
180 error("Failed to process {INTERFACE} for {NAME}",
181 "INTERFACE", modbusRegistersInterface, "NAME",
182 config.name);
183 co_return false;
184 }
185 }
186 }
187 }
188
189 co_return true;
190}
191
192auto getConfig(sdbusplus::async::context& ctx,
193 sdbusplus::message::object_path objectPath)
194 -> sdbusplus::async::task<std::optional<Config>>
195{
196 using ModbusRTUDetectIntf = sdbusplus::client::xyz::openbmc_project::
197 configuration::ModbusRTUDetect<>;
198
199 Config config = {};
200
201 auto properties =
202 co_await ModbusRTUDetectIntf(ctx)
203 .service(entity_manager::EntityManagerInterface::serviceName)
204 .path(objectPath.str)
205 .properties();
206
207 config.name = properties.name;
208 config.baudRate = properties.baud_rate;
209
210 for (const auto& [parityStr, parity] : config::validParities)
211 {
212 if (parityStr == properties.data_parity)
213 {
214 config.parity = parity;
215 break;
216 }
217 }
218 if (config.parity == Parity::unknown)
219 {
220 error("Invalid parity {PARITY} for {NAME}", "PARITY",
221 properties.data_parity, "NAME", properties.name);
222 co_return std::nullopt;
223 }
224
225 if (!co_await getConfigSubInterfaces(ctx, objectPath, config))
226 {
227 co_return std::nullopt;
228 }
229
230 printConfig(config);
231
232 co_return config;
233}
234
235} // namespace config
236
237Device::Device(sdbusplus::async::context& ctx, const config::Config& config,
238 serial_port_map_t& serialPorts) :
Jagpal Singh Gille92aba42025-10-16 00:00:13 -0700239 config(config), ctx(ctx), serialPorts(serialPorts)
Jagpal Singh Gillcad9ecf2025-10-22 19:53:16 -0700240{
241 for (const auto& [serialPort, _] : config.addressMap)
242 {
243 if (serialPorts.find(serialPort) == serialPorts.end())
244 {
245 error("Serial port {PORT} not found for {NAME}", "PORT", serialPort,
246 "NAME", config.name);
247 continue;
248 }
249 }
250}
251
252auto Device::probePorts() -> sdbusplus::async::task<void>
253{
254 debug("Probing ports for {NAME}", "NAME", config.name);
255 while (!ctx.stop_requested())
256 {
257 for (const auto& [serialPort, _] : config.addressMap)
258 {
259 if (serialPorts.find(serialPort) == serialPorts.end())
260 {
261 continue;
262 }
263 ctx.spawn(probePort(serialPort));
264 }
265 constexpr auto probeInterval = 3;
266 co_await sdbusplus::async::sleep_for(
267 ctx, std::chrono::seconds(probeInterval));
268 debug("Probing ports for {NAME} in {INTERVAL} seconds", "NAME",
269 config.name, "INTERVAL", probeInterval);
270 }
271}
272
273auto Device::probePort(std::string portName) -> sdbusplus::async::task<void>
274{
275 debug("Probing port {PORT}", "PORT", portName);
276
277 auto portConfig = config.addressMap.find(portName);
278 if (portConfig == config.addressMap.end())
279 {
280 error("Serial port {PORT} address map not found for {NAME}", "PORT",
281 portName, "NAME", config.name);
282 co_return;
283 }
284 auto addressRanges = portConfig->second;
285
286 auto port = serialPorts.find(portName);
287 if (port == serialPorts.end())
288 {
289 error("Serial port {PORT} not found for {NAME}", "PORT", portName,
290 "NAME", config.name);
291 co_return;
292 }
293
294 for (const auto& addressRange : addressRanges)
295 {
296 for (auto address = addressRange.start; address <= addressRange.end;
297 address++)
298 {
299 co_await probeDevice(address, portName, *port->second);
300 }
301 }
302}
303
304auto Device::probeDevice(uint8_t address, const std::string& portName,
305 SerialPortIntf& port) -> sdbusplus::async::task<void>
306{
307 debug("Probing device at {ADDRESS} on port {PORT}", "ADDRESS", address,
308 "PORT", portName);
309
310 if (config.registers.size() == 0)
311 {
312 error("No registers configured for {NAME}", "NAME", config.name);
313 co_return;
314 }
315 auto probeRegister = config.registers[0].offset;
316 auto registers = std::vector<uint16_t>(config.registers[0].size);
317
318 auto sourceId = std::to_string(address) + "_" + portName;
319
320 auto ret = co_await port.readHoldingRegisters(
321 address, probeRegister, config.baudRate, config.parity, registers);
322 if (ret)
323 {
324 if (inventorySources.find(sourceId) == inventorySources.end())
325 {
326 debug("Device found at {ADDRESS}", "ADDRESS", address);
327 co_await addInventorySource(address, portName, port);
328 }
329 else
330 {
331 debug("Device already exists at {ADDRESS}", "ADDRESS", address);
332 }
333 }
334 else
335 {
336 if (inventorySources.find(sourceId) != inventorySources.end())
337 {
338 warning(
339 "Device removed at {ADDRESS} due to probe failure for {PROBE_REGISTER}",
340 "ADDRESS", address, "PROBE_REGISTER", probeRegister);
341 inventorySources[sourceId]->emit_removed();
342 inventorySources.erase(sourceId);
343 }
344 }
345}
346
347static auto fillInventorySourceProperties(
348 InventorySourceIntf::properties_t& properties, const std::string& regName,
349 std::string& strValue) -> void
350{
351 constexpr auto partNumber = "PartNumber";
352 constexpr auto sparePartNumber = "SparePartNumber";
353 constexpr auto serialNumber = "SerialNumber";
354 constexpr auto buildDate = "BuildDate";
355 constexpr auto model = "Model";
356 constexpr auto manufacturer = "Manufacturer";
357
358 if (regName == partNumber)
359 {
360 properties.part_number = strValue;
361 }
362 else if (regName == sparePartNumber)
363 {
364 properties.spare_part_number = strValue;
365 }
366 else if (regName == serialNumber)
367 {
368 properties.serial_number = strValue;
369 }
370 else if (regName == buildDate)
371 {
372 properties.build_date = strValue;
373 }
374 else if (regName == model)
375 {
376 properties.model = strValue;
377 }
378 else if (regName == manufacturer)
379 {
380 properties.manufacturer = strValue;
381 }
382}
383
384auto Device::addInventorySource(uint8_t address, const std::string& portName,
385 SerialPortIntf& port)
386 -> sdbusplus::async::task<void>
387{
388 InventorySourceIntf::properties_t properties;
389
390 for (const auto& reg : config.registers)
391 {
392 auto registers = std::vector<uint16_t>(reg.size);
393 auto ret = co_await port.readHoldingRegisters(
394 address, reg.offset, config.baudRate, config.parity, registers);
395 if (!ret)
396 {
397 error(
398 "Failed to read holding registers {NAME} for {DEVICE_ADDRESS}",
399 "NAME", reg.name, "DEVICE_ADDRESS", address);
400 continue;
401 }
402
403 std::string strValue = "";
404
405 // Reswap bytes in each register for string conversion
406 for (const auto& value : registers)
407 {
408 strValue += static_cast<char>((value >> 8) & 0xFF);
409 strValue += static_cast<char>(value & 0xFF);
410 }
411
412 fillInventorySourceProperties(properties, reg.name, strValue);
413 }
414
415 auto pathSuffix =
416 config.name + " " + std::to_string(address) + " " + portName;
417
418 properties.name = pathSuffix;
419 properties.address = address;
420 properties.link_tty = portName;
421
422 std::replace(pathSuffix.begin(), pathSuffix.end(), ' ', '_');
423
424 auto objectPath =
425 std::string(InventorySourceIntf::namespace_path) + "/" + pathSuffix;
426 auto sourceId = std::to_string(address) + "_" + portName;
427
428 inventorySources[sourceId] = std::make_unique<InventorySourceIntf>(
429 ctx, objectPath.c_str(), properties);
430 inventorySources[sourceId]->emit_added();
431
432 info("Added InventorySource at {PATH}", "PATH", objectPath);
433}
434
435} // namespace phosphor::modbus::rtu::inventory