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 87 inline std::string getSensorNameFromPath(const std::string& dbusPath) 88 { 89 return dbusPath.substr(dbusPath.find_last_of('/') + 1); 90 } 91 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 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 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 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 */ 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 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 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 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