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. 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. 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 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 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 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 237 Device::Device(sdbusplus::async::context& ctx, const config::Config& config, 238 serial_port_map_t& serialPorts) : 239 ctx(ctx), config(config), 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 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 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 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 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 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