1 /*
2 // Copyright (c) 2018 Intel Corporation
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 #include "dbusconfiguration.hpp"
18
19 #include "conf.hpp"
20 #include "dbushelper.hpp"
21 #include "dbusutil.hpp"
22 #include "ec/stepwise.hpp"
23 #include "util.hpp"
24
25 #include <systemd/sd-bus.h>
26
27 #include <boost/asio/error.hpp>
28 #include <boost/asio/steady_timer.hpp>
29 #include <sdbusplus/bus.hpp>
30 #include <sdbusplus/bus/match.hpp>
31 #include <sdbusplus/exception.hpp>
32 #include <sdbusplus/message.hpp>
33 #include <sdbusplus/message/native_types.hpp>
34
35 #include <algorithm>
36 #include <array>
37 #include <chrono>
38 #include <cstdint>
39 #include <format>
40 #include <iostream>
41 #include <limits>
42 #include <list>
43 #include <map>
44 #include <stdexcept>
45 #include <string>
46 #include <tuple>
47 #include <unordered_map>
48 #include <utility>
49 #include <variant>
50 #include <vector>
51
52 namespace pid_control
53 {
54
55 constexpr const char* pidConfigurationInterface =
56 "xyz.openbmc_project.Configuration.Pid";
57 constexpr const char* objectManagerInterface =
58 "org.freedesktop.DBus.ObjectManager";
59 constexpr const char* pidZoneConfigurationInterface =
60 "xyz.openbmc_project.Configuration.Pid.Zone";
61 constexpr const char* stepwiseConfigurationInterface =
62 "xyz.openbmc_project.Configuration.Stepwise";
63 constexpr const char* thermalControlIface =
64 "xyz.openbmc_project.Control.ThermalMode";
65 constexpr const char* sensorInterface = "xyz.openbmc_project.Sensor.Value";
66 constexpr const char* defaultPwmInterface =
67 "xyz.openbmc_project.Control.FanPwm";
68
69 using Association = std::tuple<std::string, std::string, std::string>;
70 using Associations = std::vector<Association>;
71
72 namespace thresholds
73 {
74 constexpr const char* warningInterface =
75 "xyz.openbmc_project.Sensor.Threshold.Warning";
76 constexpr const char* criticalInterface =
77 "xyz.openbmc_project.Sensor.Threshold.Critical";
78 const std::array<const char*, 4> types = {"CriticalLow", "CriticalHigh",
79 "WarningLow", "WarningHigh"};
80
81 } // namespace thresholds
82
83 namespace dbus_configuration
84 {
85 using SensorInterfaceType = std::pair<std::string, std::string>;
86
getSensorNameFromPath(const std::string & dbusPath)87 inline std::string getSensorNameFromPath(const std::string& dbusPath)
88 {
89 return dbusPath.substr(dbusPath.find_last_of('/') + 1);
90 }
91
sensorNameToDbusName(const std::string & sensorName)92 inline std::string sensorNameToDbusName(const std::string& sensorName)
93 {
94 std::string retString = sensorName;
95 std::replace(retString.begin(), retString.end(), ' ', '_');
96 return retString;
97 }
98
getSelectedProfiles(sdbusplus::bus_t & bus)99 std::vector<std::string> getSelectedProfiles(sdbusplus::bus_t& bus)
100 {
101 std::vector<std::string> ret;
102 auto mapper =
103 bus.new_method_call("xyz.openbmc_project.ObjectMapper",
104 "/xyz/openbmc_project/object_mapper",
105 "xyz.openbmc_project.ObjectMapper", "GetSubTree");
106 mapper.append("/", 0, std::array<const char*, 1>{thermalControlIface});
107 std::unordered_map<
108 std::string, std::unordered_map<std::string, std::vector<std::string>>>
109 respData;
110
111 try
112 {
113 auto resp = bus.call(mapper);
114 resp.read(respData);
115 }
116 catch (const sdbusplus::exception_t&)
117 {
118 // can't do anything without mapper call data
119 throw std::runtime_error("ObjectMapper Call Failure");
120 }
121 if (respData.empty())
122 {
123 // if the user has profiles but doesn't expose the interface to select
124 // one, just go ahead without using profiles
125 return ret;
126 }
127
128 // assumption is that we should only have a small handful of selected
129 // profiles at a time (probably only 1), so calling each individually should
130 // not incur a large cost
131 for (const auto& objectPair : respData)
132 {
133 const std::string& path = objectPair.first;
134 for (const auto& ownerPair : objectPair.second)
135 {
136 const std::string& busName = ownerPair.first;
137 auto getProfile =
138 bus.new_method_call(busName.c_str(), path.c_str(),
139 "org.freedesktop.DBus.Properties", "Get");
140 getProfile.append(thermalControlIface, "Current");
141 std::variant<std::string> variantResp;
142 try
143 {
144 auto resp = bus.call(getProfile);
145 resp.read(variantResp);
146 }
147 catch (const sdbusplus::exception_t&)
148 {
149 throw std::runtime_error("Failure getting profile");
150 }
151 std::string mode = std::get<std::string>(variantResp);
152 ret.emplace_back(std::move(mode));
153 }
154 }
155 if constexpr (pid_control::conf::DEBUG)
156 {
157 std::cout << "Profiles selected: ";
158 for (const auto& profile : ret)
159 {
160 std::cout << profile << " ";
161 }
162 std::cout << "\n";
163 }
164 return ret;
165 }
166
eventHandler(sd_bus_message * m,void * context,sd_bus_error *)167 int eventHandler(sd_bus_message* m, void* context, sd_bus_error*)
168 {
169 if (context == nullptr || m == nullptr)
170 {
171 throw std::runtime_error("Invalid match");
172 }
173
174 // we skip associations because the mapper populates these, not the sensors
175 const std::array<const char*, 2> skipList = {
176 "xyz.openbmc_project.Association",
177 "xyz.openbmc_project.Association.Definitions"};
178
179 sdbusplus::message_t message(m);
180 if (std::string(message.get_member()) == "InterfacesAdded")
181 {
182 sdbusplus::message::object_path path;
183 std::unordered_map<
184 std::string,
185 std::unordered_map<std::string, std::variant<Associations, bool>>>
186 data;
187
188 message.read(path, data);
189
190 for (const char* skip : skipList)
191 {
192 auto find = data.find(skip);
193 if (find != data.end())
194 {
195 data.erase(find);
196 if (data.empty())
197 {
198 return 1;
199 }
200 }
201 }
202
203 if constexpr (pid_control::conf::DEBUG)
204 {
205 std::cout << "New config detected: " << path.str << std::endl;
206 for (auto& d : data)
207 {
208 std::cout << "\tdata is " << d.first << std::endl;
209 for (auto& second : d.second)
210 {
211 std::cout << "\t\tdata is " << second.first << std::endl;
212 }
213 }
214 }
215 }
216
217 boost::asio::steady_timer* timer =
218 static_cast<boost::asio::steady_timer*>(context);
219
220 // do a brief sleep as we tend to get a bunch of these events at
221 // once
222 timer->expires_after(std::chrono::seconds(2));
223 timer->async_wait([](const boost::system::error_code ec) {
224 if (ec == boost::asio::error::operation_aborted)
225 {
226 /* another timer started*/
227 return;
228 }
229
230 std::cout << "New configuration detected, reloading\n.";
231 tryRestartControlLoops();
232 });
233
234 return 1;
235 }
236
createMatches(sdbusplus::bus_t & bus,boost::asio::steady_timer & timer)237 void createMatches(sdbusplus::bus_t& bus, boost::asio::steady_timer& timer)
238 {
239 // this is a list because the matches can't be moved
240 static std::list<sdbusplus::bus::match_t> matches;
241
242 const std::array<std::string, 4> interfaces = {
243 thermalControlIface, pidConfigurationInterface,
244 pidZoneConfigurationInterface, stepwiseConfigurationInterface};
245
246 // this list only needs to be created once
247 if (!matches.empty())
248 {
249 return;
250 }
251
252 // we restart when the configuration changes or there are new sensors
253 for (const auto& interface : interfaces)
254 {
255 matches.emplace_back(
256 bus,
257 "type='signal',member='PropertiesChanged',arg0namespace='" +
258 interface + "'",
259 eventHandler, &timer);
260 }
261 matches.emplace_back(
262 bus,
263 "type='signal',member='InterfacesAdded',arg0path='/xyz/openbmc_project/"
264 "sensors/'",
265 eventHandler, &timer);
266 matches.emplace_back(bus,
267 "type='signal',member='InterfacesRemoved',arg0path='/"
268 "xyz/openbmc_project/sensors/'",
269 eventHandler, &timer);
270 }
271
272 /**
273 * retrieve an attribute from the pid configuration map
274 * @param[in] base - the PID configuration map, keys are the attributes and
275 * value is the variant associated with that attribute.
276 * @param attributeName - the name of the attribute
277 * @return a variant holding the value associated with a key
278 * @throw runtime_error : attributeName is not in base
279 */
getPIDAttribute(const std::unordered_map<std::string,DbusVariantType> & base,const std::string & attributeName)280 inline DbusVariantType getPIDAttribute(
281 const std::unordered_map<std::string, DbusVariantType>& base,
282 const std::string& attributeName)
283 {
284 auto search = base.find(attributeName);
285 if (search == base.end())
286 {
287 throw std::runtime_error("missing attribute " + attributeName);
288 }
289 return search->second;
290 }
291
getCycleTimeSetting(const std::unordered_map<std::string,DbusVariantType> & zone,const int zoneIndex,const std::string & attributeName,uint64_t & value)292 inline void getCycleTimeSetting(
293 const std::unordered_map<std::string, DbusVariantType>& zone,
294 const int zoneIndex, const std::string& attributeName, uint64_t& value)
295 {
296 auto findAttributeName = zone.find(attributeName);
297 if (findAttributeName != zone.end())
298 {
299 double tmpAttributeValue =
300 std::visit(VariantToDoubleVisitor(), zone.at(attributeName));
301 if (tmpAttributeValue >= 1.0)
302 {
303 value = static_cast<uint64_t>(tmpAttributeValue);
304 }
305 else
306 {
307 std::cerr << "Zone " << zoneIndex << ": " << attributeName
308 << " is invalid. Use default " << value << " ms\n";
309 }
310 }
311 else
312 {
313 std::cerr << "Zone " << zoneIndex << ": " << attributeName
314 << " cannot find setting. Use default " << value << " ms\n";
315 }
316 }
317
populatePidInfo(sdbusplus::bus_t & bus,const std::unordered_map<std::string,DbusVariantType> & base,conf::ControllerInfo & info,const std::string * thresholdProperty,const std::map<std::string,conf::SensorConfig> & sensorConfig)318 void populatePidInfo(
319 sdbusplus::bus_t& bus,
320 const std::unordered_map<std::string, DbusVariantType>& base,
321 conf::ControllerInfo& info, const std::string* thresholdProperty,
322 const std::map<std::string, conf::SensorConfig>& sensorConfig)
323 {
324 info.type = std::get<std::string>(getPIDAttribute(base, "Class"));
325 if (info.type == "fan")
326 {
327 info.setpoint = 0;
328 }
329 else
330 {
331 info.setpoint = std::visit(VariantToDoubleVisitor(),
332 getPIDAttribute(base, "SetPoint"));
333 }
334
335 int failsafepercent = 0;
336 auto findFailSafe = base.find("FailSafePercent");
337 if (findFailSafe != base.end())
338 {
339 failsafepercent = std::visit(VariantToDoubleVisitor(),
340 getPIDAttribute(base, "FailSafePercent"));
341 }
342 info.failSafePercent = failsafepercent;
343
344 if (thresholdProperty != nullptr)
345 {
346 std::string interface;
347 if (*thresholdProperty == "WarningHigh" ||
348 *thresholdProperty == "WarningLow")
349 {
350 interface = thresholds::warningInterface;
351 }
352 else
353 {
354 interface = thresholds::criticalInterface;
355 }
356
357 // Although this checks only the first vector element for the
358 // named threshold, it is OK, because the SetPointOffset parser
359 // splits up the input into individual vectors, each with only a
360 // single element, if it detects that SetPointOffset is in use.
361 const std::string& path =
362 sensorConfig.at(info.inputs.front().name).readPath;
363
364 DbusHelper helper(bus);
365 std::string service = helper.getService(interface, path);
366 double reading = 0;
367 try
368 {
369 helper.getProperty(service, path, interface, *thresholdProperty,
370 reading);
371 }
372 catch (const sdbusplus::exception_t& ex)
373 {
374 // unsupported threshold, leaving reading at 0
375 }
376
377 info.setpoint += reading;
378 }
379
380 info.pidInfo.ts = 1.0; // currently unused
381 info.pidInfo.proportionalCoeff = std::visit(
382 VariantToDoubleVisitor(), getPIDAttribute(base, "PCoefficient"));
383 info.pidInfo.integralCoeff = std::visit(
384 VariantToDoubleVisitor(), getPIDAttribute(base, "ICoefficient"));
385 // DCoefficient is below, it is optional, same reason as in buildjson.cpp
386 info.pidInfo.feedFwdOffset = std::visit(
387 VariantToDoubleVisitor(), getPIDAttribute(base, "FFOffCoefficient"));
388 info.pidInfo.feedFwdGain = std::visit(
389 VariantToDoubleVisitor(), getPIDAttribute(base, "FFGainCoefficient"));
390 info.pidInfo.integralLimit.max = std::visit(
391 VariantToDoubleVisitor(), getPIDAttribute(base, "ILimitMax"));
392 info.pidInfo.integralLimit.min = std::visit(
393 VariantToDoubleVisitor(), getPIDAttribute(base, "ILimitMin"));
394 info.pidInfo.outLim.max = std::visit(VariantToDoubleVisitor(),
395 getPIDAttribute(base, "OutLimitMax"));
396 info.pidInfo.outLim.min = std::visit(VariantToDoubleVisitor(),
397 getPIDAttribute(base, "OutLimitMin"));
398 info.pidInfo.slewNeg =
399 std::visit(VariantToDoubleVisitor(), getPIDAttribute(base, "SlewNeg"));
400 info.pidInfo.slewPos =
401 std::visit(VariantToDoubleVisitor(), getPIDAttribute(base, "SlewPos"));
402
403 bool checkHysterWithSetpt = false;
404 double negativeHysteresis = 0;
405 double positiveHysteresis = 0;
406 double derivativeCoeff = 0;
407
408 auto findCheckHysterFlag = base.find("CheckHysteresisWithSetpoint");
409 auto findNeg = base.find("NegativeHysteresis");
410 auto findPos = base.find("PositiveHysteresis");
411 auto findDerivative = base.find("DCoefficient");
412
413 if (findCheckHysterFlag != base.end())
414 {
415 checkHysterWithSetpt = std::get<bool>(findCheckHysterFlag->second);
416 }
417 if (findNeg != base.end())
418 {
419 negativeHysteresis =
420 std::visit(VariantToDoubleVisitor(), findNeg->second);
421 }
422 if (findPos != base.end())
423 {
424 positiveHysteresis =
425 std::visit(VariantToDoubleVisitor(), findPos->second);
426 }
427 if (findDerivative != base.end())
428 {
429 derivativeCoeff =
430 std::visit(VariantToDoubleVisitor(), findDerivative->second);
431 }
432
433 info.pidInfo.checkHysterWithSetpt = checkHysterWithSetpt;
434 info.pidInfo.negativeHysteresis = negativeHysteresis;
435 info.pidInfo.positiveHysteresis = positiveHysteresis;
436 info.pidInfo.derivativeCoeff = derivativeCoeff;
437 }
438
init(sdbusplus::bus_t & bus,boost::asio::steady_timer & timer,std::map<std::string,conf::SensorConfig> & sensorConfig,std::map<int64_t,conf::PIDConf> & zoneConfig,std::map<int64_t,conf::ZoneConfig> & zoneDetailsConfig)439 bool init(sdbusplus::bus_t& bus, boost::asio::steady_timer& timer,
440 std::map<std::string, conf::SensorConfig>& sensorConfig,
441 std::map<int64_t, conf::PIDConf>& zoneConfig,
442 std::map<int64_t, conf::ZoneConfig>& zoneDetailsConfig)
443 {
444 sensorConfig.clear();
445 zoneConfig.clear();
446 zoneDetailsConfig.clear();
447
448 createMatches(bus, timer);
449
450 auto mapper =
451 bus.new_method_call("xyz.openbmc_project.ObjectMapper",
452 "/xyz/openbmc_project/object_mapper",
453 "xyz.openbmc_project.ObjectMapper", "GetSubTree");
454 mapper.append(
455 "/", 0,
456 std::array<const char*, 6>{
457 objectManagerInterface, pidConfigurationInterface,
458 pidZoneConfigurationInterface, stepwiseConfigurationInterface,
459 sensorInterface, defaultPwmInterface});
460 std::unordered_map<
461 std::string, std::unordered_map<std::string, std::vector<std::string>>>
462 respData;
463 try
464 {
465 auto resp = bus.call(mapper);
466 resp.read(respData);
467 }
468 catch (const sdbusplus::exception_t&)
469 {
470 // can't do anything without mapper call data
471 throw std::runtime_error("ObjectMapper Call Failure");
472 }
473
474 if (respData.empty())
475 {
476 // can't do anything without mapper call data
477 throw std::runtime_error("No configuration data available from Mapper");
478 }
479 // create a map of pair of <has pid configuration, ObjectManager path>
480 std::unordered_map<std::string, std::pair<bool, std::string>> owners;
481 // and a map of <path, interface> for sensors
482 std::unordered_map<std::string, std::string> sensors;
483 for (const auto& objectPair : respData)
484 {
485 for (const auto& ownerPair : objectPair.second)
486 {
487 auto& owner = owners[ownerPair.first];
488 for (const std::string& interface : ownerPair.second)
489 {
490 if (interface == objectManagerInterface)
491 {
492 owner.second = objectPair.first;
493 }
494 if (interface == pidConfigurationInterface ||
495 interface == pidZoneConfigurationInterface ||
496 interface == stepwiseConfigurationInterface)
497 {
498 owner.first = true;
499 }
500 if (interface == sensorInterface ||
501 interface == defaultPwmInterface)
502 {
503 // we're not interested in pwm sensors, just pwm control
504 if (interface == sensorInterface &&
505 objectPair.first.find("pwm") != std::string::npos)
506 {
507 continue;
508 }
509 sensors[objectPair.first] = interface;
510 }
511 }
512 }
513 }
514 ManagedObjectType configurations;
515 for (const auto& owner : owners)
516 {
517 // skip if no pid configuration (means probably a sensor)
518 if (!owner.second.first)
519 {
520 continue;
521 }
522 auto endpoint = bus.new_method_call(
523 owner.first.c_str(), owner.second.second.c_str(),
524 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
525 ManagedObjectType configuration;
526 try
527 {
528 auto response = bus.call(endpoint);
529 response.read(configuration);
530 }
531 catch (const sdbusplus::exception_t&)
532 {
533 // this shouldn't happen, probably means daemon crashed
534 throw std::runtime_error(
535 "Error getting managed objects from " + owner.first);
536 }
537
538 for (auto& pathPair : configuration)
539 {
540 if (pathPair.second.find(pidConfigurationInterface) !=
541 pathPair.second.end() ||
542 pathPair.second.find(pidZoneConfigurationInterface) !=
543 pathPair.second.end() ||
544 pathPair.second.find(stepwiseConfigurationInterface) !=
545 pathPair.second.end())
546 {
547 configurations.emplace(pathPair);
548 }
549 }
550 }
551
552 // remove controllers from config that aren't in the current profile(s)
553 std::vector<std::string> selectedProfiles = getSelectedProfiles(bus);
554 if (selectedProfiles.size())
555 {
556 for (auto pathIt = configurations.begin();
557 pathIt != configurations.end();)
558 {
559 for (auto confIt = pathIt->second.begin();
560 confIt != pathIt->second.end();)
561 {
562 auto profilesFind = confIt->second.find("Profiles");
563 if (profilesFind == confIt->second.end())
564 {
565 confIt++;
566 continue; // if no profiles selected, apply always
567 }
568 auto profiles =
569 std::get<std::vector<std::string>>(profilesFind->second);
570 if (profiles.empty())
571 {
572 confIt++;
573 continue;
574 }
575
576 bool found = false;
577 for (const std::string& profile : profiles)
578 {
579 if (std::find(selectedProfiles.begin(),
580 selectedProfiles.end(), profile) !=
581 selectedProfiles.end())
582 {
583 found = true;
584 break;
585 }
586 }
587 if (found)
588 {
589 confIt++;
590 }
591 else
592 {
593 confIt = pathIt->second.erase(confIt);
594 }
595 }
596 if (pathIt->second.empty())
597 {
598 pathIt = configurations.erase(pathIt);
599 }
600 else
601 {
602 pathIt++;
603 }
604 }
605 }
606
607 // On D-Bus, although not necessary,
608 // having the "zoneID" field can still be useful,
609 // as it is used for diagnostic messages,
610 // logging file names, and so on.
611 // Accept optional "ZoneIndex" parameter to explicitly specify.
612 // If not present, or not unique, auto-assign index,
613 // using 0-based numbering, ensuring uniqueness.
614 std::map<std::string, int64_t> foundZones;
615 for (const auto& configuration : configurations)
616 {
617 auto findZone =
618 configuration.second.find(pidZoneConfigurationInterface);
619 if (findZone != configuration.second.end())
620 {
621 const auto& zone = findZone->second;
622
623 const std::string& name = std::get<std::string>(zone.at("Name"));
624
625 auto findZoneIndex = zone.find("ZoneIndex");
626 if (findZoneIndex == zone.end())
627 {
628 continue;
629 }
630
631 auto ptrZoneIndex = std::get_if<double>(&(findZoneIndex->second));
632 if (!ptrZoneIndex)
633 {
634 continue;
635 }
636
637 auto desiredIndex = static_cast<int64_t>(*ptrZoneIndex);
638 auto grantedIndex = setZoneIndex(name, foundZones, desiredIndex);
639 std::cout << "Zone " << name << " is at ZoneIndex " << grantedIndex
640 << "\n";
641 }
642 }
643
644 for (const auto& configuration : configurations)
645 {
646 auto findZone =
647 configuration.second.find(pidZoneConfigurationInterface);
648 if (findZone != configuration.second.end())
649 {
650 const auto& zone = findZone->second;
651
652 const std::string& name = std::get<std::string>(zone.at("Name"));
653
654 auto index = getZoneIndex(name, foundZones);
655
656 auto& details = zoneDetailsConfig[index];
657
658 details.minThermalOutput = std::visit(VariantToDoubleVisitor(),
659 zone.at("MinThermalOutput"));
660
661 int failsafepercent = 0;
662 auto findFailSafe = zone.find("FailSafePercent");
663 if (findFailSafe != zone.end())
664 {
665 failsafepercent = std::visit(VariantToDoubleVisitor(),
666 zone.at("FailSafePercent"));
667 }
668 details.failsafePercent = failsafepercent;
669
670 getCycleTimeSetting(zone, index, "CycleIntervalTimeMS",
671 details.cycleTime.cycleIntervalTimeMS);
672 getCycleTimeSetting(zone, index, "UpdateThermalsTimeMS",
673 details.cycleTime.updateThermalsTimeMS);
674
675 bool accumulateSetPoint = false;
676 auto findAccSetPoint = zone.find("AccumulateSetPoint");
677 if (findAccSetPoint != zone.end())
678 {
679 accumulateSetPoint = std::get<bool>(findAccSetPoint->second);
680 }
681 details.accumulateSetPoint = accumulateSetPoint;
682 }
683 auto findBase = configuration.second.find(pidConfigurationInterface);
684 // loop through all the PID configurations and fill out a sensor config
685 if (findBase != configuration.second.end())
686 {
687 const auto& base =
688 configuration.second.at(pidConfigurationInterface);
689 const std::string pidName =
690 sensorNameToDbusName(std::get<std::string>(base.at("Name")));
691 const std::string pidClass =
692 std::get<std::string>(base.at("Class"));
693 const std::vector<std::string>& zones =
694 std::get<std::vector<std::string>>(base.at("Zones"));
695 for (const std::string& zone : zones)
696 {
697 auto index = getZoneIndex(zone, foundZones);
698
699 conf::PIDConf& conf = zoneConfig[index];
700 std::vector<std::string> inputSensorNames(
701 std::get<std::vector<std::string>>(base.at("Inputs")));
702 std::vector<std::string> outputSensorNames;
703 std::vector<std::string> missingAcceptableSensorNames;
704 std::vector<std::string> archivedInputSensorNames;
705
706 auto findMissingAcceptable = base.find("MissingIsAcceptable");
707 if (findMissingAcceptable != base.end())
708 {
709 missingAcceptableSensorNames =
710 std::get<std::vector<std::string>>(
711 findMissingAcceptable->second);
712 }
713
714 // assumption: all fan pids must have at least one output
715 if (pidClass == "fan")
716 {
717 outputSensorNames = std::get<std::vector<std::string>>(
718 getPIDAttribute(base, "Outputs"));
719 }
720
721 bool unavailableAsFailed = true;
722 auto findUnavailableAsFailed =
723 base.find("InputUnavailableAsFailed");
724 if (findUnavailableAsFailed != base.end())
725 {
726 unavailableAsFailed =
727 std::get<bool>(findUnavailableAsFailed->second);
728 }
729
730 std::vector<SensorInterfaceType> inputSensorInterfaces;
731 std::vector<SensorInterfaceType> outputSensorInterfaces;
732 std::vector<SensorInterfaceType>
733 missingAcceptableSensorInterfaces;
734
735 /* populate an interface list for different sensor direction
736 * types (input,output)
737 */
738 /* take the Inputs from the configuration and generate
739 * a list of dbus descriptors (path, interface).
740 * Mapping can be many-to-one since an element of Inputs can be
741 * a regex
742 */
743 for (const std::string& sensorName : inputSensorNames)
744 {
745 #ifndef HANDLE_MISSING_OBJECT_PATHS
746 findSensors(sensors, sensorNameToDbusName(sensorName),
747 inputSensorInterfaces);
748 #else
749 std::vector<std::pair<std::string, std::string>>
750 sensorPathIfacePairs;
751 auto found =
752 findSensors(sensors, sensorNameToDbusName(sensorName),
753 sensorPathIfacePairs);
754 if (found)
755 {
756 inputSensorInterfaces.insert(
757 inputSensorInterfaces.end(),
758 sensorPathIfacePairs.begin(),
759 sensorPathIfacePairs.end());
760 }
761 else if (pidClass != "fan")
762 {
763 if (std::find(missingAcceptableSensorNames.begin(),
764 missingAcceptableSensorNames.end(),
765 sensorName) ==
766 missingAcceptableSensorNames.end())
767 {
768 std::cerr
769 << "Pid controller: Missing a missing-unacceptable sensor from D-Bus "
770 << sensorName << "\n";
771 std::string inputSensorName =
772 sensorNameToDbusName(sensorName);
773 auto& config = sensorConfig[inputSensorName];
774 archivedInputSensorNames.push_back(inputSensorName);
775 config.type = pidClass;
776 config.readPath =
777 getSensorPath(config.type, inputSensorName);
778 config.timeout = 0;
779 config.ignoreDbusMinMax = true;
780 config.unavailableAsFailed = unavailableAsFailed;
781 }
782 else
783 {
784 // When an input sensor is NOT on DBus, and it's in
785 // the MissingIsAcceptable list. Ignore it and
786 // continue with the next input sensor.
787 std::cout
788 << "Pid controller: Missing a missing-acceptable sensor from D-Bus "
789 << sensorName << "\n";
790 continue;
791 }
792 }
793 #endif
794 }
795 for (const std::string& sensorName : outputSensorNames)
796 {
797 findSensors(sensors, sensorNameToDbusName(sensorName),
798 outputSensorInterfaces);
799 }
800 for (const std::string& sensorName :
801 missingAcceptableSensorNames)
802 {
803 findSensors(sensors, sensorNameToDbusName(sensorName),
804 missingAcceptableSensorInterfaces);
805 }
806
807 for (const SensorInterfaceType& inputSensorInterface :
808 inputSensorInterfaces)
809 {
810 const std::string& dbusInterface =
811 inputSensorInterface.second;
812 const std::string& inputSensorPath =
813 inputSensorInterface.first;
814
815 // Setting timeout to 0 is intentional, as D-Bus passive
816 // sensor updates are pushed in, not pulled by timer poll.
817 // Setting ignoreDbusMinMax is intentional, as this
818 // prevents normalization of values to [0.0, 1.0] range,
819 // which would mess up the PID loop math.
820 // All non-fan PID classes should be initialized this way.
821 // As for why a fan should not use this code path, see
822 // the ed1dafdf168def37c65bfb7a5efd18d9dbe04727 commit.
823 if ((pidClass == "temp") || (pidClass == "margin") ||
824 (pidClass == "power") || (pidClass == "powersum"))
825 {
826 std::string inputSensorName =
827 getSensorNameFromPath(inputSensorPath);
828 auto& config = sensorConfig[inputSensorName];
829 archivedInputSensorNames.push_back(inputSensorName);
830 config.type = pidClass;
831 config.readPath = inputSensorInterface.first;
832 config.timeout = 0;
833 config.ignoreDbusMinMax = true;
834 config.unavailableAsFailed = unavailableAsFailed;
835 }
836
837 if (dbusInterface != sensorInterface)
838 {
839 /* all expected inputs in the configuration are expected
840 * to be sensor interfaces
841 */
842 throw std::runtime_error(std::format(
843 "sensor at dbus path [{}] has an interface [{}] that does not match the expected interface of {}",
844 inputSensorPath, dbusInterface, sensorInterface));
845 }
846 }
847
848 // MissingIsAcceptable same postprocessing as Inputs
849 missingAcceptableSensorNames.clear();
850 for (const SensorInterfaceType&
851 missingAcceptableSensorInterface :
852 missingAcceptableSensorInterfaces)
853 {
854 const std::string& dbusInterface =
855 missingAcceptableSensorInterface.second;
856 const std::string& missingAcceptableSensorPath =
857 missingAcceptableSensorInterface.first;
858
859 std::string missingAcceptableSensorName =
860 getSensorNameFromPath(missingAcceptableSensorPath);
861 missingAcceptableSensorNames.push_back(
862 missingAcceptableSensorName);
863
864 if (dbusInterface != sensorInterface)
865 {
866 /* MissingIsAcceptable same error checking as Inputs
867 */
868 throw std::runtime_error(std::format(
869 "sensor at dbus path [{}] has an interface [{}] that does not match the expected interface of {}",
870 missingAcceptableSensorPath, dbusInterface,
871 sensorInterface));
872 }
873 }
874
875 /* fan pids need to pair up tach sensors with their pwm
876 * counterparts
877 */
878 if (pidClass == "fan")
879 {
880 /* If a PID is a fan there should be either
881 * (1) one output(pwm) per input(tach)
882 * OR
883 * (2) one putput(pwm) for all inputs(tach)
884 * everything else indicates a bad configuration.
885 */
886 bool singlePwm = false;
887 if (outputSensorInterfaces.size() == 1)
888 {
889 /* one pwm, set write paths for all fan sensors to it */
890 singlePwm = true;
891 }
892 else if (inputSensorInterfaces.size() ==
893 outputSensorInterfaces.size())
894 {
895 /* one to one mapping, each fan sensor gets its own pwm
896 * control */
897 singlePwm = false;
898 }
899 else
900 {
901 throw std::runtime_error(
902 "fan PID has invalid number of Outputs");
903 }
904 std::string fanSensorName;
905 std::string pwmPath;
906 std::string pwmInterface;
907 std::string pwmSensorName;
908 if (singlePwm)
909 {
910 /* if just a single output(pwm) is provided then use
911 * that pwm control path for all the fan sensor write
912 * path configs
913 */
914 pwmPath = outputSensorInterfaces.at(0).first;
915 pwmInterface = outputSensorInterfaces.at(0).second;
916 }
917 for (uint32_t idx = 0; idx < inputSensorInterfaces.size();
918 idx++)
919 {
920 if (!singlePwm)
921 {
922 pwmPath = outputSensorInterfaces.at(idx).first;
923 pwmInterface =
924 outputSensorInterfaces.at(idx).second;
925 }
926 if (defaultPwmInterface != pwmInterface)
927 {
928 throw std::runtime_error(std::format(
929 "fan pwm control at dbus path [{}] has an interface [{}] that does not match the expected interface of {}",
930 pwmPath, pwmInterface, defaultPwmInterface));
931 }
932 const std::string& fanPath =
933 inputSensorInterfaces.at(idx).first;
934 fanSensorName = getSensorNameFromPath(fanPath);
935 pwmSensorName = getSensorNameFromPath(pwmPath);
936 std::string fanPwmIndex = fanSensorName + pwmSensorName;
937 archivedInputSensorNames.push_back(fanPwmIndex);
938 auto& fanConfig = sensorConfig[fanPwmIndex];
939 fanConfig.type = pidClass;
940 fanConfig.readPath = fanPath;
941 fanConfig.writePath = pwmPath;
942 // todo: un-hardcode this if there are fans with
943 // different ranges
944 fanConfig.max = 255;
945 fanConfig.min = 0;
946 }
947 }
948 // if the sensors aren't available in the current state, don't
949 // add them to the configuration.
950 if (archivedInputSensorNames.empty())
951 {
952 continue;
953 }
954
955 std::string offsetType;
956
957 // SetPointOffset is a threshold value to pull from the sensor
958 // to apply an offset. For upper thresholds this means the
959 // setpoint is usually negative.
960 auto findSetpointOffset = base.find("SetPointOffset");
961 if (findSetpointOffset != base.end())
962 {
963 offsetType =
964 std::get<std::string>(findSetpointOffset->second);
965 if (std::find(thresholds::types.begin(),
966 thresholds::types.end(), offsetType) ==
967 thresholds::types.end())
968 {
969 throw std::runtime_error(
970 "Unsupported type: " + offsetType);
971 }
972 }
973
974 std::vector<double> inputTempToMargin;
975
976 auto findTempToMargin = base.find("TempToMargin");
977 if (findTempToMargin != base.end())
978 {
979 inputTempToMargin =
980 std::get<std::vector<double>>(findTempToMargin->second);
981 }
982
983 std::vector<pid_control::conf::SensorInput> sensorInputs =
984 spliceInputs(archivedInputSensorNames, inputTempToMargin,
985 missingAcceptableSensorNames);
986
987 if (offsetType.empty())
988 {
989 conf::ControllerInfo& info = conf[pidName];
990 info.inputs = std::move(sensorInputs);
991 populatePidInfo(bus, base, info, nullptr, sensorConfig);
992 }
993 else
994 {
995 // we have to split up the inputs, as in practice t-control
996 // values will differ, making setpoints differ
997 for (const pid_control::conf::SensorInput& input :
998 sensorInputs)
999 {
1000 conf::ControllerInfo& info = conf[input.name];
1001 info.inputs.emplace_back(input);
1002 populatePidInfo(bus, base, info, &offsetType,
1003 sensorConfig);
1004 }
1005 }
1006 }
1007 }
1008 auto findStepwise =
1009 configuration.second.find(stepwiseConfigurationInterface);
1010 if (findStepwise != configuration.second.end())
1011 {
1012 const auto& base = findStepwise->second;
1013 const std::string pidName =
1014 sensorNameToDbusName(std::get<std::string>(base.at("Name")));
1015 const std::vector<std::string>& zones =
1016 std::get<std::vector<std::string>>(base.at("Zones"));
1017 for (const std::string& zone : zones)
1018 {
1019 auto index = getZoneIndex(zone, foundZones);
1020
1021 conf::PIDConf& conf = zoneConfig[index];
1022
1023 std::vector<std::string> inputs;
1024 std::vector<std::string> missingAcceptableSensors;
1025 std::vector<std::string> missingAcceptableSensorNames;
1026 std::vector<std::string> sensorNames =
1027 std::get<std::vector<std::string>>(base.at("Inputs"));
1028
1029 auto findMissingAcceptable = base.find("MissingIsAcceptable");
1030 if (findMissingAcceptable != base.end())
1031 {
1032 missingAcceptableSensorNames =
1033 std::get<std::vector<std::string>>(
1034 findMissingAcceptable->second);
1035 }
1036
1037 bool unavailableAsFailed = true;
1038 auto findUnavailableAsFailed =
1039 base.find("InputUnavailableAsFailed");
1040 if (findUnavailableAsFailed != base.end())
1041 {
1042 unavailableAsFailed =
1043 std::get<bool>(findUnavailableAsFailed->second);
1044 }
1045
1046 bool sensorFound = false;
1047 for (const std::string& sensorName : sensorNames)
1048 {
1049 std::vector<std::pair<std::string, std::string>>
1050 sensorPathIfacePairs;
1051 if (!findSensors(sensors, sensorNameToDbusName(sensorName),
1052 sensorPathIfacePairs))
1053 {
1054 #ifndef HANDLE_MISSING_OBJECT_PATHS
1055 break;
1056 #else
1057 if (std::find(missingAcceptableSensorNames.begin(),
1058 missingAcceptableSensorNames.end(),
1059 sensorName) ==
1060 missingAcceptableSensorNames.end())
1061 {
1062 // When an input sensor is NOT on DBus, and it's NOT
1063 // in the MissingIsAcceptable list. Build it as a
1064 // failed sensor with default information (temp
1065 // sensor path, temp type, ...)
1066 std::cerr
1067 << "Stepwise controller: Missing a missing-unacceptable sensor from D-Bus "
1068 << sensorName << "\n";
1069 std::string shortName =
1070 sensorNameToDbusName(sensorName);
1071
1072 inputs.push_back(shortName);
1073 auto& config = sensorConfig[shortName];
1074 config.type = "temp";
1075 config.readPath =
1076 getSensorPath(config.type, shortName);
1077 config.ignoreDbusMinMax = true;
1078 config.unavailableAsFailed = unavailableAsFailed;
1079 // todo: maybe un-hardcode this if we run into
1080 // slower timeouts with sensors
1081
1082 config.timeout = 0;
1083 sensorFound = true;
1084 }
1085 else
1086 {
1087 // When an input sensor is NOT on DBus, and it's in
1088 // the MissingIsAcceptable list. Ignore it and
1089 // continue with the next input sensor.
1090 std::cout
1091 << "Stepwise controller: Missing a missing-acceptable sensor from D-Bus "
1092 << sensorName << "\n";
1093 continue;
1094 }
1095 #endif
1096 }
1097 else
1098 {
1099 for (const auto& sensorPathIfacePair :
1100 sensorPathIfacePairs)
1101 {
1102 std::string shortName = getSensorNameFromPath(
1103 sensorPathIfacePair.first);
1104
1105 inputs.push_back(shortName);
1106 auto& config = sensorConfig[shortName];
1107 config.readPath = sensorPathIfacePair.first;
1108 config.type = "temp";
1109 config.ignoreDbusMinMax = true;
1110 config.unavailableAsFailed = unavailableAsFailed;
1111 // todo: maybe un-hardcode this if we run into
1112 // slower timeouts with sensors
1113
1114 config.timeout = 0;
1115 sensorFound = true;
1116 }
1117 }
1118 }
1119 if (!sensorFound)
1120 {
1121 continue;
1122 }
1123
1124 // MissingIsAcceptable same postprocessing as Inputs
1125 for (const std::string& missingAcceptableSensorName :
1126 missingAcceptableSensorNames)
1127 {
1128 std::vector<std::pair<std::string, std::string>>
1129 sensorPathIfacePairs;
1130 if (!findSensors(
1131 sensors,
1132 sensorNameToDbusName(missingAcceptableSensorName),
1133 sensorPathIfacePairs))
1134 {
1135 #ifndef HANDLE_MISSING_OBJECT_PATHS
1136 break;
1137 #else
1138 // When a sensor in the MissingIsAcceptable list is NOT
1139 // on DBus and it still reaches here, which contradicts
1140 // to what we did in the Input sensor building step.
1141 // Continue.
1142 continue;
1143 #endif
1144 }
1145
1146 for (const auto& sensorPathIfacePair : sensorPathIfacePairs)
1147 {
1148 std::string shortName =
1149 getSensorNameFromPath(sensorPathIfacePair.first);
1150
1151 missingAcceptableSensors.push_back(shortName);
1152 }
1153 }
1154
1155 conf::ControllerInfo& info = conf[pidName];
1156
1157 std::vector<double> inputTempToMargin;
1158
1159 auto findTempToMargin = base.find("TempToMargin");
1160 if (findTempToMargin != base.end())
1161 {
1162 inputTempToMargin =
1163 std::get<std::vector<double>>(findTempToMargin->second);
1164 }
1165
1166 info.inputs = spliceInputs(inputs, inputTempToMargin,
1167 missingAcceptableSensors);
1168
1169 info.type = "stepwise";
1170 info.stepwiseInfo.ts = 1.0; // currently unused
1171 info.stepwiseInfo.positiveHysteresis = 0.0;
1172 info.stepwiseInfo.negativeHysteresis = 0.0;
1173 std::string subtype = std::get<std::string>(base.at("Class"));
1174
1175 info.stepwiseInfo.isCeiling = (subtype == "Ceiling");
1176 auto findPosHyst = base.find("PositiveHysteresis");
1177 auto findNegHyst = base.find("NegativeHysteresis");
1178 if (findPosHyst != base.end())
1179 {
1180 info.stepwiseInfo.positiveHysteresis = std::visit(
1181 VariantToDoubleVisitor(), findPosHyst->second);
1182 }
1183 if (findNegHyst != base.end())
1184 {
1185 info.stepwiseInfo.negativeHysteresis = std::visit(
1186 VariantToDoubleVisitor(), findNegHyst->second);
1187 }
1188 std::vector<double> readings =
1189 std::get<std::vector<double>>(base.at("Reading"));
1190 if (readings.size() > ec::maxStepwisePoints)
1191 {
1192 throw std::invalid_argument("Too many stepwise points.");
1193 }
1194 if (readings.empty())
1195 {
1196 throw std::invalid_argument(
1197 "Must have one stepwise point.");
1198 }
1199 std::copy(readings.begin(), readings.end(),
1200 info.stepwiseInfo.reading);
1201 if (readings.size() < ec::maxStepwisePoints)
1202 {
1203 info.stepwiseInfo.reading[readings.size()] =
1204 std::numeric_limits<double>::quiet_NaN();
1205 }
1206 std::vector<double> outputs =
1207 std::get<std::vector<double>>(base.at("Output"));
1208 if (readings.size() != outputs.size())
1209 {
1210 throw std::invalid_argument(
1211 "Outputs size must match readings");
1212 }
1213 std::copy(outputs.begin(), outputs.end(),
1214 info.stepwiseInfo.output);
1215 if (outputs.size() < ec::maxStepwisePoints)
1216 {
1217 info.stepwiseInfo.output[outputs.size()] =
1218 std::numeric_limits<double>::quiet_NaN();
1219 }
1220 }
1221 }
1222 }
1223 if constexpr (pid_control::conf::DEBUG)
1224 {
1225 debugPrint(sensorConfig, zoneConfig, zoneDetailsConfig);
1226 }
1227 if (zoneConfig.empty() || zoneDetailsConfig.empty())
1228 {
1229 std::cerr
1230 << "No fan zones, application pausing until new configuration\n";
1231 return false;
1232 }
1233 return true;
1234 }
1235
1236 } // namespace dbus_configuration
1237 } // namespace pid_control
1238