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