| /** | 
 |  * Copyright 2017 Google Inc. | 
 |  * | 
 |  * 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. | 
 |  */ | 
 |  | 
 | /* Configuration. */ | 
 | #include "zone.hpp" | 
 |  | 
 | #include "conf.hpp" | 
 | #include "failsafeloggers/failsafe_logger_utility.hpp" | 
 | #include "interfaces.hpp" | 
 | #include "pid/controller.hpp" | 
 | #include "pid/tuning.hpp" | 
 |  | 
 | #include <sdbusplus/bus.hpp> | 
 |  | 
 | #include <algorithm> | 
 | #include <chrono> | 
 | #include <cstdint> | 
 | #include <cstring> | 
 | #include <exception> | 
 | #include <fstream> | 
 | #include <iostream> | 
 | #include <limits> | 
 | #include <memory> | 
 | #include <sstream> | 
 | #include <string> | 
 | #include <string_view> | 
 | #include <utility> | 
 | #include <vector> | 
 |  | 
 | using tstamp = std::chrono::high_resolution_clock::time_point; | 
 | using namespace std::literals::chrono_literals; | 
 |  | 
 | // Enforces minimum duration between events | 
 | // Rreturns true if event should be allowed, false if disallowed | 
 | bool allowThrottle(const tstamp& now, const std::chrono::seconds& pace) | 
 | { | 
 |     static tstamp then; | 
 |     static bool first = true; | 
 |  | 
 |     if (first) | 
 |     { | 
 |         // Special case initialization | 
 |         then = now; | 
 |         first = false; | 
 |  | 
 |         // Initialization, always allow | 
 |         return true; | 
 |     } | 
 |  | 
 |     auto elapsed = now - then; | 
 |     if (elapsed < pace) | 
 |     { | 
 |         // Too soon since last time, disallow | 
 |         return false; | 
 |     } | 
 |  | 
 |     // It has been long enough, allow | 
 |     then = now; | 
 |     return true; | 
 | } | 
 |  | 
 | namespace pid_control | 
 | { | 
 |  | 
 | double DbusPidZone::getMaxSetPointRequest(void) const | 
 | { | 
 |     return _maximumSetPoint; | 
 | } | 
 |  | 
 | bool DbusPidZone::getManualMode(void) const | 
 | { | 
 |     return _manualMode; | 
 | } | 
 |  | 
 | void DbusPidZone::setManualMode(bool mode) | 
 | { | 
 |     _manualMode = mode; | 
 |  | 
 |     // If returning to automatic mode, need to restore PWM from PID loop | 
 |     if (!mode) | 
 |     { | 
 |         _redundantWrite = true; | 
 |     } | 
 | } | 
 |  | 
 | bool DbusPidZone::getFailSafeMode(void) const | 
 | { | 
 |     // If any keys are present at least one sensor is in fail safe mode. | 
 |     return !_failSafeSensors.empty(); | 
 | } | 
 |  | 
 | FailSafeSensorsMap DbusPidZone::getFailSafeSensors(void) const | 
 | { | 
 |     return _failSafeSensors; | 
 | } | 
 |  | 
 | void DbusPidZone::markSensorMissing(const std::string& name, | 
 |                                     const std::string& failReason) | 
 | { | 
 |     if (_missingAcceptable.find(name) != _missingAcceptable.end()) | 
 |     { | 
 |         // Disallow sensors in MissingIsAcceptable list from causing failsafe | 
 |         outputFailsafeLogWithZone(_zoneId, this->getFailSafeMode(), name, | 
 |                                   "The sensor is missing but is acceptable."); | 
 |         return; | 
 |     } | 
 |  | 
 |     if (_sensorFailSafePercent[name] == 0) | 
 |     { | 
 |         _failSafeSensors[name] = std::pair(failReason, _zoneFailSafePercent); | 
 |     } | 
 |     else | 
 |     { | 
 |         _failSafeSensors[name] = | 
 |             std::pair(failReason, _sensorFailSafePercent[name]); | 
 |     } | 
 |  | 
 |     if (debugEnabled) | 
 |     { | 
 |         std::cerr << "Sensor " << name << " marked missing\n"; | 
 |     } | 
 | } | 
 |  | 
 | int64_t DbusPidZone::getZoneID(void) const | 
 | { | 
 |     return _zoneId; | 
 | } | 
 |  | 
 | void DbusPidZone::addSetPoint(double setPoint, const std::string& name) | 
 | { | 
 |     /* exclude disabled pidloop from _maximumSetPoint calculation*/ | 
 |     if (!isPidProcessEnabled(name)) | 
 |     { | 
 |         return; | 
 |     } | 
 |  | 
 |     auto profileName = name; | 
 |     if (getAccSetPoint()) | 
 |     { | 
 |         /* | 
 |          * If the name of controller is Linear_Temp_CPU0. | 
 |          * The profile name will be Temp_CPU0. | 
 |          */ | 
 |         profileName = name.substr(name.find('_') + 1); | 
 |         setPoints[profileName] += setPoint; | 
 |     } | 
 |     else | 
 |     { | 
 |         if (setPoints[profileName] < setPoint) | 
 |         { | 
 |             setPoints[profileName] = setPoint; | 
 |         } | 
 |     } | 
 |  | 
 |     /* | 
 |      * if there are multiple thermal controllers with the same | 
 |      * value, pick the first one in the iterator | 
 |      */ | 
 |     if (_maximumSetPoint < setPoints[profileName]) | 
 |     { | 
 |         _maximumSetPoint = setPoints[profileName]; | 
 |         _maximumSetPointName = profileName; | 
 |     } | 
 | } | 
 |  | 
 | void DbusPidZone::addRPMCeiling(double ceiling) | 
 | { | 
 |     rpmCeilings.push_back(ceiling); | 
 | } | 
 |  | 
 | void DbusPidZone::clearRPMCeilings(void) | 
 | { | 
 |     rpmCeilings.clear(); | 
 | } | 
 |  | 
 | void DbusPidZone::clearSetPoints(void) | 
 | { | 
 |     setPoints.clear(); | 
 |     _maximumSetPoint = 0; | 
 |     _maximumSetPointName.clear(); | 
 | } | 
 |  | 
 | double DbusPidZone::getFailSafePercent(void) | 
 | { | 
 |     if (_failSafeSensors.empty()) | 
 |     { | 
 |         return _zoneFailSafePercent; | 
 |     } | 
 |  | 
 |     FailSafeSensorsMap::iterator maxData = std::max_element( | 
 |         _failSafeSensors.begin(), _failSafeSensors.end(), | 
 |         [](const FailSafeSensorPair& firstData, | 
 |            const FailSafeSensorPair& secondData) { | 
 |             return firstData.second.second < secondData.second.second; | 
 |         }); | 
 |  | 
 |     // In dbus/dbusconfiguration.cpp, the default sensor failsafepercent is 0 if | 
 |     // there is no setting in json. | 
 |     // Therfore, if the max failsafe duty in _failSafeSensors is 0, set final | 
 |     // failsafe duty to _zoneFailSafePercent. | 
 |     if ((*maxData).second.second == 0) | 
 |     { | 
 |         return _zoneFailSafePercent; | 
 |     } | 
 |  | 
 |     return (*maxData).second.second; | 
 | } | 
 |  | 
 | double DbusPidZone::getMinThermalSetPoint(void) const | 
 | { | 
 |     return _minThermalOutputSetPt; | 
 | } | 
 |  | 
 | uint64_t DbusPidZone::getCycleIntervalTime(void) const | 
 | { | 
 |     return _cycleTime.cycleIntervalTimeMS; | 
 | } | 
 |  | 
 | uint64_t DbusPidZone::getUpdateThermalsCycle(void) const | 
 | { | 
 |     return _cycleTime.updateThermalsTimeMS; | 
 | } | 
 |  | 
 | void DbusPidZone::addFanPID(std::unique_ptr<Controller> pid) | 
 | { | 
 |     _fans.push_back(std::move(pid)); | 
 | } | 
 |  | 
 | void DbusPidZone::addThermalPID(std::unique_ptr<Controller> pid) | 
 | { | 
 |     _thermals.push_back(std::move(pid)); | 
 | } | 
 |  | 
 | double DbusPidZone::getCachedValue(const std::string& name) | 
 | { | 
 |     return _cachedValuesByName.at(name).scaled; | 
 | } | 
 |  | 
 | ValueCacheEntry DbusPidZone::getCachedValues(const std::string& name) | 
 | { | 
 |     return _cachedValuesByName.at(name); | 
 | } | 
 |  | 
 | void DbusPidZone::setOutputCache(std::string_view name, | 
 |                                  const ValueCacheEntry& values) | 
 | { | 
 |     _cachedFanOutputs[std::string{name}] = values; | 
 | } | 
 |  | 
 | void DbusPidZone::addFanInput(const std::string& fan, bool missingAcceptable) | 
 | { | 
 |     _fanInputs.push_back(fan); | 
 |  | 
 |     if (missingAcceptable) | 
 |     { | 
 |         _missingAcceptable.emplace(fan); | 
 |     } | 
 | } | 
 |  | 
 | void DbusPidZone::addThermalInput(const std::string& therm, | 
 |                                   bool missingAcceptable) | 
 | { | 
 |     /* | 
 |      * One sensor may have stepwise and PID at the same time. | 
 |      * Searching the sensor name before inserting it to avoid duplicated sensor | 
 |      * names. | 
 |      */ | 
 |     if (std::find(_thermalInputs.begin(), _thermalInputs.end(), therm) == | 
 |         _thermalInputs.end()) | 
 |     { | 
 |         _thermalInputs.push_back(therm); | 
 |     } | 
 |  | 
 |     if (missingAcceptable) | 
 |     { | 
 |         _missingAcceptable.emplace(therm); | 
 |     } | 
 | } | 
 |  | 
 | // Updates desired RPM setpoint from optional text file | 
 | // Returns true if rpmValue updated, false if left unchanged | 
 | static bool fileParseRpm(const std::string& fileName, double& rpmValue) | 
 | { | 
 |     static constexpr std::chrono::seconds throttlePace{3}; | 
 |  | 
 |     std::string errText; | 
 |  | 
 |     try | 
 |     { | 
 |         std::ifstream ifs; | 
 |         ifs.open(fileName); | 
 |         if (ifs) | 
 |         { | 
 |             int value; | 
 |             ifs >> value; | 
 |  | 
 |             if (value <= 0) | 
 |             { | 
 |                 errText = "File content could not be parsed to a number"; | 
 |             } | 
 |             else if (value <= 100) | 
 |             { | 
 |                 errText = "File must contain RPM value, not PWM value"; | 
 |             } | 
 |             else | 
 |             { | 
 |                 rpmValue = static_cast<double>(value); | 
 |                 return true; | 
 |             } | 
 |         } | 
 |     } | 
 |     catch (const std::exception& e) | 
 |     { | 
 |         errText = "Exception: "; | 
 |         errText += e.what(); | 
 |     } | 
 |  | 
 |     // The file is optional, intentionally not an error if file not found | 
 |     if (!(errText.empty())) | 
 |     { | 
 |         tstamp now = std::chrono::high_resolution_clock::now(); | 
 |         if (allowThrottle(now, throttlePace)) | 
 |         { | 
 |             std::cerr << "Unable to read from '" << fileName << "': " << errText | 
 |                       << "\n"; | 
 |         } | 
 |     } | 
 |  | 
 |     return false; | 
 | } | 
 |  | 
 | void DbusPidZone::determineMaxSetPointRequest(void) | 
 | { | 
 |     std::vector<double>::iterator result; | 
 |     double minThermalThreshold = getMinThermalSetPoint(); | 
 |  | 
 |     if (rpmCeilings.size() > 0) | 
 |     { | 
 |         result = std::min_element(rpmCeilings.begin(), rpmCeilings.end()); | 
 |         // if Max set point is larger than the lowest ceiling, reset to lowest | 
 |         // ceiling. | 
 |         if (*result < _maximumSetPoint) | 
 |         { | 
 |             _maximumSetPoint = *result; | 
 |             // When using lowest ceiling, controller name is ceiling. | 
 |             _maximumSetPointName = "Ceiling"; | 
 |         } | 
 |     } | 
 |  | 
 |     /* | 
 |      * Combine the maximum SetPoint Name if the controllers have same profile | 
 |      * name. e.g., PID_BB_INLET_TEMP_C + Stepwise_BB_INLET_TEMP_C. | 
 |      */ | 
 |     if (getAccSetPoint()) | 
 |     { | 
 |         auto profileName = _maximumSetPointName; | 
 |         _maximumSetPointName = ""; | 
 |  | 
 |         for (auto& p : _thermals) | 
 |         { | 
 |             auto controllerID = p->getID(); | 
 |             auto found = controllerID.find(profileName); | 
 |             if (found != std::string::npos) | 
 |             { | 
 |                 if (_maximumSetPointName.empty()) | 
 |                 { | 
 |                     _maximumSetPointName = controllerID; | 
 |                 } | 
 |                 else | 
 |                 { | 
 |                     _maximumSetPointName += " + " + controllerID; | 
 |                 } | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     /* | 
 |      * If the maximum RPM setpoint output is below the minimum RPM | 
 |      * setpoint, set it to the minimum. | 
 |      */ | 
 |     if (minThermalThreshold >= _maximumSetPoint) | 
 |     { | 
 |         _maximumSetPoint = minThermalThreshold; | 
 |         _maximumSetPointName = "Minimum"; | 
 |     } | 
 |     else if (_maximumSetPointName.compare(_maximumSetPointNamePrev)) | 
 |     { | 
 |         std::cerr << "PID Zone " << _zoneId << " max SetPoint " | 
 |                   << _maximumSetPoint << " requested by " | 
 |                   << _maximumSetPointName; | 
 |         for (const auto& sensor : _failSafeSensors) | 
 |         { | 
 |             if (sensor.first.find("Fan") == std::string::npos) | 
 |             { | 
 |                 std::cerr << " " << sensor.first; | 
 |             } | 
 |         } | 
 |         std::cerr << "\n"; | 
 |         _maximumSetPointNamePrev.assign(_maximumSetPointName); | 
 |     } | 
 |     if (tuningEnabled) | 
 |     { | 
 |         /* | 
 |          * We received no setpoints from thermal sensors. | 
 |          * This is a case experienced during tuning where they only specify | 
 |          * fan sensors and one large fan PID for all the fans. | 
 |          */ | 
 |         static constexpr auto setpointpath = "/etc/thermal.d/setpoint"; | 
 |  | 
 |         fileParseRpm(setpointpath, _maximumSetPoint); | 
 |  | 
 |         // Allow per-zone setpoint files to override overall setpoint file | 
 |         std::ostringstream zoneSuffix; | 
 |         zoneSuffix << ".zone" << _zoneId; | 
 |         std::string zoneSetpointPath = setpointpath + zoneSuffix.str(); | 
 |  | 
 |         fileParseRpm(zoneSetpointPath, _maximumSetPoint); | 
 |     } | 
 |     return; | 
 | } | 
 |  | 
 | void DbusPidZone::initializeLog(void) | 
 | { | 
 |     /* Print header for log file: | 
 |      * epoch_ms,setpt,fan1,fan1_raw,fan1_pwm,fan1_pwm_raw,fan2,fan2_raw,fan2_pwm,fan2_pwm_raw,fanN,fanN_raw,fanN_pwm,fanN_pwm_raw,sensor1,sensor1_raw,sensor2,sensor2_raw,sensorN,sensorN_raw,failsafe | 
 |      */ | 
 |  | 
 |     _log << "epoch_ms,setpt,requester"; | 
 |  | 
 |     for (const auto& f : _fanInputs) | 
 |     { | 
 |         _log << "," << f << "," << f << "_raw"; | 
 |         _log << "," << f << "_pwm," << f << "_pwm_raw"; | 
 |     } | 
 |     for (const auto& t : _thermalInputs) | 
 |     { | 
 |         _log << "," << t << "," << t << "_raw"; | 
 |     } | 
 |  | 
 |     _log << ",failsafe"; | 
 |     _log << std::endl; | 
 | } | 
 |  | 
 | void DbusPidZone::writeLog(const std::string& value) | 
 | { | 
 |     _log << value; | 
 | } | 
 |  | 
 | /* | 
 |  * TODO(venture) This is effectively updating the cache and should check if the | 
 |  * values they're using to update it are new or old, or whatnot.  For instance, | 
 |  * if we haven't heard from the host in X time we need to detect this failure. | 
 |  * | 
 |  * I haven't decided if the Sensor should have a lastUpdated method or whether | 
 |  * that should be for the ReadInterface or etc... | 
 |  */ | 
 |  | 
 | /** | 
 |  * We want the PID loop to run with values cached, so this will get all the | 
 |  * fan tachs for the loop. | 
 |  */ | 
 | void DbusPidZone::updateFanTelemetry(void) | 
 | { | 
 |     /* TODO(venture): Should I just make _log point to /dev/null when logging | 
 |      * is disabled?  I think it's a waste to try and log things even if the | 
 |      * data is just being dropped though. | 
 |      */ | 
 |     const auto now = std::chrono::high_resolution_clock::now(); | 
 |     if (loggingEnabled) | 
 |     { | 
 |         _log << std::chrono::duration_cast<std::chrono::milliseconds>( | 
 |                     now.time_since_epoch()) | 
 |                     .count(); | 
 |         _log << "," << _maximumSetPoint; | 
 |         _log << "," << _maximumSetPointName; | 
 |     } | 
 |  | 
 |     processSensorInputs</* fanSensorLogging */ true>(_fanInputs, now); | 
 |  | 
 |     if (loggingEnabled) | 
 |     { | 
 |         for (const auto& t : _thermalInputs) | 
 |         { | 
 |             const auto& v = _cachedValuesByName[t]; | 
 |             _log << "," << v.scaled << "," << v.unscaled; | 
 |         } | 
 |     } | 
 |  | 
 |     return; | 
 | } | 
 |  | 
 | void DbusPidZone::updateSensors(void) | 
 | { | 
 |     processSensorInputs</* fanSensorLogging */ false>( | 
 |         _thermalInputs, std::chrono::high_resolution_clock::now()); | 
 |  | 
 |     return; | 
 | } | 
 |  | 
 | void DbusPidZone::initializeCache(void) | 
 | { | 
 |     auto nan = std::numeric_limits<double>::quiet_NaN(); | 
 |  | 
 |     for (const auto& f : _fanInputs) | 
 |     { | 
 |         _cachedValuesByName[f] = {nan, nan}; | 
 |         _cachedFanOutputs[f] = {nan, nan}; | 
 |  | 
 |         // Start all fans in fail-safe mode. | 
 |         markSensorMissing(f, ""); | 
 |     } | 
 |  | 
 |     for (const auto& t : _thermalInputs) | 
 |     { | 
 |         _cachedValuesByName[t] = {nan, nan}; | 
 |  | 
 |         // Start all sensors in fail-safe mode. | 
 |         markSensorMissing(t, ""); | 
 |     } | 
 | } | 
 |  | 
 | void DbusPidZone::dumpCache(void) | 
 | { | 
 |     std::cerr << "Cache values now: \n"; | 
 |     for (const auto& [name, value] : _cachedValuesByName) | 
 |     { | 
 |         std::cerr << name << ": " << value.scaled << " " << value.unscaled | 
 |                   << "\n"; | 
 |     } | 
 |  | 
 |     std::cerr << "Fan outputs now: \n"; | 
 |     for (const auto& [name, value] : _cachedFanOutputs) | 
 |     { | 
 |         std::cerr << name << ": " << value.scaled << " " << value.unscaled | 
 |                   << "\n"; | 
 |     } | 
 | } | 
 |  | 
 | void DbusPidZone::processFans(void) | 
 | { | 
 |     for (auto& p : _fans) | 
 |     { | 
 |         p->process(); | 
 |     } | 
 |  | 
 |     if (_redundantWrite) | 
 |     { | 
 |         // This is only needed once | 
 |         _redundantWrite = false; | 
 |     } | 
 | } | 
 |  | 
 | void DbusPidZone::processThermals(void) | 
 | { | 
 |     for (auto& p : _thermals) | 
 |     { | 
 |         p->process(); | 
 |     } | 
 | } | 
 |  | 
 | Sensor* DbusPidZone::getSensor(const std::string& name) | 
 | { | 
 |     return _mgr.getSensor(name); | 
 | } | 
 |  | 
 | std::vector<std::string> DbusPidZone::getSensorNames(void) | 
 | { | 
 |     return _thermalInputs; | 
 | } | 
 |  | 
 | bool DbusPidZone::getRedundantWrite(void) const | 
 | { | 
 |     return _redundantWrite; | 
 | } | 
 |  | 
 | bool DbusPidZone::manual(bool value) | 
 | { | 
 |     std::cerr << "manual: " << value << std::endl; | 
 |     setManualMode(value); | 
 |     return ModeObject::manual(value); | 
 | } | 
 |  | 
 | bool DbusPidZone::failSafe() const | 
 | { | 
 |     return getFailSafeMode(); | 
 | } | 
 |  | 
 | void DbusPidZone::addPidControlProcess( | 
 |     const std::string& name, const std::string& type, double setpoint, | 
 |     sdbusplus::bus_t& bus, const std::string& objPath, bool defer) | 
 | { | 
 |     _pidsControlProcess[name] = std::make_unique<ProcessObject>( | 
 |         bus, objPath.c_str(), | 
 |         defer ? ProcessObject::action::defer_emit | 
 |               : ProcessObject::action::emit_object_added); | 
 |     // Default enable setting = true | 
 |     _pidsControlProcess[name]->enabled(true); | 
 |     _pidsControlProcess[name]->setpoint(setpoint); | 
 |  | 
 |     if (type == "temp") | 
 |     { | 
 |         _pidsControlProcess[name]->classType("Temperature"); | 
 |     } | 
 |     else if (type == "margin") | 
 |     { | 
 |         _pidsControlProcess[name]->classType("Margin"); | 
 |     } | 
 |     else if (type == "power") | 
 |     { | 
 |         _pidsControlProcess[name]->classType("Power"); | 
 |     } | 
 |     else if (type == "powersum") | 
 |     { | 
 |         _pidsControlProcess[name]->classType("PowerSum"); | 
 |     } | 
 | } | 
 |  | 
 | bool DbusPidZone::isPidProcessEnabled(const std::string& name) | 
 | { | 
 |     return _pidsControlProcess[name]->enabled(); | 
 | } | 
 |  | 
 | void DbusPidZone::addPidFailSafePercent(const std::vector<std::string>& inputs, | 
 |                                         double percent) | 
 | { | 
 |     for (const auto& sensorName : inputs) | 
 |     { | 
 |         if (_sensorFailSafePercent.find(sensorName) != | 
 |             _sensorFailSafePercent.end()) | 
 |         { | 
 |             _sensorFailSafePercent[sensorName] = | 
 |                 std::max(_sensorFailSafePercent[sensorName], percent); | 
 |             if (debugEnabled) | 
 |             { | 
 |                 std::cerr << "Sensor " << sensorName | 
 |                           << " failsafe percent updated to " | 
 |                           << _sensorFailSafePercent[sensorName] << "\n"; | 
 |             } | 
 |         } | 
 |         else | 
 |         { | 
 |             _sensorFailSafePercent[sensorName] = percent; | 
 |             if (debugEnabled) | 
 |             { | 
 |                 std::cerr << "Sensor " << sensorName | 
 |                           << " failsafe percent set to " << percent << "\n"; | 
 |             } | 
 |         } | 
 |     } | 
 | } | 
 |  | 
 | std::string DbusPidZone::leader() const | 
 | { | 
 |     return _maximumSetPointName; | 
 | } | 
 |  | 
 | void DbusPidZone::updateThermalPowerDebugInterface( | 
 |     std::string pidName, std::string leader, double input, double output) | 
 | { | 
 |     if (leader.empty()) | 
 |     { | 
 |         _pidsControlProcess[pidName]->output(output); | 
 |     } | 
 |     else | 
 |     { | 
 |         _pidsControlProcess[pidName]->leader(leader); | 
 |         _pidsControlProcess[pidName]->input(input); | 
 |     } | 
 | } | 
 |  | 
 | bool DbusPidZone::getAccSetPoint(void) const | 
 | { | 
 |     return _accumulateSetPoint; | 
 | } | 
 |  | 
 | } // namespace pid_control |