|  | #include "ExternalSensor.hpp" | 
|  | #include "Thresholds.hpp" | 
|  | #include "Utils.hpp" | 
|  | #include "VariantVisitors.hpp" | 
|  |  | 
|  | #include <boost/asio/error.hpp> | 
|  | #include <boost/asio/io_context.hpp> | 
|  | #include <boost/asio/post.hpp> | 
|  | #include <boost/asio/steady_timer.hpp> | 
|  | #include <boost/container/flat_map.hpp> | 
|  | #include <boost/container/flat_set.hpp> | 
|  | #include <phosphor-logging/lg2.hpp> | 
|  | #include <sdbusplus/asio/connection.hpp> | 
|  | #include <sdbusplus/asio/object_server.hpp> | 
|  | #include <sdbusplus/bus/match.hpp> | 
|  | #include <sdbusplus/message.hpp> | 
|  | #include <sdbusplus/message/native_types.hpp> | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <array> | 
|  | #include <chrono> | 
|  | #include <cmath> | 
|  | #include <functional> | 
|  | #include <memory> | 
|  | #include <string> | 
|  | #include <utility> | 
|  | #include <variant> | 
|  | #include <vector> | 
|  |  | 
|  | // 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/<Units>/<Name> | 
|  |  | 
|  | // 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 const char* sensorType = "ExternalSensor"; | 
|  |  | 
|  | void updateReaper( | 
|  | boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>& | 
|  | 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) | 
|  | { | 
|  | lg2::debug("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()); | 
|  | }); | 
|  |  | 
|  | lg2::debug("Next ExternalSensor timer '{VALUE}' us", "VALUE", | 
|  | std::chrono::duration_cast<std::chrono::microseconds>(nextCheck) | 
|  | .count()); | 
|  | } | 
|  |  | 
|  | void createSensors( | 
|  | sdbusplus::asio::object_server& objectServer, | 
|  | boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>& | 
|  | sensors, | 
|  | std::shared_ptr<sdbusplus::asio::connection>& dbusConnection, | 
|  | const std::shared_ptr<boost::container::flat_set<std::string>>& | 
|  | sensorsChanged, | 
|  | boost::asio::steady_timer& reaperTimer) | 
|  | { | 
|  | lg2::debug("ExternalSensor considering creating sensors"); | 
|  |  | 
|  | auto getter = std::make_shared<GetSensorConfiguration>( | 
|  | dbusConnection, | 
|  | [&objectServer, &sensors, &dbusConnection, sensorsChanged, | 
|  | &reaperTimer](const ManagedObjectType& sensorConfigurations) { | 
|  | bool firstScan = (sensorsChanged == nullptr); | 
|  |  | 
|  | for (const std::pair<sdbusplus::message::object_path, SensorData>& | 
|  | 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; | 
|  | lg2::debug("ExternalSensor '{NAME}' change found", | 
|  | "NAME", sensorName); | 
|  | break; | 
|  | } | 
|  | } | 
|  | if (!found) | 
|  | { | 
|  | continue; | 
|  | } | 
|  | } | 
|  |  | 
|  | std::vector<thresholds::Threshold> 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<ExternalSensor>( | 
|  | 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); | 
|  | }); | 
|  |  | 
|  | lg2::debug("ExternalSensor '{NAME}' created", "NAME", | 
|  | sensorName); | 
|  | } | 
|  | }); | 
|  |  | 
|  | getter->getConfiguration(std::vector<std::string>{sensorType}); | 
|  | } | 
|  |  | 
|  | int main() | 
|  | { | 
|  | lg2::debug("ExternalSensor service starting up"); | 
|  |  | 
|  | boost::asio::io_context io; | 
|  | auto systemBus = std::make_shared<sdbusplus::asio::connection>(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<std::string, std::shared_ptr<ExternalSensor>> | 
|  | sensors; | 
|  | auto sensorsChanged = | 
|  | std::make_shared<boost::container::flat_set<std::string>>(); | 
|  | 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<void(sdbusplus::message_t&)> 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); | 
|  | lg2::debug("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<std::unique_ptr<sdbusplus::bus::match_t>> matches = | 
|  | setupPropertiesChangedMatches( | 
|  | *systemBus, std::to_array<const char*>({sensorType}), eventHandler); | 
|  |  | 
|  | lg2::debug("ExternalSensor service entering main loop"); | 
|  |  | 
|  | io.run(); | 
|  | } |