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