blob: 9bd7a4df0dad7eb93119399d7ecab582a6e80c1e [file] [log] [blame]
#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();
}