| #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 <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 <iostream> |
| #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 constexpr bool debug = false; |
| |
| 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) |
| { |
| if constexpr (debug) |
| { |
| std::cerr << "Next ExternalSensor timer idle\n"; |
| } |
| |
| 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) |
| { |
| std::cerr << "ExternalSensor timer scheduling problem: " |
| << err.message() << "\n"; |
| } |
| return; |
| } |
| |
| updateReaper(sensors, timer, std::chrono::steady_clock::now()); |
| }); |
| |
| if constexpr (debug) |
| { |
| std::cerr << "Next ExternalSensor timer " |
| << std::chrono::duration_cast<std::chrono::microseconds>( |
| nextCheck) |
| .count() |
| << " us\n"; |
| } |
| } |
| |
| 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) |
| { |
| if constexpr (debug) |
| { |
| std::cerr << "ExternalSensor considering creating sensors\n"; |
| } |
| |
| 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()) |
| { |
| std::cerr << "Base configuration not found for " |
| << interfacePath << "\n"; |
| 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()) |
| { |
| std::cerr << "MinValue parameter not found for " |
| << interfacePath << "\n"; |
| continue; |
| } |
| double minValue = |
| std::visit(VariantToDoubleVisitor(), minFound->second); |
| if (!std::isfinite(minValue)) |
| { |
| std::cerr << "MinValue parameter not parsed for " |
| << interfacePath << "\n"; |
| continue; |
| } |
| |
| auto maxFound = baseConfigMap.find("MaxValue"); |
| if (maxFound == baseConfigMap.end()) |
| { |
| std::cerr << "MaxValue parameter not found for " |
| << interfacePath << "\n"; |
| continue; |
| } |
| double maxValue = |
| std::visit(VariantToDoubleVisitor(), maxFound->second); |
| if (!std::isfinite(maxValue)) |
| { |
| std::cerr << "MaxValue parameter not parsed for " |
| << interfacePath << "\n"; |
| 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)) |
| { |
| std::cerr << "Timeout parameter not parsed for " |
| << interfacePath << "\n"; |
| continue; |
| } |
| |
| std::string sensorName; |
| std::string sensorUnits; |
| |
| // Name and Units are mandatory string parameters |
| auto nameFound = baseConfigMap.find("Name"); |
| if (nameFound == baseConfigMap.end()) |
| { |
| std::cerr << "Name parameter not found for " |
| << interfacePath << "\n"; |
| continue; |
| } |
| sensorName = |
| std::visit(VariantToStringVisitor(), nameFound->second); |
| if (sensorName.empty()) |
| { |
| std::cerr << "Name parameter not parsed for " |
| << interfacePath << "\n"; |
| continue; |
| } |
| |
| auto unitsFound = baseConfigMap.find("Units"); |
| if (unitsFound == baseConfigMap.end()) |
| { |
| std::cerr << "Units parameter not found for " |
| << interfacePath << "\n"; |
| continue; |
| } |
| sensorUnits = |
| std::visit(VariantToStringVisitor(), unitsFound->second); |
| if (sensorUnits.empty()) |
| { |
| std::cerr << "Units parameter not parsed for " |
| << interfacePath << "\n"; |
| 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) |
| { |
| std::cerr << "ExternalSensor " << sensorName |
| << " change found\n"; |
| } |
| break; |
| } |
| } |
| if (!found) |
| { |
| continue; |
| } |
| } |
| |
| std::vector<thresholds::Threshold> sensorThresholds; |
| if (!parseThresholdsFromConfig(sensorData, sensorThresholds)) |
| { |
| std::cerr << "error populating thresholds for " |
| << sensorName << "\n"; |
| } |
| |
| 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); |
| }); |
| |
| if constexpr (debug) |
| { |
| std::cerr |
| << "ExternalSensor " << sensorName << " created\n"; |
| } |
| } |
| }); |
| |
| getter->getConfiguration(std::vector<std::string>{sensorType}); |
| } |
| |
| int main() |
| { |
| if constexpr (debug) |
| { |
| std::cerr << "ExternalSensor service starting up\n"; |
| } |
| |
| 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()) |
| { |
| std::cerr << "callback method error\n"; |
| return; |
| } |
| |
| const auto* messagePath = message.get_path(); |
| sensorsChanged->insert(messagePath); |
| if constexpr (debug) |
| { |
| std::cerr << "ExternalSensor change event received: " |
| << messagePath << "\n"; |
| } |
| |
| // 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) |
| { |
| std::cerr |
| << "callback error: " << ec.message() << "\n"; |
| } |
| 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); |
| |
| if constexpr (debug) |
| { |
| std::cerr << "ExternalSensor service entering main loop\n"; |
| } |
| |
| io.run(); |
| } |