xref: /openbmc/phosphor-pid-control/pid/zone.cpp (revision 8b4478cc)
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 
87 bool DbusPidZone::getFailSafeMode(void) const
88 {
89     // If any keys are present at least one sensor is in fail safe mode.
90     return !_failSafeSensors.empty();
91 }
92 
93 int64_t DbusPidZone::getZoneID(void) const
94 {
95     return _zoneId;
96 }
97 
98 void DbusPidZone::addSetPoint(double setpoint)
99 {
100     _SetPoints.push_back(setpoint);
101 }
102 
103 void DbusPidZone::addRPMCeiling(double ceiling)
104 {
105     _RPMCeilings.push_back(ceiling);
106 }
107 
108 void DbusPidZone::clearRPMCeilings(void)
109 {
110     _RPMCeilings.clear();
111 }
112 
113 void DbusPidZone::clearSetPoints(void)
114 {
115     _SetPoints.clear();
116 }
117 
118 double DbusPidZone::getFailSafePercent(void) const
119 {
120     return _failSafePercent;
121 }
122 
123 double DbusPidZone::getMinThermalSetpoint(void) const
124 {
125     return _minThermalOutputSetPt;
126 }
127 
128 void DbusPidZone::addFanPID(std::unique_ptr<Controller> pid)
129 {
130     _fans.push_back(std::move(pid));
131 }
132 
133 void DbusPidZone::addThermalPID(std::unique_ptr<Controller> pid)
134 {
135     _thermals.push_back(std::move(pid));
136 }
137 
138 double DbusPidZone::getCachedValue(const std::string& name)
139 {
140     return _cachedValuesByName.at(name);
141 }
142 
143 void DbusPidZone::addFanInput(const std::string& fan)
144 {
145     _fanInputs.push_back(fan);
146 }
147 
148 void DbusPidZone::addThermalInput(const std::string& therm)
149 {
150     _thermalInputs.push_back(therm);
151 }
152 
153 // Updates desired RPM setpoint from optional text file
154 // Returns true if rpmValue updated, false if left unchanged
155 static bool fileParseRpm(const std::string& fileName, double& rpmValue)
156 {
157     static constexpr std::chrono::seconds throttlePace{3};
158 
159     std::string errText;
160 
161     try
162     {
163         std::ifstream ifs;
164         ifs.open(fileName);
165         if (ifs)
166         {
167             int value;
168             ifs >> value;
169 
170             if (value <= 0)
171             {
172                 errText = "File content could not be parsed to a number";
173             }
174             else if (value <= 100)
175             {
176                 errText = "File must contain RPM value, not PWM value";
177             }
178             else
179             {
180                 rpmValue = static_cast<double>(value);
181                 return true;
182             }
183         }
184     }
185     catch (const std::exception& e)
186     {
187         errText = "Exception: ";
188         errText += e.what();
189     }
190 
191     // The file is optional, intentionally not an error if file not found
192     if (!(errText.empty()))
193     {
194         tstamp now = std::chrono::high_resolution_clock::now();
195         if (allowThrottle(now, throttlePace))
196         {
197             std::cerr << "Unable to read from '" << fileName << "': " << errText
198                       << "\n";
199         }
200     }
201 
202     return false;
203 }
204 
205 void DbusPidZone::determineMaxSetPointRequest(void)
206 {
207     double max = 0;
208     std::vector<double>::iterator result;
209 
210     if (_SetPoints.size() > 0)
211     {
212         result = std::max_element(_SetPoints.begin(), _SetPoints.end());
213         max = *result;
214     }
215 
216     if (_RPMCeilings.size() > 0)
217     {
218         result = std::min_element(_RPMCeilings.begin(), _RPMCeilings.end());
219         max = std::min(max, *result);
220     }
221 
222     /*
223      * If the maximum RPM setpoint output is below the minimum RPM
224      * setpoint, set it to the minimum.
225      */
226     max = std::max(getMinThermalSetpoint(), max);
227 
228     if (tuningEnabled)
229     {
230         /*
231          * We received no setpoints from thermal sensors.
232          * This is a case experienced during tuning where they only specify
233          * fan sensors and one large fan PID for all the fans.
234          */
235         static constexpr auto setpointpath = "/etc/thermal.d/setpoint";
236 
237         fileParseRpm(setpointpath, max);
238 
239         // Allow per-zone setpoint files to override overall setpoint file
240         std::ostringstream zoneSuffix;
241         zoneSuffix << ".zone" << _zoneId;
242         std::string zoneSetpointPath = setpointpath + zoneSuffix.str();
243 
244         fileParseRpm(zoneSetpointPath, max);
245     }
246 
247     _maximumSetPoint = max;
248     return;
249 }
250 
251 void DbusPidZone::initializeLog(void)
252 {
253     /* Print header for log file:
254      * epoch_ms,setpt,fan1,fan2,fanN,sensor1,sensor2,sensorN,failsafe
255      */
256 
257     _log << "epoch_ms,setpt";
258 
259     for (const auto& f : _fanInputs)
260     {
261         _log << "," << f;
262     }
263     for (const auto& t : _thermalInputs)
264     {
265         _log << "," << t;
266     }
267     _log << ",failsafe";
268     _log << std::endl;
269 
270     return;
271 }
272 
273 void DbusPidZone::writeLog(const std::string& value)
274 {
275     _log << value;
276     return;
277 }
278 
279 /*
280  * TODO(venture) This is effectively updating the cache and should check if the
281  * values they're using to update it are new or old, or whatnot.  For instance,
282  * if we haven't heard from the host in X time we need to detect this failure.
283  *
284  * I haven't decided if the Sensor should have a lastUpdated method or whether
285  * that should be for the ReadInterface or etc...
286  */
287 
288 /**
289  * We want the PID loop to run with values cached, so this will get all the
290  * fan tachs for the loop.
291  */
292 void DbusPidZone::updateFanTelemetry(void)
293 {
294     /* TODO(venture): Should I just make _log point to /dev/null when logging
295      * is disabled?  I think it's a waste to try and log things even if the
296      * data is just being dropped though.
297      */
298     tstamp now = std::chrono::high_resolution_clock::now();
299     if (loggingEnabled)
300     {
301         _log << std::chrono::duration_cast<std::chrono::milliseconds>(
302                     now.time_since_epoch())
303                     .count();
304         _log << "," << _maximumSetPoint;
305     }
306 
307     for (const auto& f : _fanInputs)
308     {
309         auto sensor = _mgr.getSensor(f);
310         ReadReturn r = sensor->read();
311         _cachedValuesByName[f] = r.value;
312         int64_t timeout = sensor->getTimeout();
313         tstamp then = r.updated;
314 
315         auto duration =
316             std::chrono::duration_cast<std::chrono::seconds>(now - then)
317                 .count();
318         auto period = std::chrono::seconds(timeout).count();
319         /*
320          * TODO(venture): We should check when these were last read.
321          * However, these are the fans, so if I'm not getting updated values
322          * for them... what should I do?
323          */
324         if (loggingEnabled)
325         {
326             _log << "," << r.value;
327         }
328 
329         // check if fan fail.
330         if (sensor->getFailed())
331         {
332             _failSafeSensors.insert(f);
333         }
334         else if (timeout != 0 && duration >= period)
335         {
336             _failSafeSensors.insert(f);
337         }
338         else
339         {
340             // Check if it's in there: remove it.
341             auto kt = _failSafeSensors.find(f);
342             if (kt != _failSafeSensors.end())
343             {
344                 _failSafeSensors.erase(kt);
345             }
346         }
347     }
348 
349     if (loggingEnabled)
350     {
351         for (const auto& t : _thermalInputs)
352         {
353             _log << "," << _cachedValuesByName[t];
354         }
355     }
356 
357     return;
358 }
359 
360 void DbusPidZone::updateSensors(void)
361 {
362     using namespace std::chrono;
363     /* margin and temp are stored as temp */
364     tstamp now = high_resolution_clock::now();
365 
366     for (const auto& t : _thermalInputs)
367     {
368         auto sensor = _mgr.getSensor(t);
369         ReadReturn r = sensor->read();
370         int64_t timeout = sensor->getTimeout();
371 
372         _cachedValuesByName[t] = r.value;
373         tstamp then = r.updated;
374 
375         auto duration = duration_cast<std::chrono::seconds>(now - then).count();
376         auto period = std::chrono::seconds(timeout).count();
377 
378         if (sensor->getFailed())
379         {
380             _failSafeSensors.insert(t);
381         }
382         else if (timeout != 0 && duration >= period)
383         {
384             // std::cerr << "Entering fail safe mode.\n";
385             _failSafeSensors.insert(t);
386         }
387         else
388         {
389             // Check if it's in there: remove it.
390             auto kt = _failSafeSensors.find(t);
391             if (kt != _failSafeSensors.end())
392             {
393                 _failSafeSensors.erase(kt);
394             }
395         }
396     }
397 
398     return;
399 }
400 
401 void DbusPidZone::initializeCache(void)
402 {
403     for (const auto& f : _fanInputs)
404     {
405         _cachedValuesByName[f] = 0;
406 
407         // Start all fans in fail-safe mode.
408         _failSafeSensors.insert(f);
409     }
410 
411     for (const auto& t : _thermalInputs)
412     {
413         _cachedValuesByName[t] = 0;
414 
415         // Start all sensors in fail-safe mode.
416         _failSafeSensors.insert(t);
417     }
418 }
419 
420 void DbusPidZone::dumpCache(void)
421 {
422     std::cerr << "Cache values now: \n";
423     for (const auto& [name, value] : _cachedValuesByName)
424     {
425         std::cerr << name << ": " << value << "\n";
426     }
427 }
428 
429 void DbusPidZone::processFans(void)
430 {
431     for (auto& p : _fans)
432     {
433         p->process();
434     }
435 }
436 
437 void DbusPidZone::processThermals(void)
438 {
439     for (auto& p : _thermals)
440     {
441         p->process();
442     }
443 }
444 
445 Sensor* DbusPidZone::getSensor(const std::string& name)
446 {
447     return _mgr.getSensor(name);
448 }
449 
450 bool DbusPidZone::manual(bool value)
451 {
452     std::cerr << "manual: " << value << std::endl;
453     setManualMode(value);
454     return ModeObject::manual(value);
455 }
456 
457 bool DbusPidZone::failSafe() const
458 {
459     return getFailSafeMode();
460 }
461 
462 } // namespace pid_control
463