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