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