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