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