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