#include "ExternalSensor.hpp" #include "Thresholds.hpp" #include "Utils.hpp" #include "VariantVisitors.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Copied from HwmonTempSensor and inspired by // https://gerrit.openbmc-project.xyz/c/openbmc/dbus-sensors/+/35476 // The ExternalSensor is a sensor whose value is intended to be writable // by something external to the BMC, so that the host (or something else) // can write to it, perhaps by using an IPMI or Redfish connection. // Unlike most other sensors, an external sensor does not correspond // to a hwmon file or any other kernel/hardware interface, // so, after initialization, this module does not have much to do, // but it handles reinitialization and thresholds, similar to the others. // The main work of this module is to provide backing storage for a // sensor that exists only virtually, and to provide an optional // timeout service for detecting loss of timely updates. // As there is no corresponding driver or hardware to support, // all configuration of this sensor comes from the JSON parameters: // MinValue, MaxValue, Timeout, PowerState, Units, Name // The purpose of "Units" is to specify the physical characteristic // the external sensor is measuring, because with an external sensor // there is no other way to tell, and it will be used for the object path // here: /xyz/openbmc_project/sensors// // For more information, see external-sensor.md design document: // https://gerrit.openbmc-project.xyz/c/openbmc/docs/+/41452 // https://github.com/openbmc/docs/tree/master/designs/ static constexpr bool debug = false; static const char* sensorType = "ExternalSensor"; void updateReaper( boost::container::flat_map>& sensors, boost::asio::steady_timer& timer, const std::chrono::steady_clock::time_point& now) { // First pass, reap all stale sensors for (const auto& [name, sensor] : sensors) { if (!sensor) { continue; } if (!sensor->isAliveAndPerishable()) { continue; } if (!sensor->isAliveAndFresh(now)) { // Mark sensor as dead, no longer alive sensor->writeInvalidate(); } } std::chrono::steady_clock::duration nextCheck; bool needCheck = false; // Second pass, determine timer interval to next check for (const auto& [name, sensor] : sensors) { if (!sensor) { continue; } if (!sensor->isAliveAndPerishable()) { continue; } auto expiration = sensor->ageRemaining(now); if (needCheck) { nextCheck = std::min(nextCheck, expiration); } else { // Initialization nextCheck = expiration; needCheck = true; } } if (!needCheck) { if constexpr (debug) { lg2::error("Next ExternalSensor timer idle"); } return; } timer.expires_at(now + nextCheck); timer.async_wait([&sensors, &timer](const boost::system::error_code& err) { if (err != boost::system::errc::success) { // Cancellation is normal, as timer is dynamically rescheduled if (err != boost::asio::error::operation_aborted) { lg2::error( "ExternalSensor timer scheduling problem: {ERROR_MESSAGE}", "ERROR_MESSAGE", err.message()); } return; } updateReaper(sensors, timer, std::chrono::steady_clock::now()); }); if constexpr (debug) { lg2::error( "Next ExternalSensor timer '{VALUE}' us", "VALUE", std::chrono::duration_cast(nextCheck) .count()); } } void createSensors( sdbusplus::asio::object_server& objectServer, boost::container::flat_map>& sensors, std::shared_ptr& dbusConnection, const std::shared_ptr>& sensorsChanged, boost::asio::steady_timer& reaperTimer) { if constexpr (debug) { lg2::error("ExternalSensor considering creating sensors"); } auto getter = std::make_shared( dbusConnection, [&objectServer, &sensors, &dbusConnection, sensorsChanged, &reaperTimer](const ManagedObjectType& sensorConfigurations) { bool firstScan = (sensorsChanged == nullptr); for (const std::pair& sensor : sensorConfigurations) { const std::string& interfacePath = sensor.first.str; const SensorData& sensorData = sensor.second; auto sensorBase = sensorData.find(configInterfaceName(sensorType)); if (sensorBase == sensorData.end()) { lg2::error("Base configuration not found for '{PATH}'", "PATH", interfacePath); continue; } const SensorBaseConfiguration& baseConfiguration = *sensorBase; const SensorBaseConfigMap& baseConfigMap = baseConfiguration.second; // MinValue and MinValue are mandatory numeric parameters auto minFound = baseConfigMap.find("MinValue"); if (minFound == baseConfigMap.end()) { lg2::error("MinValue parameter not found for '{PATH}'", "PATH", interfacePath); continue; } double minValue = std::visit(VariantToDoubleVisitor(), minFound->second); if (!std::isfinite(minValue)) { lg2::error("MinValue parameter not parsed for '{PATH}'", "PATH", interfacePath); continue; } auto maxFound = baseConfigMap.find("MaxValue"); if (maxFound == baseConfigMap.end()) { lg2::error("MaxValue parameter not found for '{PATH}'", "PATH", interfacePath); continue; } double maxValue = std::visit(VariantToDoubleVisitor(), maxFound->second); if (!std::isfinite(maxValue)) { lg2::error("MaxValue parameter not parsed for '{PATH}'", "PATH", interfacePath); continue; } double timeoutSecs = 0.0; // Timeout is an optional numeric parameter auto timeoutFound = baseConfigMap.find("Timeout"); if (timeoutFound != baseConfigMap.end()) { timeoutSecs = std::visit(VariantToDoubleVisitor(), timeoutFound->second); } if (!std::isfinite(timeoutSecs) || (timeoutSecs < 0.0)) { lg2::error("Timeout parameter not parsed for '{PATH}'", "PATH", interfacePath); continue; } std::string sensorName; std::string sensorUnits; // Name and Units are mandatory string parameters auto nameFound = baseConfigMap.find("Name"); if (nameFound == baseConfigMap.end()) { lg2::error("Name parameter not found for '{PATH}'", "PATH", interfacePath); continue; } sensorName = std::visit(VariantToStringVisitor(), nameFound->second); if (sensorName.empty()) { lg2::error("Name parameter not parsed for '{PATH}'", "PATH", interfacePath); continue; } auto unitsFound = baseConfigMap.find("Units"); if (unitsFound == baseConfigMap.end()) { lg2::error("Units parameter not found for '{PATH}'", "PATH", interfacePath); continue; } sensorUnits = std::visit(VariantToStringVisitor(), unitsFound->second); if (sensorUnits.empty()) { lg2::error("Units parameter not parsed for '{PATH}'", "PATH", interfacePath); continue; } // on rescans, only update sensors we were signaled by auto findSensor = sensors.find(sensorName); if (!firstScan && (findSensor != sensors.end())) { std::string suffixName = "/"; suffixName += findSensor->second->name; bool found = false; for (auto it = sensorsChanged->begin(); it != sensorsChanged->end(); it++) { std::string suffixIt = "/"; suffixIt += *it; if (suffixIt.ends_with(suffixName)) { sensorsChanged->erase(it); findSensor->second = nullptr; found = true; if constexpr (debug) { lg2::error( "ExternalSensor '{NAME}' change found", "NAME", sensorName); } break; } } if (!found) { continue; } } std::vector sensorThresholds; if (!parseThresholdsFromConfig(sensorData, sensorThresholds)) { lg2::error("error populating thresholds for '{NAME}'", "NAME", sensorName); } PowerState readState = getPowerState(baseConfigMap); auto& sensorEntry = sensors[sensorName]; sensorEntry = nullptr; sensorEntry = std::make_shared( sensorType, objectServer, dbusConnection, sensorName, sensorUnits, std::move(sensorThresholds), interfacePath, maxValue, minValue, timeoutSecs, readState); sensorEntry->initWriteHook( [&sensors, &reaperTimer]( const std::chrono::steady_clock::time_point& now) { updateReaper(sensors, reaperTimer, now); }); if constexpr (debug) { lg2::error("ExternalSensor '{NAME}' created", "NAME", sensorName); } } }); getter->getConfiguration(std::vector{sensorType}); } int main() { if constexpr (debug) { lg2::error("ExternalSensor service starting up"); } boost::asio::io_context io; auto systemBus = std::make_shared(io); sdbusplus::asio::object_server objectServer(systemBus, true); objectServer.add_manager("/xyz/openbmc_project/sensors"); systemBus->request_name("xyz.openbmc_project.ExternalSensor"); boost::container::flat_map> sensors; auto sensorsChanged = std::make_shared>(); boost::asio::steady_timer reaperTimer(io); boost::asio::post(io, [&objectServer, &sensors, &systemBus, &reaperTimer]() { createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer); }); boost::asio::steady_timer filterTimer(io); std::function eventHandler = [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer, &reaperTimer](sdbusplus::message_t& message) mutable { if (message.is_method_error()) { lg2::error("callback method error"); return; } const auto* messagePath = message.get_path(); sensorsChanged->insert(messagePath); if constexpr (debug) { lg2::error("ExternalSensor change event received: '{PATH}'", "PATH", messagePath); } // this implicitly cancels the timer filterTimer.expires_after(std::chrono::seconds(1)); filterTimer.async_wait( [&objectServer, &sensors, &systemBus, &sensorsChanged, &reaperTimer](const boost::system::error_code& ec) mutable { if (ec != boost::system::errc::success) { if (ec != boost::asio::error::operation_aborted) { lg2::error("callback error: '{ERROR_MESSAGE}'", "ERROR_MESSAGE", ec.message()); } return; } createSensors(objectServer, sensors, systemBus, sensorsChanged, reaperTimer); }); }; std::vector> matches = setupPropertiesChangedMatches( *systemBus, std::to_array({sensorType}), eventHandler); if constexpr (debug) { lg2::error("ExternalSensor service entering main loop"); } io.run(); }