1 #include "ExternalSensor.hpp" 2 #include "Thresholds.hpp" 3 #include "Utils.hpp" 4 #include "VariantVisitors.hpp" 5 6 #include <boost/asio/error.hpp> 7 #include <boost/asio/io_context.hpp> 8 #include <boost/asio/post.hpp> 9 #include <boost/asio/steady_timer.hpp> 10 #include <boost/container/flat_map.hpp> 11 #include <boost/container/flat_set.hpp> 12 #include <sdbusplus/asio/connection.hpp> 13 #include <sdbusplus/asio/object_server.hpp> 14 #include <sdbusplus/bus/match.hpp> 15 #include <sdbusplus/message.hpp> 16 #include <sdbusplus/message/native_types.hpp> 17 18 #include <algorithm> 19 #include <array> 20 #include <chrono> 21 #include <cmath> 22 #include <functional> 23 #include <iostream> 24 #include <memory> 25 #include <string> 26 #include <utility> 27 #include <variant> 28 #include <vector> 29 30 // Copied from HwmonTempSensor and inspired by 31 // https://gerrit.openbmc-project.xyz/c/openbmc/dbus-sensors/+/35476 32 33 // The ExternalSensor is a sensor whose value is intended to be writable 34 // by something external to the BMC, so that the host (or something else) 35 // can write to it, perhaps by using an IPMI or Redfish connection. 36 37 // Unlike most other sensors, an external sensor does not correspond 38 // to a hwmon file or any other kernel/hardware interface, 39 // so, after initialization, this module does not have much to do, 40 // but it handles reinitialization and thresholds, similar to the others. 41 // The main work of this module is to provide backing storage for a 42 // sensor that exists only virtually, and to provide an optional 43 // timeout service for detecting loss of timely updates. 44 45 // As there is no corresponding driver or hardware to support, 46 // all configuration of this sensor comes from the JSON parameters: 47 // MinValue, MaxValue, Timeout, PowerState, Units, Name 48 49 // The purpose of "Units" is to specify the physical characteristic 50 // the external sensor is measuring, because with an external sensor 51 // there is no other way to tell, and it will be used for the object path 52 // here: /xyz/openbmc_project/sensors/<Units>/<Name> 53 54 // For more information, see external-sensor.md design document: 55 // https://gerrit.openbmc-project.xyz/c/openbmc/docs/+/41452 56 // https://github.com/openbmc/docs/tree/master/designs/ 57 58 static constexpr bool debug = false; 59 60 static const char* sensorType = "ExternalSensor"; 61 62 void updateReaper(boost::container::flat_map< 63 std::string, std::shared_ptr<ExternalSensor>>& sensors, 64 boost::asio::steady_timer& timer, 65 const std::chrono::steady_clock::time_point& now) 66 { 67 // First pass, reap all stale sensors 68 for (const auto& [name, sensor] : sensors) 69 { 70 if (!sensor) 71 { 72 continue; 73 } 74 75 if (!sensor->isAliveAndPerishable()) 76 { 77 continue; 78 } 79 80 if (!sensor->isAliveAndFresh(now)) 81 { 82 // Mark sensor as dead, no longer alive 83 sensor->writeInvalidate(); 84 } 85 } 86 87 std::chrono::steady_clock::duration nextCheck; 88 bool needCheck = false; 89 90 // Second pass, determine timer interval to next check 91 for (const auto& [name, sensor] : sensors) 92 { 93 if (!sensor) 94 { 95 continue; 96 } 97 98 if (!sensor->isAliveAndPerishable()) 99 { 100 continue; 101 } 102 103 auto expiration = sensor->ageRemaining(now); 104 105 if (needCheck) 106 { 107 nextCheck = std::min(nextCheck, expiration); 108 } 109 else 110 { 111 // Initialization 112 nextCheck = expiration; 113 needCheck = true; 114 } 115 } 116 117 if (!needCheck) 118 { 119 if constexpr (debug) 120 { 121 std::cerr << "Next ExternalSensor timer idle\n"; 122 } 123 124 return; 125 } 126 127 timer.expires_at(now + nextCheck); 128 129 timer.async_wait([&sensors, &timer](const boost::system::error_code& err) { 130 if (err != boost::system::errc::success) 131 { 132 // Cancellation is normal, as timer is dynamically rescheduled 133 if (err != boost::asio::error::operation_aborted) 134 { 135 std::cerr << "ExternalSensor timer scheduling problem: " 136 << err.message() << "\n"; 137 } 138 return; 139 } 140 141 updateReaper(sensors, timer, std::chrono::steady_clock::now()); 142 }); 143 144 if constexpr (debug) 145 { 146 std::cerr << "Next ExternalSensor timer " 147 << std::chrono::duration_cast<std::chrono::microseconds>( 148 nextCheck) 149 .count() 150 << " us\n"; 151 } 152 } 153 154 void createSensors( 155 sdbusplus::asio::object_server& objectServer, 156 boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>& 157 sensors, 158 std::shared_ptr<sdbusplus::asio::connection>& dbusConnection, 159 const std::shared_ptr<boost::container::flat_set<std::string>>& 160 sensorsChanged, 161 boost::asio::steady_timer& reaperTimer) 162 { 163 if constexpr (debug) 164 { 165 std::cerr << "ExternalSensor considering creating sensors\n"; 166 } 167 168 auto getter = std::make_shared<GetSensorConfiguration>( 169 dbusConnection, 170 [&objectServer, &sensors, &dbusConnection, sensorsChanged, 171 &reaperTimer](const ManagedObjectType& sensorConfigurations) { 172 bool firstScan = (sensorsChanged == nullptr); 173 174 for (const std::pair<sdbusplus::message::object_path, SensorData>& 175 sensor : sensorConfigurations) 176 { 177 const std::string& interfacePath = sensor.first.str; 178 const SensorData& sensorData = sensor.second; 179 180 auto sensorBase = sensorData.find(configInterfaceName(sensorType)); 181 if (sensorBase == sensorData.end()) 182 { 183 std::cerr << "Base configuration not found for " 184 << interfacePath << "\n"; 185 continue; 186 } 187 188 const SensorBaseConfiguration& baseConfiguration = *sensorBase; 189 const SensorBaseConfigMap& baseConfigMap = baseConfiguration.second; 190 191 // MinValue and MinValue are mandatory numeric parameters 192 auto minFound = baseConfigMap.find("MinValue"); 193 if (minFound == baseConfigMap.end()) 194 { 195 std::cerr << "MinValue parameter not found for " 196 << interfacePath << "\n"; 197 continue; 198 } 199 double minValue = std::visit(VariantToDoubleVisitor(), 200 minFound->second); 201 if (!std::isfinite(minValue)) 202 { 203 std::cerr << "MinValue parameter not parsed for " 204 << interfacePath << "\n"; 205 continue; 206 } 207 208 auto maxFound = baseConfigMap.find("MaxValue"); 209 if (maxFound == baseConfigMap.end()) 210 { 211 std::cerr << "MaxValue parameter not found for " 212 << interfacePath << "\n"; 213 continue; 214 } 215 double maxValue = std::visit(VariantToDoubleVisitor(), 216 maxFound->second); 217 if (!std::isfinite(maxValue)) 218 { 219 std::cerr << "MaxValue parameter not parsed for " 220 << interfacePath << "\n"; 221 continue; 222 } 223 224 double timeoutSecs = 0.0; 225 226 // Timeout is an optional numeric parameter 227 auto timeoutFound = baseConfigMap.find("Timeout"); 228 if (timeoutFound != baseConfigMap.end()) 229 { 230 timeoutSecs = std::visit(VariantToDoubleVisitor(), 231 timeoutFound->second); 232 } 233 if (!std::isfinite(timeoutSecs) || (timeoutSecs < 0.0)) 234 { 235 std::cerr << "Timeout parameter not parsed for " 236 << interfacePath << "\n"; 237 continue; 238 } 239 240 std::string sensorName; 241 std::string sensorUnits; 242 243 // Name and Units are mandatory string parameters 244 auto nameFound = baseConfigMap.find("Name"); 245 if (nameFound == baseConfigMap.end()) 246 { 247 std::cerr << "Name parameter not found for " << interfacePath 248 << "\n"; 249 continue; 250 } 251 sensorName = std::visit(VariantToStringVisitor(), 252 nameFound->second); 253 if (sensorName.empty()) 254 { 255 std::cerr << "Name parameter not parsed for " << interfacePath 256 << "\n"; 257 continue; 258 } 259 260 auto unitsFound = baseConfigMap.find("Units"); 261 if (unitsFound == baseConfigMap.end()) 262 { 263 std::cerr << "Units parameter not found for " << interfacePath 264 << "\n"; 265 continue; 266 } 267 sensorUnits = std::visit(VariantToStringVisitor(), 268 unitsFound->second); 269 if (sensorUnits.empty()) 270 { 271 std::cerr << "Units parameter not parsed for " << interfacePath 272 << "\n"; 273 continue; 274 } 275 276 // on rescans, only update sensors we were signaled by 277 auto findSensor = sensors.find(sensorName); 278 if (!firstScan && (findSensor != sensors.end())) 279 { 280 std::string suffixName = "/"; 281 suffixName += findSensor->second->name; 282 bool found = false; 283 for (auto it = sensorsChanged->begin(); 284 it != sensorsChanged->end(); it++) 285 { 286 std::string suffixIt = "/"; 287 suffixIt += *it; 288 if (suffixIt.ends_with(suffixName)) 289 { 290 sensorsChanged->erase(it); 291 findSensor->second = nullptr; 292 found = true; 293 if constexpr (debug) 294 { 295 std::cerr << "ExternalSensor " << sensorName 296 << " change found\n"; 297 } 298 break; 299 } 300 } 301 if (!found) 302 { 303 continue; 304 } 305 } 306 307 std::vector<thresholds::Threshold> sensorThresholds; 308 if (!parseThresholdsFromConfig(sensorData, sensorThresholds)) 309 { 310 std::cerr << "error populating thresholds for " << sensorName 311 << "\n"; 312 } 313 314 PowerState readState = getPowerState(baseConfigMap); 315 316 auto& sensorEntry = sensors[sensorName]; 317 sensorEntry = nullptr; 318 319 sensorEntry = std::make_shared<ExternalSensor>( 320 sensorType, objectServer, dbusConnection, sensorName, 321 sensorUnits, std::move(sensorThresholds), interfacePath, 322 maxValue, minValue, timeoutSecs, readState); 323 sensorEntry->initWriteHook( 324 [&sensors, &reaperTimer]( 325 const std::chrono::steady_clock::time_point& now) { 326 updateReaper(sensors, reaperTimer, now); 327 }); 328 329 if constexpr (debug) 330 { 331 std::cerr << "ExternalSensor " << sensorName << " created\n"; 332 } 333 } 334 }); 335 336 getter->getConfiguration(std::vector<std::string>{sensorType}); 337 } 338 339 int main() 340 { 341 if constexpr (debug) 342 { 343 std::cerr << "ExternalSensor service starting up\n"; 344 } 345 346 boost::asio::io_context io; 347 auto systemBus = std::make_shared<sdbusplus::asio::connection>(io); 348 sdbusplus::asio::object_server objectServer(systemBus, true); 349 350 objectServer.add_manager("/xyz/openbmc_project/sensors"); 351 systemBus->request_name("xyz.openbmc_project.ExternalSensor"); 352 353 boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>> 354 sensors; 355 auto sensorsChanged = 356 std::make_shared<boost::container::flat_set<std::string>>(); 357 boost::asio::steady_timer reaperTimer(io); 358 359 boost::asio::post(io, 360 [&objectServer, &sensors, &systemBus, &reaperTimer]() { 361 createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer); 362 }); 363 364 boost::asio::steady_timer filterTimer(io); 365 std::function<void(sdbusplus::message_t&)> eventHandler = 366 [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer, 367 &reaperTimer](sdbusplus::message_t& message) mutable { 368 if (message.is_method_error()) 369 { 370 std::cerr << "callback method error\n"; 371 return; 372 } 373 374 const auto* messagePath = message.get_path(); 375 sensorsChanged->insert(messagePath); 376 if constexpr (debug) 377 { 378 std::cerr << "ExternalSensor change event received: " << messagePath 379 << "\n"; 380 } 381 382 // this implicitly cancels the timer 383 filterTimer.expires_after(std::chrono::seconds(1)); 384 385 filterTimer.async_wait( 386 [&objectServer, &sensors, &systemBus, &sensorsChanged, 387 &reaperTimer](const boost::system::error_code& ec) mutable { 388 if (ec != boost::system::errc::success) 389 { 390 if (ec != boost::asio::error::operation_aborted) 391 { 392 std::cerr << "callback error: " << ec.message() << "\n"; 393 } 394 return; 395 } 396 397 createSensors(objectServer, sensors, systemBus, sensorsChanged, 398 reaperTimer); 399 }); 400 }; 401 402 std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches = 403 setupPropertiesChangedMatches( 404 *systemBus, std::to_array<const char*>({sensorType}), eventHandler); 405 406 if constexpr (debug) 407 { 408 std::cerr << "ExternalSensor service entering main loop\n"; 409 } 410 411 io.run(); 412 } 413