1 #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
11 namespace phosphor::modbus::rtu::inventory
12 {
13 PHOSPHOR_LOG2_USING;
14
15 namespace config
16 {
17
18 using 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>;
22 using InventoryBaseConfigMap = std::flat_map<std::string, BasicVariantType>;
23 using InventoryData = std::flat_map<std::string, InventoryBaseConfigMap>;
24 using ManagedObjectType =
25 std::flat_map<sdbusplus::message::object_path, InventoryData>;
26
27 static 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.
processModbusAddressInterface(Config & config,const InventoryBaseConfigMap & configMap)33 static 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.
processModbusRegistersInterface(Config & config,const InventoryBaseConfigMap & configMap)73 static 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
printConfig(const Config & config)112 static 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
getConfigSubInterfaces(sdbusplus::async::context & ctx,sdbusplus::message::object_path objectPath,Config & config)136 auto 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
getConfig(sdbusplus::async::context & ctx,sdbusplus::message::object_path objectPath)192 auto 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
Device(sdbusplus::async::context & ctx,const config::Config & config,serial_port_map_t & serialPorts)237 Device::Device(sdbusplus::async::context& ctx, const config::Config& config,
238 serial_port_map_t& serialPorts) :
239 config(config), ctx(ctx), serialPorts(serialPorts)
240 {
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
probePorts()252 auto 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
probePort(std::string portName)273 auto 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
probeDevice(uint8_t address,const std::string & portName,SerialPortIntf & port)304 auto 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
fillInventorySourceProperties(InventorySourceIntf::properties_t & properties,const std::string & regName,std::string & strValue)347 static 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
addInventorySource(uint8_t address,const std::string & portName,SerialPortIntf & port)384 auto 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
436