xref: /openbmc/phosphor-pid-control/pid/zone.cpp (revision 3dcfd546)
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 "pid/controller.hpp"
22 #include "pid/ec/pid.hpp"
23 #include "pid/fancontroller.hpp"
24 #include "pid/stepwisecontroller.hpp"
25 #include "pid/thermalcontroller.hpp"
26 #include "pid/tuning.hpp"
27 
28 #include <algorithm>
29 #include <chrono>
30 #include <cstring>
31 #include <fstream>
32 #include <iostream>
33 #include <memory>
34 #include <sstream>
35 #include <string>
36 
37 using tstamp = std::chrono::high_resolution_clock::time_point;
38 using namespace std::literals::chrono_literals;
39 
40 // Enforces minimum duration between events
41 // Rreturns true if event should be allowed, false if disallowed
42 bool allowThrottle(const tstamp& now, const std::chrono::seconds& pace)
43 {
44     static tstamp then;
45     static bool first = true;
46 
47     if (first)
48     {
49         // Special case initialization
50         then = now;
51         first = false;
52 
53         // Initialization, always allow
54         return true;
55     }
56 
57     auto elapsed = now - then;
58     if (elapsed < pace)
59     {
60         // Too soon since last time, disallow
61         return false;
62     }
63 
64     // It has been long enough, allow
65     then = now;
66     return true;
67 }
68 
69 namespace pid_control
70 {
71 
72 double DbusPidZone::getMaxSetPointRequest(void) const
73 {
74     return _maximumSetPoint;
75 }
76 
77 bool DbusPidZone::getManualMode(void) const
78 {
79     return _manualMode;
80 }
81 
82 void DbusPidZone::setManualMode(bool mode)
83 {
84     _manualMode = mode;
85 
86     // If returning to automatic mode, need to restore PWM from PID loop
87     if (!mode)
88     {
89         _redundantWrite = true;
90     }
91 }
92 
93 bool DbusPidZone::getFailSafeMode(void) const
94 {
95     // If any keys are present at least one sensor is in fail safe mode.
96     return !_failSafeSensors.empty();
97 }
98 
99 int64_t DbusPidZone::getZoneID(void) const
100 {
101     return _zoneId;
102 }
103 
104 void DbusPidZone::addSetPoint(double setPoint, const std::string& name)
105 {
106     _SetPoints.push_back(setPoint);
107     /*
108      * if there are multiple thermal controllers with the same
109      * value, pick the first one in the iterator
110      */
111     if (_maximumSetPoint < setPoint)
112     {
113         _maximumSetPoint = setPoint;
114         _maximumSetPointName = name;
115     }
116 }
117 
118 void DbusPidZone::addRPMCeiling(double ceiling)
119 {
120     _RPMCeilings.push_back(ceiling);
121 }
122 
123 void DbusPidZone::clearRPMCeilings(void)
124 {
125     _RPMCeilings.clear();
126 }
127 
128 void DbusPidZone::clearSetPoints(void)
129 {
130     _SetPoints.clear();
131     _maximumSetPoint = 0;
132 }
133 
134 double DbusPidZone::getFailSafePercent(void) const
135 {
136     return _failSafePercent;
137 }
138 
139 double DbusPidZone::getMinThermalSetPoint(void) const
140 {
141     return _minThermalOutputSetPt;
142 }
143 
144 void DbusPidZone::addFanPID(std::unique_ptr<Controller> pid)
145 {
146     _fans.push_back(std::move(pid));
147 }
148 
149 void DbusPidZone::addThermalPID(std::unique_ptr<Controller> pid)
150 {
151     _thermals.push_back(std::move(pid));
152 }
153 
154 double DbusPidZone::getCachedValue(const std::string& name)
155 {
156     return _cachedValuesByName.at(name);
157 }
158 
159 void DbusPidZone::addFanInput(const std::string& fan)
160 {
161     _fanInputs.push_back(fan);
162 }
163 
164 void DbusPidZone::addThermalInput(const std::string& therm)
165 {
166     _thermalInputs.push_back(therm);
167 }
168 
169 // Updates desired RPM setpoint from optional text file
170 // Returns true if rpmValue updated, false if left unchanged
171 static bool fileParseRpm(const std::string& fileName, double& rpmValue)
172 {
173     static constexpr std::chrono::seconds throttlePace{3};
174 
175     std::string errText;
176 
177     try
178     {
179         std::ifstream ifs;
180         ifs.open(fileName);
181         if (ifs)
182         {
183             int value;
184             ifs >> value;
185 
186             if (value <= 0)
187             {
188                 errText = "File content could not be parsed to a number";
189             }
190             else if (value <= 100)
191             {
192                 errText = "File must contain RPM value, not PWM value";
193             }
194             else
195             {
196                 rpmValue = static_cast<double>(value);
197                 return true;
198             }
199         }
200     }
201     catch (const std::exception& e)
202     {
203         errText = "Exception: ";
204         errText += e.what();
205     }
206 
207     // The file is optional, intentionally not an error if file not found
208     if (!(errText.empty()))
209     {
210         tstamp now = std::chrono::high_resolution_clock::now();
211         if (allowThrottle(now, throttlePace))
212         {
213             std::cerr << "Unable to read from '" << fileName << "': " << errText
214                       << "\n";
215         }
216     }
217 
218     return false;
219 }
220 
221 void DbusPidZone::determineMaxSetPointRequest(void)
222 {
223     std::vector<double>::iterator result;
224     double minThermalThreshold = getMinThermalSetPoint();
225 
226     if (_RPMCeilings.size() > 0)
227     {
228         result = std::min_element(_RPMCeilings.begin(), _RPMCeilings.end());
229         // if Max set point is larger than the lowest ceiling, reset to lowest
230         // ceiling.
231         if (*result < _maximumSetPoint)
232         {
233             _maximumSetPoint = *result;
234             // When using lowest ceiling, controller name is ceiling.
235             _maximumSetPointName = "Ceiling";
236         }
237     }
238 
239     /*
240      * If the maximum RPM setpoint output is below the minimum RPM
241      * setpoint, set it to the minimum.
242      */
243     if (minThermalThreshold >= _maximumSetPoint)
244     {
245         _maximumSetPoint = minThermalThreshold;
246         _maximumSetPointName = "";
247     }
248     else if (_maximumSetPointName.compare(_maximumSetPointNamePrev))
249     {
250         std::cerr << "PID Zone " << _zoneId << " max SetPoint "
251                   << _maximumSetPoint << " requested by "
252                   << _maximumSetPointName;
253         for (const auto& sensor : _failSafeSensors)
254         {
255             if (sensor.find("Fan") == std::string::npos)
256             {
257                 std::cerr << " " << sensor;
258             }
259         }
260         std::cerr << "\n";
261         _maximumSetPointNamePrev.assign(_maximumSetPointName);
262     }
263     if (tuningEnabled)
264     {
265         /*
266          * We received no setpoints from thermal sensors.
267          * This is a case experienced during tuning where they only specify
268          * fan sensors and one large fan PID for all the fans.
269          */
270         static constexpr auto setpointpath = "/etc/thermal.d/setpoint";
271 
272         fileParseRpm(setpointpath, _maximumSetPoint);
273 
274         // Allow per-zone setpoint files to override overall setpoint file
275         std::ostringstream zoneSuffix;
276         zoneSuffix << ".zone" << _zoneId;
277         std::string zoneSetpointPath = setpointpath + zoneSuffix.str();
278 
279         fileParseRpm(zoneSetpointPath, _maximumSetPoint);
280     }
281     return;
282 }
283 
284 void DbusPidZone::initializeLog(void)
285 {
286     /* Print header for log file:
287      * epoch_ms,setpt,fan1,fan2,fanN,sensor1,sensor2,sensorN,failsafe
288      */
289 
290     _log << "epoch_ms,setpt,requester";
291 
292     for (const auto& f : _fanInputs)
293     {
294         _log << "," << f;
295     }
296     for (const auto& t : _thermalInputs)
297     {
298         _log << "," << t;
299     }
300     _log << ",failsafe";
301     _log << std::endl;
302 
303     return;
304 }
305 
306 void DbusPidZone::writeLog(const std::string& value)
307 {
308     _log << value;
309     return;
310 }
311 
312 /*
313  * TODO(venture) This is effectively updating the cache and should check if the
314  * values they're using to update it are new or old, or whatnot.  For instance,
315  * if we haven't heard from the host in X time we need to detect this failure.
316  *
317  * I haven't decided if the Sensor should have a lastUpdated method or whether
318  * that should be for the ReadInterface or etc...
319  */
320 
321 /**
322  * We want the PID loop to run with values cached, so this will get all the
323  * fan tachs for the loop.
324  */
325 void DbusPidZone::updateFanTelemetry(void)
326 {
327     /* TODO(venture): Should I just make _log point to /dev/null when logging
328      * is disabled?  I think it's a waste to try and log things even if the
329      * data is just being dropped though.
330      */
331     tstamp now = std::chrono::high_resolution_clock::now();
332     if (loggingEnabled)
333     {
334         _log << std::chrono::duration_cast<std::chrono::milliseconds>(
335                     now.time_since_epoch())
336                     .count();
337         _log << "," << _maximumSetPoint;
338         _log << "," << _maximumSetPointName;
339     }
340 
341     for (const auto& f : _fanInputs)
342     {
343         auto sensor = _mgr.getSensor(f);
344         ReadReturn r = sensor->read();
345         _cachedValuesByName[f] = r.value;
346         int64_t timeout = sensor->getTimeout();
347         tstamp then = r.updated;
348 
349         auto duration =
350             std::chrono::duration_cast<std::chrono::seconds>(now - then)
351                 .count();
352         auto period = std::chrono::seconds(timeout).count();
353         /*
354          * TODO(venture): We should check when these were last read.
355          * However, these are the fans, so if I'm not getting updated values
356          * for them... what should I do?
357          */
358         if (loggingEnabled)
359         {
360             _log << "," << r.value;
361         }
362 
363         // check if fan fail.
364         if (sensor->getFailed())
365         {
366             _failSafeSensors.insert(f);
367         }
368         else if (timeout != 0 && duration >= period)
369         {
370             _failSafeSensors.insert(f);
371         }
372         else
373         {
374             // Check if it's in there: remove it.
375             auto kt = _failSafeSensors.find(f);
376             if (kt != _failSafeSensors.end())
377             {
378                 _failSafeSensors.erase(kt);
379             }
380         }
381     }
382 
383     if (loggingEnabled)
384     {
385         for (const auto& t : _thermalInputs)
386         {
387             _log << "," << _cachedValuesByName[t];
388         }
389     }
390 
391     return;
392 }
393 
394 void DbusPidZone::updateSensors(void)
395 {
396     using namespace std::chrono;
397     /* margin and temp are stored as temp */
398     tstamp now = high_resolution_clock::now();
399 
400     for (const auto& t : _thermalInputs)
401     {
402         auto sensor = _mgr.getSensor(t);
403         ReadReturn r = sensor->read();
404         int64_t timeout = sensor->getTimeout();
405 
406         _cachedValuesByName[t] = r.value;
407         tstamp then = r.updated;
408 
409         auto duration = duration_cast<std::chrono::seconds>(now - then).count();
410         auto period = std::chrono::seconds(timeout).count();
411 
412         if (sensor->getFailed())
413         {
414             _failSafeSensors.insert(t);
415         }
416         else if (timeout != 0 && duration >= period)
417         {
418             // std::cerr << "Entering fail safe mode.\n";
419             _failSafeSensors.insert(t);
420         }
421         else
422         {
423             // Check if it's in there: remove it.
424             auto kt = _failSafeSensors.find(t);
425             if (kt != _failSafeSensors.end())
426             {
427                 _failSafeSensors.erase(kt);
428             }
429         }
430     }
431 
432     return;
433 }
434 
435 void DbusPidZone::initializeCache(void)
436 {
437     for (const auto& f : _fanInputs)
438     {
439         _cachedValuesByName[f] = 0;
440 
441         // Start all fans in fail-safe mode.
442         _failSafeSensors.insert(f);
443     }
444 
445     for (const auto& t : _thermalInputs)
446     {
447         _cachedValuesByName[t] = 0;
448 
449         // Start all sensors in fail-safe mode.
450         _failSafeSensors.insert(t);
451     }
452 }
453 
454 void DbusPidZone::dumpCache(void)
455 {
456     std::cerr << "Cache values now: \n";
457     for (const auto& [name, value] : _cachedValuesByName)
458     {
459         std::cerr << name << ": " << value << "\n";
460     }
461 }
462 
463 void DbusPidZone::processFans(void)
464 {
465     for (auto& p : _fans)
466     {
467         p->process();
468     }
469 
470     if (_redundantWrite)
471     {
472         // This is only needed once
473         _redundantWrite = false;
474     }
475 }
476 
477 void DbusPidZone::processThermals(void)
478 {
479     for (auto& p : _thermals)
480     {
481         p->process();
482     }
483 }
484 
485 Sensor* DbusPidZone::getSensor(const std::string& name)
486 {
487     return _mgr.getSensor(name);
488 }
489 
490 bool DbusPidZone::getRedundantWrite(void) const
491 {
492     return _redundantWrite;
493 }
494 
495 bool DbusPidZone::manual(bool value)
496 {
497     std::cerr << "manual: " << value << std::endl;
498     setManualMode(value);
499     return ModeObject::manual(value);
500 }
501 
502 bool DbusPidZone::failSafe() const
503 {
504     return getFailSafeMode();
505 }
506 
507 } // namespace pid_control
508