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