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