xref: /openbmc/phosphor-pid-control/pid/zone.cpp (revision a4270075f7cbdb2dee38f444a59e25b96d8128f4)
1 /**
2  * Copyright 2017 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 /* Configuration. */
18 #include "zone.hpp"
19 
20 #include "conf.hpp"
21 #include "failsafeloggers/failsafe_logger_utility.hpp"
22 #include "pid/controller.hpp"
23 #include "pid/ec/pid.hpp"
24 #include "pid/fancontroller.hpp"
25 #include "pid/stepwisecontroller.hpp"
26 #include "pid/thermalcontroller.hpp"
27 #include "pid/tuning.hpp"
28 
29 #include <algorithm>
30 #include <chrono>
31 #include <cstring>
32 #include <fstream>
33 #include <iostream>
34 #include <memory>
35 #include <sstream>
36 #include <string>
37 
38 using tstamp = std::chrono::high_resolution_clock::time_point;
39 using namespace std::literals::chrono_literals;
40 
41 // Enforces minimum duration between events
42 // Rreturns true if event should be allowed, false if disallowed
43 bool allowThrottle(const tstamp& now, const std::chrono::seconds& pace)
44 {
45     static tstamp then;
46     static bool first = true;
47 
48     if (first)
49     {
50         // Special case initialization
51         then = now;
52         first = false;
53 
54         // Initialization, always allow
55         return true;
56     }
57 
58     auto elapsed = now - then;
59     if (elapsed < pace)
60     {
61         // Too soon since last time, disallow
62         return false;
63     }
64 
65     // It has been long enough, allow
66     then = now;
67     return true;
68 }
69 
70 namespace pid_control
71 {
72 
73 double DbusPidZone::getMaxSetPointRequest(void) const
74 {
75     return _maximumSetPoint;
76 }
77 
78 bool DbusPidZone::getManualMode(void) const
79 {
80     return _manualMode;
81 }
82 
83 void DbusPidZone::setManualMode(bool mode)
84 {
85     _manualMode = mode;
86 
87     // If returning to automatic mode, need to restore PWM from PID loop
88     if (!mode)
89     {
90         _redundantWrite = true;
91     }
92 }
93 
94 bool DbusPidZone::getFailSafeMode(void) const
95 {
96     // If any keys are present at least one sensor is in fail safe mode.
97     return !_failSafeSensors.empty();
98 }
99 
100 FailSafeSensorsMap DbusPidZone::getFailSafeSensors(void) const
101 {
102     return _failSafeSensors;
103 }
104 
105 void DbusPidZone::markSensorMissing(const std::string& name,
106                                     const std::string& failReason)
107 {
108     if (_missingAcceptable.find(name) != _missingAcceptable.end())
109     {
110         // Disallow sensors in MissingIsAcceptable list from causing failsafe
111         outputFailsafeLogWithZone(_zoneId, this->getFailSafeMode(), name,
112                                   "The sensor is missing but is acceptable.");
113         return;
114     }
115 
116     if (_sensorFailSafePercent[name] == 0)
117     {
118         _failSafeSensors[name] = std::pair(failReason, _zoneFailSafePercent);
119     }
120     else
121     {
122         _failSafeSensors[name] =
123             std::pair(failReason, _sensorFailSafePercent[name]);
124     }
125 
126     if (debugEnabled)
127     {
128         std::cerr << "Sensor " << name << " marked missing\n";
129     }
130 }
131 
132 int64_t DbusPidZone::getZoneID(void) const
133 {
134     return _zoneId;
135 }
136 
137 void DbusPidZone::addSetPoint(double setPoint, const std::string& name)
138 {
139     /* exclude disabled pidloop from _maximumSetPoint calculation*/
140     if (!isPidProcessEnabled(name))
141     {
142         return;
143     }
144 
145     auto profileName = name;
146     if (getAccSetPoint())
147     {
148         /*
149          * If the name of controller is Linear_Temp_CPU0.
150          * The profile name will be Temp_CPU0.
151          */
152         profileName = name.substr(name.find("_") + 1);
153         _SetPoints[profileName] += setPoint;
154     }
155     else
156     {
157         if (_SetPoints[profileName] < setPoint)
158         {
159             _SetPoints[profileName] = setPoint;
160         }
161     }
162 
163     /*
164      * if there are multiple thermal controllers with the same
165      * value, pick the first one in the iterator
166      */
167     if (_maximumSetPoint < _SetPoints[profileName])
168     {
169         _maximumSetPoint = _SetPoints[profileName];
170         _maximumSetPointName = profileName;
171     }
172 }
173 
174 void DbusPidZone::addRPMCeiling(double ceiling)
175 {
176     _RPMCeilings.push_back(ceiling);
177 }
178 
179 void DbusPidZone::clearRPMCeilings(void)
180 {
181     _RPMCeilings.clear();
182 }
183 
184 void DbusPidZone::clearSetPoints(void)
185 {
186     _SetPoints.clear();
187     _maximumSetPoint = 0;
188     _maximumSetPointName.clear();
189 }
190 
191 double DbusPidZone::getFailSafePercent(void)
192 {
193     FailSafeSensorsMap::iterator maxData = std::max_element(
194         _failSafeSensors.begin(), _failSafeSensors.end(),
195         [](const FailSafeSensorPair firstData,
196            const FailSafeSensorPair secondData) {
197             return firstData.second.second < secondData.second.second;
198         });
199 
200     // In dbus/dbusconfiguration.cpp, the default sensor failsafepercent is 0 if
201     // there is no setting in json.
202     // Therfore, if the max failsafe duty in _failSafeSensors is 0, set final
203     // failsafe duty to _zoneFailSafePercent.
204     if ((*maxData).second.second == 0)
205     {
206         return _zoneFailSafePercent;
207     }
208     else
209     {
210         return (*maxData).second.second;
211     }
212 }
213 
214 double DbusPidZone::getMinThermalSetPoint(void) const
215 {
216     return _minThermalOutputSetPt;
217 }
218 
219 uint64_t DbusPidZone::getCycleIntervalTime(void) const
220 {
221     return _cycleTime.cycleIntervalTimeMS;
222 }
223 
224 uint64_t DbusPidZone::getUpdateThermalsCycle(void) const
225 {
226     return _cycleTime.updateThermalsTimeMS;
227 }
228 
229 void DbusPidZone::addFanPID(std::unique_ptr<Controller> pid)
230 {
231     _fans.push_back(std::move(pid));
232 }
233 
234 void DbusPidZone::addThermalPID(std::unique_ptr<Controller> pid)
235 {
236     _thermals.push_back(std::move(pid));
237 }
238 
239 double DbusPidZone::getCachedValue(const std::string& name)
240 {
241     return _cachedValuesByName.at(name).scaled;
242 }
243 
244 ValueCacheEntry DbusPidZone::getCachedValues(const std::string& name)
245 {
246     return _cachedValuesByName.at(name);
247 }
248 
249 void DbusPidZone::setOutputCache(std::string_view name,
250                                  const ValueCacheEntry& values)
251 {
252     _cachedFanOutputs[std::string{name}] = values;
253 }
254 
255 void DbusPidZone::addFanInput(const std::string& fan, bool missingAcceptable)
256 {
257     _fanInputs.push_back(fan);
258 
259     if (missingAcceptable)
260     {
261         _missingAcceptable.emplace(fan);
262     }
263 }
264 
265 void DbusPidZone::addThermalInput(const std::string& therm,
266                                   bool missingAcceptable)
267 {
268     /*
269      * One sensor may have stepwise and PID at the same time.
270      * Searching the sensor name before inserting it to avoid duplicated sensor
271      * names.
272      */
273     if (std::find(_thermalInputs.begin(), _thermalInputs.end(), therm) ==
274         _thermalInputs.end())
275     {
276         _thermalInputs.push_back(therm);
277     }
278 
279     if (missingAcceptable)
280     {
281         _missingAcceptable.emplace(therm);
282     }
283 }
284 
285 // Updates desired RPM setpoint from optional text file
286 // Returns true if rpmValue updated, false if left unchanged
287 static bool fileParseRpm(const std::string& fileName, double& rpmValue)
288 {
289     static constexpr std::chrono::seconds throttlePace{3};
290 
291     std::string errText;
292 
293     try
294     {
295         std::ifstream ifs;
296         ifs.open(fileName);
297         if (ifs)
298         {
299             int value;
300             ifs >> value;
301 
302             if (value <= 0)
303             {
304                 errText = "File content could not be parsed to a number";
305             }
306             else if (value <= 100)
307             {
308                 errText = "File must contain RPM value, not PWM value";
309             }
310             else
311             {
312                 rpmValue = static_cast<double>(value);
313                 return true;
314             }
315         }
316     }
317     catch (const std::exception& e)
318     {
319         errText = "Exception: ";
320         errText += e.what();
321     }
322 
323     // The file is optional, intentionally not an error if file not found
324     if (!(errText.empty()))
325     {
326         tstamp now = std::chrono::high_resolution_clock::now();
327         if (allowThrottle(now, throttlePace))
328         {
329             std::cerr << "Unable to read from '" << fileName << "': " << errText
330                       << "\n";
331         }
332     }
333 
334     return false;
335 }
336 
337 void DbusPidZone::determineMaxSetPointRequest(void)
338 {
339     std::vector<double>::iterator result;
340     double minThermalThreshold = getMinThermalSetPoint();
341 
342     if (_RPMCeilings.size() > 0)
343     {
344         result = std::min_element(_RPMCeilings.begin(), _RPMCeilings.end());
345         // if Max set point is larger than the lowest ceiling, reset to lowest
346         // ceiling.
347         if (*result < _maximumSetPoint)
348         {
349             _maximumSetPoint = *result;
350             // When using lowest ceiling, controller name is ceiling.
351             _maximumSetPointName = "Ceiling";
352         }
353     }
354 
355     /*
356      * Combine the maximum SetPoint Name if the controllers have same profile
357      * name. e.g., PID_BB_INLET_TEMP_C + Stepwise_BB_INLET_TEMP_C.
358      */
359     if (getAccSetPoint())
360     {
361         auto profileName = _maximumSetPointName;
362         _maximumSetPointName = "";
363 
364         for (auto& p : _thermals)
365         {
366             auto controllerID = p->getID();
367             auto found = controllerID.find(profileName);
368             if (found != std::string::npos)
369             {
370                 if (_maximumSetPointName.empty())
371                 {
372                     _maximumSetPointName = controllerID;
373                 }
374                 else
375                 {
376                     _maximumSetPointName += " + " + controllerID;
377                 }
378             }
379         }
380     }
381 
382     /*
383      * If the maximum RPM setpoint output is below the minimum RPM
384      * setpoint, set it to the minimum.
385      */
386     if (minThermalThreshold >= _maximumSetPoint)
387     {
388         _maximumSetPoint = minThermalThreshold;
389         _maximumSetPointName = "Minimum";
390     }
391     else if (_maximumSetPointName.compare(_maximumSetPointNamePrev))
392     {
393         std::cerr << "PID Zone " << _zoneId << " max SetPoint "
394                   << _maximumSetPoint << " requested by "
395                   << _maximumSetPointName;
396         for (const auto& sensor : _failSafeSensors)
397         {
398             if (sensor.first.find("Fan") == std::string::npos)
399             {
400                 std::cerr << " " << sensor.first;
401             }
402         }
403         std::cerr << "\n";
404         _maximumSetPointNamePrev.assign(_maximumSetPointName);
405     }
406     if (tuningEnabled)
407     {
408         /*
409          * We received no setpoints from thermal sensors.
410          * This is a case experienced during tuning where they only specify
411          * fan sensors and one large fan PID for all the fans.
412          */
413         static constexpr auto setpointpath = "/etc/thermal.d/setpoint";
414 
415         fileParseRpm(setpointpath, _maximumSetPoint);
416 
417         // Allow per-zone setpoint files to override overall setpoint file
418         std::ostringstream zoneSuffix;
419         zoneSuffix << ".zone" << _zoneId;
420         std::string zoneSetpointPath = setpointpath + zoneSuffix.str();
421 
422         fileParseRpm(zoneSetpointPath, _maximumSetPoint);
423     }
424     return;
425 }
426 
427 void DbusPidZone::initializeLog(void)
428 {
429     /* Print header for log file:
430      * 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
431      */
432 
433     _log << "epoch_ms,setpt,requester";
434 
435     for (const auto& f : _fanInputs)
436     {
437         _log << "," << f << "," << f << "_raw";
438         _log << "," << f << "_pwm," << f << "_pwm_raw";
439     }
440     for (const auto& t : _thermalInputs)
441     {
442         _log << "," << t << "," << t << "_raw";
443     }
444 
445     _log << ",failsafe";
446     _log << std::endl;
447 }
448 
449 void DbusPidZone::writeLog(const std::string& value)
450 {
451     _log << value;
452 }
453 
454 /*
455  * TODO(venture) This is effectively updating the cache and should check if the
456  * values they're using to update it are new or old, or whatnot.  For instance,
457  * if we haven't heard from the host in X time we need to detect this failure.
458  *
459  * I haven't decided if the Sensor should have a lastUpdated method or whether
460  * that should be for the ReadInterface or etc...
461  */
462 
463 /**
464  * We want the PID loop to run with values cached, so this will get all the
465  * fan tachs for the loop.
466  */
467 void DbusPidZone::updateFanTelemetry(void)
468 {
469     /* TODO(venture): Should I just make _log point to /dev/null when logging
470      * is disabled?  I think it's a waste to try and log things even if the
471      * data is just being dropped though.
472      */
473     const auto now = std::chrono::high_resolution_clock::now();
474     if (loggingEnabled)
475     {
476         _log << std::chrono::duration_cast<std::chrono::milliseconds>(
477                     now.time_since_epoch())
478                     .count();
479         _log << "," << _maximumSetPoint;
480         _log << "," << _maximumSetPointName;
481     }
482 
483     processSensorInputs</* fanSensorLogging */ true>(_fanInputs, now);
484 
485     if (loggingEnabled)
486     {
487         for (const auto& t : _thermalInputs)
488         {
489             const auto& v = _cachedValuesByName[t];
490             _log << "," << v.scaled << "," << v.unscaled;
491         }
492     }
493 
494     return;
495 }
496 
497 void DbusPidZone::updateSensors(void)
498 {
499     processSensorInputs</* fanSensorLogging */ false>(
500         _thermalInputs, std::chrono::high_resolution_clock::now());
501 
502     return;
503 }
504 
505 void DbusPidZone::initializeCache(void)
506 {
507     auto nan = std::numeric_limits<double>::quiet_NaN();
508 
509     for (const auto& f : _fanInputs)
510     {
511         _cachedValuesByName[f] = {nan, nan};
512         _cachedFanOutputs[f] = {nan, nan};
513 
514         // Start all fans in fail-safe mode.
515         markSensorMissing(f, "");
516     }
517 
518     for (const auto& t : _thermalInputs)
519     {
520         _cachedValuesByName[t] = {nan, nan};
521 
522         // Start all sensors in fail-safe mode.
523         markSensorMissing(t, "");
524     }
525 }
526 
527 void DbusPidZone::dumpCache(void)
528 {
529     std::cerr << "Cache values now: \n";
530     for (const auto& [name, value] : _cachedValuesByName)
531     {
532         std::cerr << name << ": " << value.scaled << " " << value.unscaled
533                   << "\n";
534     }
535 
536     std::cerr << "Fan outputs now: \n";
537     for (const auto& [name, value] : _cachedFanOutputs)
538     {
539         std::cerr << name << ": " << value.scaled << " " << value.unscaled
540                   << "\n";
541     }
542 }
543 
544 void DbusPidZone::processFans(void)
545 {
546     for (auto& p : _fans)
547     {
548         p->process();
549     }
550 
551     if (_redundantWrite)
552     {
553         // This is only needed once
554         _redundantWrite = false;
555     }
556 }
557 
558 void DbusPidZone::processThermals(void)
559 {
560     for (auto& p : _thermals)
561     {
562         p->process();
563     }
564 }
565 
566 Sensor* DbusPidZone::getSensor(const std::string& name)
567 {
568     return _mgr.getSensor(name);
569 }
570 
571 std::vector<std::string> DbusPidZone::getSensorNames(void)
572 {
573     return _thermalInputs;
574 }
575 
576 bool DbusPidZone::getRedundantWrite(void) const
577 {
578     return _redundantWrite;
579 }
580 
581 bool DbusPidZone::manual(bool value)
582 {
583     std::cerr << "manual: " << value << std::endl;
584     setManualMode(value);
585     return ModeObject::manual(value);
586 }
587 
588 bool DbusPidZone::failSafe() const
589 {
590     return getFailSafeMode();
591 }
592 
593 void DbusPidZone::addPidControlProcess(std::string name, std::string type,
594                                        double setpoint, sdbusplus::bus_t& bus,
595                                        std::string objPath, bool defer)
596 {
597     _pidsControlProcess[name] = std::make_unique<ProcessObject>(
598         bus, objPath.c_str(),
599         defer ? ProcessObject::action::defer_emit
600               : ProcessObject::action::emit_object_added);
601     // Default enable setting = true
602     _pidsControlProcess[name]->enabled(true);
603     _pidsControlProcess[name]->setpoint(setpoint);
604 
605     if (type == "temp")
606     {
607         _pidsControlProcess[name]->classType("Temperature");
608     }
609     else if (type == "margin")
610     {
611         _pidsControlProcess[name]->classType("Margin");
612     }
613     else if (type == "power")
614     {
615         _pidsControlProcess[name]->classType("Power");
616     }
617     else if (type == "powersum")
618     {
619         _pidsControlProcess[name]->classType("PowerSum");
620     }
621 }
622 
623 bool DbusPidZone::isPidProcessEnabled(std::string name)
624 {
625     return _pidsControlProcess[name]->enabled();
626 }
627 
628 void DbusPidZone::addPidFailSafePercent(std::vector<std::string> inputs,
629                                         double percent)
630 {
631     for (const auto& sensorName : inputs)
632     {
633         if (_sensorFailSafePercent.find(sensorName) !=
634             _sensorFailSafePercent.end())
635         {
636             _sensorFailSafePercent[sensorName] =
637                 std::max(_sensorFailSafePercent[sensorName], percent);
638             if (debugEnabled)
639             {
640                 std::cerr << "Sensor " << sensorName
641                           << " failsafe percent updated to "
642                           << _sensorFailSafePercent[sensorName] << "\n";
643             }
644         }
645         else
646         {
647             _sensorFailSafePercent[sensorName] = percent;
648             if (debugEnabled)
649             {
650                 std::cerr << "Sensor " << sensorName
651                           << " failsafe percent set to " << percent << "\n";
652             }
653         }
654     }
655 }
656 
657 std::string DbusPidZone::leader() const
658 {
659     return _maximumSetPointName;
660 }
661 
662 void DbusPidZone::updateThermalPowerDebugInterface(
663     std::string pidName, std::string leader, double input, double output)
664 {
665     if (leader.empty())
666     {
667         _pidsControlProcess[pidName]->output(output);
668     }
669     else
670     {
671         _pidsControlProcess[pidName]->leader(leader);
672         _pidsControlProcess[pidName]->input(input);
673     }
674 }
675 
676 bool DbusPidZone::getAccSetPoint(void) const
677 {
678     return _accumulateSetPoint;
679 }
680 
681 } // namespace pid_control
682