/* // Copyright (c) 2018 Intel Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. */ #include "ExitAirTempSensor.hpp" #include "Utils.hpp" #include "VariantVisitors.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include constexpr const double altitudeFactor = 1.14; constexpr const char* exitAirType = "ExitAirTempSensor"; constexpr const char* cfmType = "CFMSensor"; // todo: this *might* need to be configurable constexpr const char* inletTemperatureSensor = "temperature/Front_Panel_Temp"; constexpr const char* pidConfigurationType = "xyz.openbmc_project.Configuration.Pid"; constexpr const char* settingsDaemon = "xyz.openbmc_project.Settings"; constexpr const char* cfmSettingPath = "/xyz/openbmc_project/control/cfm_limit"; constexpr const char* cfmSettingIface = "xyz.openbmc_project.Control.CFMLimit"; static constexpr bool debug = false; static constexpr double cfmMaxReading = 255; static constexpr double cfmMinReading = 0; static constexpr size_t minSystemCfm = 50; constexpr const auto monitorTypes{ std::to_array({exitAirType, cfmType})}; static std::vector> cfmSensors; static void setupSensorMatch( std::vector& matches, sdbusplus::bus_t& connection, const std::string& type, std::function&& callback) { std::function eventHandler = [callback{std::move(callback)}](sdbusplus::message_t& message) { std::string objectName; boost::container::flat_map> values; message.read(objectName, values); auto findValue = values.find("Value"); if (findValue == values.end()) { return; } double value = std::visit(VariantToDoubleVisitor(), findValue->second); if (std::isnan(value)) { return; } callback(value, message); }; matches.emplace_back(connection, "type='signal'," "member='PropertiesChanged',interface='org." "freedesktop.DBus.Properties',path_" "namespace='/xyz/openbmc_project/sensors/" + std::string(type) + "',arg0='xyz.openbmc_project.Sensor.Value'", std::move(eventHandler)); } static void setMaxPWM(const std::shared_ptr& conn, double value) { using GetSubTreeType = std::vector>>>>; conn->async_method_call( [conn, value](const boost::system::error_code ec, const GetSubTreeType& ret) { if (ec) { std::cerr << "Error calling mapper\n"; return; } for (const auto& [path, objDict] : ret) { if (objDict.empty()) { return; } const std::string& owner = objDict.begin()->first; conn->async_method_call( [conn, value, owner, path{path}](const boost::system::error_code ec, const std::variant& classType) { if (ec) { std::cerr << "Error getting pid class\n"; return; } const auto* classStr = std::get_if(&classType); if (classStr == nullptr || *classStr != "fan") { return; } conn->async_method_call( [](boost::system::error_code& ec) { if (ec) { std::cerr << "Error setting pid class\n"; return; } }, owner, path, "org.freedesktop.DBus.Properties", "Set", pidConfigurationType, "OutLimitMax", std::variant(value)); }, owner, path, "org.freedesktop.DBus.Properties", "Get", pidConfigurationType, "Class"); } }, mapper::busName, mapper::path, mapper::interface, mapper::subtree, "/", 0, std::array{pidConfigurationType}); } CFMSensor::CFMSensor(std::shared_ptr& conn, const std::string& sensorName, const std::string& sensorConfiguration, sdbusplus::asio::object_server& objectServer, std::vector&& thresholdData, std::shared_ptr& parent) : Sensor(escapeName(sensorName), std::move(thresholdData), sensorConfiguration, "CFMSensor", false, false, cfmMaxReading, cfmMinReading, conn, PowerState::on), parent(parent), objServer(objectServer) { sensorInterface = objectServer.add_interface( "/xyz/openbmc_project/sensors/airflow/" + name, "xyz.openbmc_project.Sensor.Value"); for (const auto& threshold : thresholds) { std::string interface = thresholds::getInterface(threshold.level); thresholdInterfaces[static_cast(threshold.level)] = objectServer.add_interface( "/xyz/openbmc_project/sensors/airflow/" + name, interface); } association = objectServer.add_interface( "/xyz/openbmc_project/sensors/airflow/" + name, association::interface); setInitialProperties(sensor_paths::unitCFM); pwmLimitIface = objectServer.add_interface("/xyz/openbmc_project/control/pwm_limit", "xyz.openbmc_project.Control.PWMLimit"); cfmLimitIface = objectServer.add_interface("/xyz/openbmc_project/control/MaxCFM", "xyz.openbmc_project.Control.CFMLimit"); } void CFMSensor::setupMatches() { std::weak_ptr weakRef = weak_from_this(); setupSensorMatch( matches, *dbusConnection, "fan_tach", [weakRef](const double& value, sdbusplus::message_t& message) { auto self = weakRef.lock(); if (!self) { return; } self->tachReadings[message.get_path()] = value; if (self->tachRanges.find(message.get_path()) == self->tachRanges.end()) { // calls update reading after updating ranges self->addTachRanges(message.get_sender(), message.get_path()); } else { self->updateReading(); } }); dbusConnection->async_method_call( [weakRef](const boost::system::error_code ec, const std::variant cfmVariant) { auto self = weakRef.lock(); if (!self) { return; } uint64_t maxRpm = 100; if (!ec) { const auto* cfm = std::get_if(&cfmVariant); if (cfm != nullptr && *cfm >= minSystemCfm) { maxRpm = self->getMaxRpm(*cfm); } } self->pwmLimitIface->register_property("Limit", maxRpm); self->pwmLimitIface->initialize(); setMaxPWM(self->dbusConnection, maxRpm); }, settingsDaemon, cfmSettingPath, "org.freedesktop.DBus.Properties", "Get", cfmSettingIface, "Limit"); matches.emplace_back(*dbusConnection, "type='signal'," "member='PropertiesChanged',interface='org." "freedesktop.DBus.Properties',path='" + std::string(cfmSettingPath) + "',arg0='" + std::string(cfmSettingIface) + "'", [weakRef](sdbusplus::message_t& message) { auto self = weakRef.lock(); if (!self) { return; } boost::container::flat_map> values; std::string objectName; message.read(objectName, values); const auto findValue = values.find("Limit"); if (findValue == values.end()) { return; } auto* const reading = std::get_if(&(findValue->second)); if (reading == nullptr) { std::cerr << "Got CFM Limit of wrong type\n"; return; } if (*reading < minSystemCfm && *reading != 0) { std::cerr << "Illegal CFM setting detected\n"; return; } uint64_t maxRpm = self->getMaxRpm(*reading); self->pwmLimitIface->set_property("Limit", maxRpm); setMaxPWM(self->dbusConnection, maxRpm); }); } CFMSensor::~CFMSensor() { for (const auto& iface : thresholdInterfaces) { objServer.remove_interface(iface); } objServer.remove_interface(sensorInterface); objServer.remove_interface(association); objServer.remove_interface(cfmLimitIface); objServer.remove_interface(pwmLimitIface); } void CFMSensor::createMaxCFMIface(void) { cfmLimitIface->register_property("Limit", c2 * maxCFM * tachs.size()); cfmLimitIface->initialize(); } void CFMSensor::addTachRanges(const std::string& serviceName, const std::string& path) { std::weak_ptr weakRef = weak_from_this(); dbusConnection->async_method_call( [weakRef, path](const boost::system::error_code ec, const SensorBaseConfigMap& data) { if (ec) { std::cerr << "Error getting properties from " << path << "\n"; return; } auto self = weakRef.lock(); if (!self) { return; } double max = loadVariant(data, "MaxValue"); double min = loadVariant(data, "MinValue"); self->tachRanges[path] = std::make_pair(min, max); self->updateReading(); }, serviceName, path, "org.freedesktop.DBus.Properties", "GetAll", "xyz.openbmc_project.Sensor.Value"); } void CFMSensor::checkThresholds(void) { thresholds::checkThresholds(this); } void CFMSensor::updateReading(void) { double val = 0.0; if (calculate(val)) { if (value != val && parent) { parent->updateReading(); } updateValue(val); } else { updateValue(std::numeric_limits::quiet_NaN()); } } uint64_t CFMSensor::getMaxRpm(uint64_t cfmMaxSetting) const { uint64_t pwmPercent = 100; double totalCFM = std::numeric_limits::max(); if (cfmMaxSetting == 0) { return pwmPercent; } bool firstLoop = true; while (totalCFM > cfmMaxSetting) { if (firstLoop) { firstLoop = false; } else { pwmPercent--; } double ci = 0; if (pwmPercent == 0) { ci = 0; } else if (pwmPercent < tachMinPercent) { ci = c1; } else if (pwmPercent > tachMaxPercent) { ci = c2; } else { ci = c1 + (((c2 - c1) * (pwmPercent - tachMinPercent)) / (tachMaxPercent - tachMinPercent)); } // Now calculate the CFM for this tach // CFMi = Ci * Qmaxi * TACHi totalCFM = ci * maxCFM * pwmPercent; totalCFM *= tachs.size(); // divide by 100 since pwm is in percent totalCFM /= 100; if (pwmPercent <= 0) { break; } } return pwmPercent; } bool CFMSensor::calculate(double& value) { double totalCFM = 0; for (const std::string& tachName : tachs) { auto findReading = std::find_if( tachReadings.begin(), tachReadings.end(), [&](const auto& item) { return item.first.ends_with(tachName); }); auto findRange = std::find_if( tachRanges.begin(), tachRanges.end(), [&](const auto& item) { return item.first.ends_with(tachName); }); if (findReading == tachReadings.end()) { if constexpr (debug) { std::cerr << "Can't find " << tachName << "in readings\n"; } continue; // haven't gotten a reading } if (findRange == tachRanges.end()) { std::cerr << "Can't find " << tachName << " in ranges\n"; return false; // haven't gotten a max / min } // avoid divide by 0 if (findRange->second.second == 0) { std::cerr << "Tach Max Set to 0 " << tachName << "\n"; return false; } double rpm = findReading->second; // for now assume the min for a fan is always 0, divide by max to get // percent and mult by 100 rpm /= findRange->second.second; rpm *= 100; if constexpr (debug) { std::cout << "Tach " << tachName << "at " << rpm << "\n"; } // Do a linear interpolation to get Ci // Ci = C1 + (C2 - C1)/(RPM2 - RPM1) * (TACHi - TACH1) double ci = 0; if (rpm == 0) { ci = 0; } else if (rpm < tachMinPercent) { ci = c1; } else if (rpm > tachMaxPercent) { ci = c2; } else { ci = c1 + (((c2 - c1) * (rpm - tachMinPercent)) / (tachMaxPercent - tachMinPercent)); } // Now calculate the CFM for this tach // CFMi = Ci * Qmaxi * TACHi totalCFM += ci * maxCFM * rpm; if constexpr (debug) { std::cerr << "totalCFM = " << totalCFM << "\n"; std::cerr << "Ci " << ci << " MaxCFM " << maxCFM << " rpm " << rpm << "\n"; std::cerr << "c1 " << c1 << " c2 " << c2 << " max " << tachMaxPercent << " min " << tachMinPercent << "\n"; } } // divide by 100 since rpm is in percent value = totalCFM / 100; if constexpr (debug) { std::cerr << "cfm value = " << value << "\n"; } return true; } static constexpr double exitAirMaxReading = 127; static constexpr double exitAirMinReading = -128; ExitAirTempSensor::ExitAirTempSensor( std::shared_ptr& conn, const std::string& sensorName, const std::string& sensorConfiguration, sdbusplus::asio::object_server& objectServer, std::vector&& thresholdData) : Sensor(escapeName(sensorName), std::move(thresholdData), sensorConfiguration, "ExitAirTemp", false, false, exitAirMaxReading, exitAirMinReading, conn, PowerState::on), objServer(objectServer) { sensorInterface = objectServer.add_interface( "/xyz/openbmc_project/sensors/temperature/" + name, "xyz.openbmc_project.Sensor.Value"); for (const auto& threshold : thresholds) { std::string interface = thresholds::getInterface(threshold.level); thresholdInterfaces[static_cast(threshold.level)] = objectServer.add_interface( "/xyz/openbmc_project/sensors/temperature/" + name, interface); } association = objectServer.add_interface( "/xyz/openbmc_project/sensors/temperature/" + name, association::interface); setInitialProperties(sensor_paths::unitDegreesC); } ExitAirTempSensor::~ExitAirTempSensor() { for (const auto& iface : thresholdInterfaces) { objServer.remove_interface(iface); } objServer.remove_interface(sensorInterface); objServer.remove_interface(association); } void ExitAirTempSensor::setupMatches(void) { constexpr const auto matchTypes{ std::to_array({"power", inletTemperatureSensor})}; std::weak_ptr weakRef = weak_from_this(); for (const std::string type : matchTypes) { setupSensorMatch(matches, *dbusConnection, type, [weakRef, type](const double& value, sdbusplus::message_t& message) { auto self = weakRef.lock(); if (!self) { return; } if (type == "power") { std::string path = message.get_path(); if (path.find("PS") != std::string::npos && path.ends_with("Input_Power")) { self->powerReadings[message.get_path()] = value; } } else if (type == inletTemperatureSensor) { self->inletTemp = value; } self->updateReading(); }); } dbusConnection->async_method_call( [weakRef](boost::system::error_code ec, const std::variant& value) { if (ec) { // sensor not ready yet return; } auto self = weakRef.lock(); if (!self) { return; } self->inletTemp = std::visit(VariantToDoubleVisitor(), value); }, "xyz.openbmc_project.HwmonTempSensor", std::string("/xyz/openbmc_project/sensors/") + inletTemperatureSensor, properties::interface, properties::get, sensorValueInterface, "Value"); dbusConnection->async_method_call( [weakRef](boost::system::error_code ec, const GetSubTreeType& subtree) { if (ec) { std::cerr << "Error contacting mapper\n"; return; } auto self = weakRef.lock(); if (!self) { return; } for (const auto& [path, matches] : subtree) { size_t lastSlash = path.rfind('/'); if (lastSlash == std::string::npos || lastSlash == path.size() || matches.empty()) { continue; } std::string sensorName = path.substr(lastSlash + 1); if (sensorName.starts_with("PS") && sensorName.ends_with("Input_Power")) { // lambda capture requires a proper variable (not a structured // binding) const std::string& cbPath = path; self->dbusConnection->async_method_call( [weakRef, cbPath](boost::system::error_code ec, const std::variant& value) { if (ec) { std::cerr << "Error getting value from " << cbPath << "\n"; } auto self = weakRef.lock(); if (!self) { return; } double reading = std::visit(VariantToDoubleVisitor(), value); if constexpr (debug) { std::cerr << cbPath << "Reading " << reading << "\n"; } self->powerReadings[cbPath] = reading; }, matches[0].first, cbPath, properties::interface, properties::get, sensorValueInterface, "Value"); } } }, mapper::busName, mapper::path, mapper::interface, mapper::subtree, "/xyz/openbmc_project/sensors/power", 0, std::array{sensorValueInterface}); } void ExitAirTempSensor::updateReading(void) { double val = 0.0; if (calculate(val)) { val = std::floor(val + 0.5); updateValue(val); } else { updateValue(std::numeric_limits::quiet_NaN()); } } double ExitAirTempSensor::getTotalCFM(void) { double sum = 0; for (auto& sensor : cfmSensors) { double reading = 0; if (!sensor->calculate(reading)) { return -1; } sum += reading; } return sum; } bool ExitAirTempSensor::calculate(double& val) { constexpr size_t maxErrorPrint = 5; static bool firstRead = false; static size_t errorPrint = maxErrorPrint; double cfm = getTotalCFM(); if (cfm <= 0) { std::cerr << "Error getting cfm\n"; return false; } // Though cfm is not expected to be less than qMin normally, // it is not a hard limit for exit air temp calculation. // 50% qMin is chosen as a generic limit between providing // a valid derived exit air temp and reporting exit air temp not available. constexpr const double cfmLimitFactor = 0.5; if (cfm < (qMin * cfmLimitFactor)) { if (errorPrint > 0) { errorPrint--; std::cerr << "cfm " << cfm << " is too low, expected qMin " << qMin << "\n"; } val = 0; return false; } // if there is an error getting inlet temp, return error if (std::isnan(inletTemp)) { if (errorPrint > 0) { errorPrint--; std::cerr << "Cannot get inlet temp\n"; } val = 0; return false; } // if fans are off, just make the exit temp equal to inlet if (!isPowerOn()) { val = inletTemp; return true; } double totalPower = 0; for (const auto& [path, reading] : powerReadings) { if (std::isnan(reading)) { continue; } totalPower += reading; } // Calculate power correction factor // Ci = CL + (CH - CL)/(QMax - QMin) * (CFM - QMin) double powerFactor = 0.0; if (cfm <= qMin) { powerFactor = powerFactorMin; } else if (cfm >= qMax) { powerFactor = powerFactorMax; } else { powerFactor = powerFactorMin + ((powerFactorMax - powerFactorMin) / (qMax - qMin) * (cfm - qMin)); } totalPower *= powerFactor; totalPower += pOffset; if (totalPower == 0) { if (errorPrint > 0) { errorPrint--; std::cerr << "total power 0\n"; } val = 0; return false; } if constexpr (debug) { std::cout << "Power Factor " << powerFactor << "\n"; std::cout << "Inlet Temp " << inletTemp << "\n"; std::cout << "Total Power" << totalPower << "\n"; } // Calculate the exit air temp // Texit = Tfp + (1.76 * TotalPower / CFM * Faltitude) double reading = 1.76 * totalPower * altitudeFactor; reading /= cfm; reading += inletTemp; if constexpr (debug) { std::cout << "Reading 1: " << reading << "\n"; } // Now perform the exponential average // Calculate alpha based on SDR values and CFM // Ai = As + (Af - As)/(QMax - QMin) * (CFM - QMin) double alpha = 0.0; if (cfm < qMin) { alpha = alphaS; } else if (cfm >= qMax) { alpha = alphaF; } else { alpha = alphaS + ((alphaF - alphaS) * (cfm - qMin) / (qMax - qMin)); } auto time = std::chrono::steady_clock::now(); if (!firstRead) { firstRead = true; lastTime = time; lastReading = reading; } double alphaDT = std::chrono::duration_cast(time - lastTime) .count() * alpha; // cap at 1.0 or the below fails if (alphaDT > 1.0) { alphaDT = 1.0; } if constexpr (debug) { std::cout << "AlphaDT: " << alphaDT << "\n"; } reading = ((reading * alphaDT) + (lastReading * (1.0 - alphaDT))); if constexpr (debug) { std::cout << "Reading 2: " << reading << "\n"; } val = reading; lastReading = reading; lastTime = time; errorPrint = maxErrorPrint; return true; } void ExitAirTempSensor::checkThresholds(void) { thresholds::checkThresholds(this); } static void loadVariantPathArray(const SensorBaseConfigMap& data, const std::string& key, std::vector& resp) { auto it = data.find(key); if (it == data.end()) { std::cerr << "Configuration missing " << key << "\n"; throw std::invalid_argument("Key Missing"); } BasicVariantType copy = it->second; std::vector config = std::get>(copy); for (auto& str : config) { boost::replace_all(str, " ", "_"); } resp = std::move(config); } void createSensor(sdbusplus::asio::object_server& objectServer, std::shared_ptr& exitAirSensor, std::shared_ptr& dbusConnection) { if (!dbusConnection) { std::cerr << "Connection not created\n"; return; } auto getter = std::make_shared( dbusConnection, [&objectServer, &dbusConnection, &exitAirSensor](const ManagedObjectType& resp) { cfmSensors.clear(); for (const auto& [path, interfaces] : resp) { for (const auto& [intf, cfg] : interfaces) { if (intf == configInterfaceName(exitAirType)) { // thresholds should be under the same path std::vector sensorThresholds; parseThresholdsFromConfig(interfaces, sensorThresholds); std::string name = loadVariant(cfg, "Name"); exitAirSensor = nullptr; exitAirSensor = std::make_shared( dbusConnection, name, path.str, objectServer, std::move(sensorThresholds)); exitAirSensor->powerFactorMin = loadVariant(cfg, "PowerFactorMin"); exitAirSensor->powerFactorMax = loadVariant(cfg, "PowerFactorMax"); exitAirSensor->qMin = loadVariant(cfg, "QMin"); exitAirSensor->qMax = loadVariant(cfg, "QMax"); exitAirSensor->alphaS = loadVariant(cfg, "AlphaS"); exitAirSensor->alphaF = loadVariant(cfg, "AlphaF"); } else if (intf == configInterfaceName(cfmType)) { // thresholds should be under the same path std::vector sensorThresholds; parseThresholdsFromConfig(interfaces, sensorThresholds); std::string name = loadVariant(cfg, "Name"); auto sensor = std::make_shared( dbusConnection, name, path.str, objectServer, std::move(sensorThresholds), exitAirSensor); loadVariantPathArray(cfg, "Tachs", sensor->tachs); sensor->maxCFM = loadVariant(cfg, "MaxCFM"); // change these into percent upon getting the data sensor->c1 = loadVariant(cfg, "C1") / 100; sensor->c2 = loadVariant(cfg, "C2") / 100; sensor->tachMinPercent = loadVariant(cfg, "TachMinPercent"); sensor->tachMaxPercent = loadVariant(cfg, "TachMaxPercent"); sensor->createMaxCFMIface(); sensor->setupMatches(); cfmSensors.emplace_back(std::move(sensor)); } } } if (exitAirSensor) { exitAirSensor->setupMatches(); exitAirSensor->updateReading(); } }); getter->getConfiguration( std::vector(monitorTypes.begin(), monitorTypes.end())); } int main() { 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.ExitAirTempSensor"); std::shared_ptr sensor = nullptr; // wait until we find the config boost::asio::post(io, [&]() { createSensor(objectServer, sensor, systemBus); }); boost::asio::steady_timer configTimer(io); std::function eventHandler = [&](sdbusplus::message_t&) { configTimer.expires_after(std::chrono::seconds(1)); // create a timer because normally multiple properties change configTimer.async_wait([&](const boost::system::error_code& ec) { if (ec == boost::asio::error::operation_aborted) { return; // we're being canceled } createSensor(objectServer, sensor, systemBus); if (!sensor) { std::cout << "Configuration not detected\n"; } }); }; std::vector> matches = setupPropertiesChangedMatches(*systemBus, monitorTypes, eventHandler); setupManufacturingModeMatch(*systemBus); io.run(); return 0; }