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