xref: /openbmc/phosphor-pid-control/pid/zone.cpp (revision e49bbfc7)
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 "conf.hpp"
19 
20 #include "zone.hpp"
21 
22 #include <algorithm>
23 #include <chrono>
24 #include <cstring>
25 #include <fstream>
26 #include <iostream>
27 #include <libconfig.h++>
28 #include <memory>
29 
30 #include "pid/controller.hpp"
31 #include "pid/fancontroller.hpp"
32 #include "pid/thermalcontroller.hpp"
33 #include "pid/ec/pid.hpp"
34 
35 
36 using tstamp = std::chrono::high_resolution_clock::time_point;
37 using namespace std::literals::chrono_literals;
38 
39 static constexpr bool deferSignals = true;
40 static constexpr auto objectPath = "/xyz/openbmc_project/settings/fanctrl/zone";
41 
42 float PIDZone::getMaxRPMRequest(void) const
43 {
44     return _maximumRPMSetPt;
45 }
46 
47 bool PIDZone::getManualMode(void) const
48 {
49     return _manualMode;
50 }
51 
52 void PIDZone::setManualMode(bool mode)
53 {
54     _manualMode = mode;
55 }
56 
57 bool PIDZone::getFailSafeMode(void) const
58 {
59     // If any keys are present at least one sensor is in fail safe mode.
60     return !_failSafeSensors.empty();
61 }
62 
63 int64_t PIDZone::getZoneId(void) const
64 {
65     return _zoneId;
66 }
67 
68 void PIDZone::addRPMSetPoint(float setpoint)
69 {
70     _RPMSetPoints.push_back(setpoint);
71 }
72 
73 void PIDZone::clearRPMSetPoints(void)
74 {
75     _RPMSetPoints.clear();
76 }
77 
78 float PIDZone::getFailSafePercent(void) const
79 {
80     return _failSafePercent;
81 }
82 
83 float PIDZone::getMinThermalRpmSetPt(void) const
84 {
85     return _minThermalRpmSetPt;
86 }
87 
88 void PIDZone::addFanPID(std::unique_ptr<PIDController> pid)
89 {
90     _fans.push_back(std::move(pid));
91 }
92 
93 void PIDZone::addThermalPID(std::unique_ptr<PIDController> pid)
94 {
95     _thermals.push_back(std::move(pid));
96 }
97 
98 double PIDZone::getCachedValue(const std::string& name)
99 {
100     return _cachedValuesByName.at(name);
101 }
102 
103 void PIDZone::addFanInput(std::string fan)
104 {
105     _fanInputs.push_back(fan);
106 }
107 
108 void PIDZone::addThermalInput(std::string therm)
109 {
110     _thermalInputs.push_back(therm);
111 }
112 
113 void PIDZone::determineMaxRPMRequest(void)
114 {
115     float max = 0;
116     std::vector<float>::iterator result;
117 
118     if (_RPMSetPoints.size() > 0)
119     {
120         result = std::max_element(_RPMSetPoints.begin(), _RPMSetPoints.end());
121         max = *result;
122     }
123 
124     /*
125      * If the maximum RPM set-point output is below the minimum RPM
126      * set-point, set it to the minimum.
127      */
128     max = std::max(getMinThermalRpmSetPt(), max);
129 
130 #ifdef __TUNING_LOGGING__
131     /*
132      * We received no set-points from thermal sensors.
133      * This is a case experienced during tuning where they only specify
134      * fan sensors and one large fan PID for all the fans.
135      */
136     static constexpr auto setpointpath = "/etc/thermal.d/set-point";
137     try
138     {
139         int value;
140         std::ifstream ifs;
141         ifs.open(setpointpath);
142         if (ifs.good()) {
143             ifs >> value;
144             max = value; // expecting RPM set-point, not pwm%
145         }
146     }
147     catch (const std::exception& e)
148     {
149         /* This exception is uninteresting. */
150         std::cerr << "Unable to read from '" << setpointpath << "'\n";
151     }
152 #endif
153 
154     _maximumRPMSetPt = max;
155     return;
156 }
157 
158 #ifdef __TUNING_LOGGING__
159 void PIDZone::initializeLog(void)
160 {
161     /* Print header for log file:
162      * epoch_ms,setpt,fan1,fan2,fanN,sensor1,sensor2,sensorN,failsafe
163      */
164 
165     _log << "epoch_ms,setpt";
166 
167     for (auto& f : _fanInputs)
168     {
169         _log << "," << f;
170     }
171     for (auto& t : _thermalInputs)
172     {
173         _log << "," << t;
174     }
175     _log << ",failsafe";
176     _log << std::endl;
177 
178     return;
179 }
180 
181 std::ofstream& PIDZone::getLogHandle(void)
182 {
183     return _log;
184 }
185 #endif
186 
187 /*
188  * TODO(venture) This is effectively updating the cache and should check if the
189  * values they're using to update it are new or old, or whatnot.  For instance,
190  * if we haven't heard from the host in X time we need to detect this failure.
191  *
192  * I haven't decided if the Sensor should have a lastUpdated method or whether
193  * that should be for the ReadInterface or etc...
194  */
195 
196 /**
197  * We want the PID loop to run with values cached, so this will get all the
198  * fan tachs for the loop.
199  */
200 void PIDZone::updateFanTelemetry(void)
201 {
202     /* TODO(venture): Should I just make _log point to /dev/null when logging
203      * is disabled?  I think it's a waste to try and log things even if the
204      * data is just being dropped though.
205      */
206 #ifdef __TUNING_LOGGING__
207     tstamp now = std::chrono::high_resolution_clock::now();
208     _log << std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
209     _log << "," << _maximumRPMSetPt;
210 #endif
211 
212     for (auto& f : _fanInputs)
213     {
214         auto& sensor = _mgr->getSensor(f);
215         ReadReturn r = sensor->read();
216         _cachedValuesByName[f] = r.value;
217 
218         /*
219          * TODO(venture): We should check when these were last read.
220          * However, these are the fans, so if I'm not getting updated values
221          * for them... what should I do?
222          */
223 #ifdef __TUNING_LOGGING__
224         _log << "," << r.value;
225 #endif
226     }
227 
228 #ifdef __TUNING_LOGGING__
229     for (auto& t : _thermalInputs)
230     {
231         _log << "," << _cachedValuesByName[t];
232     }
233 #endif
234 
235     return;
236 }
237 
238 void PIDZone::updateSensors(void)
239 {
240     using namespace std::chrono;
241     /* margin and temp are stored as temp */
242     tstamp now = high_resolution_clock::now();
243 
244     for (auto& t : _thermalInputs)
245     {
246         auto& sensor = _mgr->getSensor(t);
247         ReadReturn r = sensor->read();
248         int64_t timeout = sensor->GetTimeout();
249 
250         _cachedValuesByName[t] = r.value;
251         tstamp then = r.updated;
252 
253         /* Only go into failsafe if the timeout is set for
254          * the sensor.
255          */
256         if (timeout > 0)
257         {
258             auto duration = duration_cast<std::chrono::seconds>
259                     (now - then).count();
260             auto period = std::chrono::seconds(timeout).count();
261             if (duration >= period)
262             {
263                 //std::cerr << "Entering fail safe mode.\n";
264                 _failSafeSensors.insert(t);
265             }
266             else
267             {
268                 // Check if it's in there: remove it.
269                 auto kt = _failSafeSensors.find(t);
270                 if (kt != _failSafeSensors.end())
271                 {
272                     _failSafeSensors.erase(kt);
273                 }
274             }
275         }
276     }
277 
278     return;
279 }
280 
281 void PIDZone::initializeCache(void)
282 {
283     for (auto& f : _fanInputs)
284     {
285         _cachedValuesByName[f] = 0;
286     }
287 
288     for (auto& t : _thermalInputs)
289     {
290         _cachedValuesByName[t] = 0;
291 
292         // Start all sensors in fail-safe mode.
293         _failSafeSensors.insert(t);
294     }
295 }
296 
297 void PIDZone::dumpCache(void)
298 {
299     std::cerr << "Cache values now: \n";
300     for (auto& k : _cachedValuesByName)
301     {
302         std::cerr << k.first << ": " << k.second << "\n";
303     }
304 }
305 
306 void PIDZone::process_fans(void)
307 {
308     for (auto& p : _fans)
309     {
310         p->pid_process();
311     }
312 }
313 
314 void PIDZone::process_thermals(void)
315 {
316     for (auto& p : _thermals)
317     {
318         p->pid_process();
319     }
320 }
321 
322 std::unique_ptr<Sensor>& PIDZone::getSensor(std::string name)
323 {
324     return _mgr->getSensor(name);
325 }
326 
327 bool PIDZone::manual(bool value)
328 {
329     std::cerr << "manual: " << value << std::endl;
330     setManualMode(value);
331     return ModeObject::manual(value);
332 }
333 
334 bool PIDZone::failSafe() const
335 {
336     return getFailSafeMode();
337 }
338 
339 static std::string GetControlPath(int64_t zone)
340 {
341     return std::string(objectPath) + std::to_string(zone);
342 }
343 
344 std::map<int64_t, std::shared_ptr<PIDZone>> BuildZones(
345             std::map<int64_t, PIDConf>& ZonePids,
346             std::map<int64_t, struct zone>& ZoneConfigs,
347             std::shared_ptr<SensorManager> mgr,
348             sdbusplus::bus::bus& ModeControlBus)
349 {
350     std::map<int64_t, std::shared_ptr<PIDZone>> zones;
351 
352     for (auto& zi : ZonePids)
353     {
354         auto zoneId = static_cast<int64_t>(zi.first);
355         /* The above shouldn't be necessary but is, and I am having trouble
356          * locating my notes on why.  If I recall correctly it was casting it
357          * down to a byte in at least some cases causing weird behaviors.
358          */
359 
360         auto zoneConf = ZoneConfigs.find(zoneId);
361         if (zoneConf == ZoneConfigs.end())
362         {
363             /* The Zone doesn't have a configuration, bail. */
364             static constexpr auto err =
365                 "Bailing during load, missing Zone Configuration";
366             std::cerr << err << std::endl;
367             throw std::runtime_error(err);
368         }
369 
370         PIDConf& PIDConfig = zi.second;
371 
372         auto zone = std::make_shared<PIDZone>(
373                         zoneId,
374                         zoneConf->second.minthermalrpm,
375                         zoneConf->second.failsafepercent,
376                         mgr,
377                         ModeControlBus,
378                         GetControlPath(zi.first).c_str(),
379                         deferSignals);
380 
381         zones[zoneId] = zone;
382 
383         std::cerr << "Zone Id: " << zone->getZoneId() << "\n";
384 
385         // For each PID create a Controller and a Sensor.
386         PIDConf::iterator pit = PIDConfig.begin();
387         while (pit != PIDConfig.end())
388         {
389             std::vector<std::string> inputs;
390             std::string name = pit->first;
391             struct controller_info* info = &pit->second;
392 
393             std::cerr << "PID name: " << name << "\n";
394 
395             /*
396              * TODO(venture): Need to check if input is known to the
397              * SensorManager.
398              */
399             if (info->type == "fan")
400             {
401                 for (auto i : info->inputs)
402                 {
403                     inputs.push_back(i);
404                     zone->addFanInput(i);
405                 }
406 
407                 auto pid = FanController::CreateFanPid(
408                                zone,
409                                name,
410                                inputs,
411                                info->info);
412                 zone->addFanPID(std::move(pid));
413             }
414             else if (info->type == "temp" || info->type == "margin")
415             {
416                 for (auto i : info->inputs)
417                 {
418                     inputs.push_back(i);
419                     zone->addThermalInput(i);
420                 }
421 
422                 auto pid = ThermalController::CreateThermalPid(
423                                zone,
424                                name,
425                                inputs,
426                                info->setpoint,
427                                info->info);
428                 zone->addThermalPID(std::move(pid));
429             }
430 
431             std::cerr << "inputs: ";
432             for (auto& i : inputs)
433             {
434                 std::cerr << i << ", ";
435             }
436             std::cerr << "\n";
437 
438             ++pit;
439         }
440 
441         zone->emit_object_added();
442     }
443 
444     return zones;
445 }
446 
447 std::map<int64_t, std::shared_ptr<PIDZone>> BuildZonesFromConfig(
448             std::string& path,
449             std::shared_ptr<SensorManager> mgr,
450             sdbusplus::bus::bus& ModeControlBus)
451 {
452     using namespace libconfig;
453     // zone -> pids
454     std::map<int64_t, PIDConf> pidConfig;
455     // zone -> configs
456     std::map<int64_t, struct zone> zoneConfig;
457 
458     std::cerr << "entered BuildZonesFromConfig\n";
459 
460     Config cfg;
461 
462     /* The load was modeled after the example source provided. */
463     try
464     {
465         cfg.readFile(path.c_str());
466     }
467     catch (const FileIOException& fioex)
468     {
469         std::cerr << "I/O error while reading file: " << fioex.what() << std::endl;
470         throw;
471     }
472     catch (const ParseException& pex)
473     {
474         std::cerr << "Parse error at " << pex.getFile() << ":" << pex.getLine()
475                   << " - " << pex.getError() << std::endl;
476         throw;
477     }
478 
479     try
480     {
481         const Setting& root = cfg.getRoot();
482         const Setting& zones = root["zones"];
483         int count = zones.getLength();
484 
485         /* For each zone. */
486         for (int i = 0; i < count; ++i)
487         {
488             const Setting& zoneSettings = zones[i];
489 
490             int id;
491             PIDConf thisZone;
492             struct zone thisZoneConfig;
493 
494             zoneSettings.lookupValue("id", id);
495 
496             thisZoneConfig.minthermalrpm =
497                     zoneSettings.lookup("minthermalrpm");
498             thisZoneConfig.failsafepercent =
499                     zoneSettings.lookup("failsafepercent");
500 
501             const Setting& pids = zoneSettings["pids"];
502             int pidCount = pids.getLength();
503 
504             for (int j = 0; j < pidCount; ++j)
505             {
506                 const Setting& pid = pids[j];
507 
508                 std::string name;
509                 controller_info info;
510 
511                 /*
512                  * Mysteriously if you use lookupValue on these, and the type
513                  * is float.  It won't work right.
514                  *
515                  * If the configuration file value doesn't look explicitly like
516                  * a float it won't let you assign it to one.
517                  */
518                 name = pid.lookup("name").c_str();
519                 info.type = pid.lookup("type").c_str();
520                 /* set-point is only required to be set for thermal. */
521                 /* TODO(venture): Verify this works optionally here. */
522                 info.setpoint = pid.lookup("set-point");
523                 info.info.ts = pid.lookup("pid.sampleperiod");
524                 info.info.p_c = pid.lookup("pid.p_coefficient");
525                 info.info.i_c = pid.lookup("pid.i_coefficient");
526                 info.info.ff_off = pid.lookup("pid.ff_off_coefficient");
527                 info.info.ff_gain = pid.lookup("pid.ff_gain_coefficient");
528                 info.info.i_lim.min = pid.lookup("pid.i_limit.min");
529                 info.info.i_lim.max = pid.lookup("pid.i_limit.max");
530                 info.info.out_lim.min = pid.lookup("pid.out_limit.min");
531                 info.info.out_lim.max = pid.lookup("pid.out_limit.max");
532                 info.info.slew_neg = pid.lookup("pid.slew_neg");
533                 info.info.slew_pos = pid.lookup("pid.slew_pos");
534 
535                 std::cerr << "out_lim.min: " << info.info.out_lim.min << "\n";
536                 std::cerr << "out_lim.max: " << info.info.out_lim.max << "\n";
537 
538                 const Setting& inputs = pid["inputs"];
539                 int icount = inputs.getLength();
540 
541                 for (int z = 0; z < icount; ++z)
542                 {
543                     std::string v;
544                     v = pid["inputs"][z].c_str();
545                     info.inputs.push_back(v);
546                 }
547 
548                 thisZone[name] = info;
549             }
550 
551             pidConfig[static_cast<int64_t>(id)] = thisZone;
552             zoneConfig[static_cast<int64_t>(id)] = thisZoneConfig;
553         }
554     }
555     catch (const SettingTypeException &setex)
556     {
557         std::cerr << "Setting '" << setex.getPath() << "' type exception!" << std::endl;
558         throw;
559     }
560     catch (const SettingNotFoundException& snex)
561     {
562         std::cerr << "Setting '" << snex.getPath() << "' not found!" << std::endl;
563         throw;
564     }
565 
566     return BuildZones(pidConfig, zoneConfig, mgr, ModeControlBus);
567 }
568