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