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 = 88 bus.new_method_call("xyz.openbmc_project.ObjectMapper", 89 "/xyz/openbmc_project/object_mapper", 90 "xyz.openbmc_project.ObjectMapper", "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 = 285 std::visit(VariantToDoubleVisitor(), 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 = 384 std::visit(VariantToDoubleVisitor(), getPIDAttribute(base, "SlewNeg")); 385 info.pidInfo.slewPos = 386 std::visit(VariantToDoubleVisitor(), getPIDAttribute(base, "SlewPos")); 387 388 bool checkHysterWithSetpt = false; 389 double negativeHysteresis = 0; 390 double positiveHysteresis = 0; 391 double derivativeCoeff = 0; 392 393 auto findCheckHysterFlag = base.find("CheckHysteresisWithSetpoint"); 394 auto findNeg = base.find("NegativeHysteresis"); 395 auto findPos = base.find("PositiveHysteresis"); 396 auto findDerivative = base.find("DCoefficient"); 397 398 if (findCheckHysterFlag != base.end()) 399 { 400 checkHysterWithSetpt = std::get<bool>(findCheckHysterFlag->second); 401 } 402 if (findNeg != base.end()) 403 { 404 negativeHysteresis = 405 std::visit(VariantToDoubleVisitor(), findNeg->second); 406 } 407 if (findPos != base.end()) 408 { 409 positiveHysteresis = 410 std::visit(VariantToDoubleVisitor(), findPos->second); 411 } 412 if (findDerivative != base.end()) 413 { 414 derivativeCoeff = 415 std::visit(VariantToDoubleVisitor(), findDerivative->second); 416 } 417 418 info.pidInfo.checkHysterWithSetpt = checkHysterWithSetpt; 419 info.pidInfo.negativeHysteresis = negativeHysteresis; 420 info.pidInfo.positiveHysteresis = positiveHysteresis; 421 info.pidInfo.derivativeCoeff = derivativeCoeff; 422 } 423 424 bool init(sdbusplus::bus_t& bus, boost::asio::steady_timer& timer, 425 std::map<std::string, conf::SensorConfig>& sensorConfig, 426 std::map<int64_t, conf::PIDConf>& zoneConfig, 427 std::map<int64_t, conf::ZoneConfig>& zoneDetailsConfig) 428 { 429 sensorConfig.clear(); 430 zoneConfig.clear(); 431 zoneDetailsConfig.clear(); 432 433 createMatches(bus, timer); 434 435 auto mapper = 436 bus.new_method_call("xyz.openbmc_project.ObjectMapper", 437 "/xyz/openbmc_project/object_mapper", 438 "xyz.openbmc_project.ObjectMapper", "GetSubTree"); 439 mapper.append( 440 "/", 0, 441 std::array<const char*, 6>{ 442 objectManagerInterface, pidConfigurationInterface, 443 pidZoneConfigurationInterface, stepwiseConfigurationInterface, 444 sensorInterface, defaultPwmInterface}); 445 std::unordered_map< 446 std::string, std::unordered_map<std::string, std::vector<std::string>>> 447 respData; 448 try 449 { 450 auto resp = bus.call(mapper); 451 resp.read(respData); 452 } 453 catch (const sdbusplus::exception_t&) 454 { 455 // can't do anything without mapper call data 456 throw std::runtime_error("ObjectMapper Call Failure"); 457 } 458 459 if (respData.empty()) 460 { 461 // can't do anything without mapper call data 462 throw std::runtime_error("No configuration data available from Mapper"); 463 } 464 // create a map of pair of <has pid configuration, ObjectManager path> 465 std::unordered_map<std::string, std::pair<bool, std::string>> owners; 466 // and a map of <path, interface> for sensors 467 std::unordered_map<std::string, std::string> sensors; 468 for (const auto& objectPair : respData) 469 { 470 for (const auto& ownerPair : objectPair.second) 471 { 472 auto& owner = owners[ownerPair.first]; 473 for (const std::string& interface : ownerPair.second) 474 { 475 if (interface == objectManagerInterface) 476 { 477 owner.second = objectPair.first; 478 } 479 if (interface == pidConfigurationInterface || 480 interface == pidZoneConfigurationInterface || 481 interface == stepwiseConfigurationInterface) 482 { 483 owner.first = true; 484 } 485 if (interface == sensorInterface || 486 interface == defaultPwmInterface) 487 { 488 // we're not interested in pwm sensors, just pwm control 489 if (interface == sensorInterface && 490 objectPair.first.find("pwm") != std::string::npos) 491 { 492 continue; 493 } 494 sensors[objectPair.first] = interface; 495 } 496 } 497 } 498 } 499 ManagedObjectType configurations; 500 for (const auto& owner : owners) 501 { 502 // skip if no pid configuration (means probably a sensor) 503 if (!owner.second.first) 504 { 505 continue; 506 } 507 auto endpoint = bus.new_method_call( 508 owner.first.c_str(), owner.second.second.c_str(), 509 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); 510 ManagedObjectType configuration; 511 try 512 { 513 auto responce = bus.call(endpoint); 514 responce.read(configuration); 515 } 516 catch (const sdbusplus::exception_t&) 517 { 518 // this shouldn't happen, probably means daemon crashed 519 throw std::runtime_error( 520 "Error getting managed objects from " + owner.first); 521 } 522 523 for (auto& pathPair : configuration) 524 { 525 if (pathPair.second.find(pidConfigurationInterface) != 526 pathPair.second.end() || 527 pathPair.second.find(pidZoneConfigurationInterface) != 528 pathPair.second.end() || 529 pathPair.second.find(stepwiseConfigurationInterface) != 530 pathPair.second.end()) 531 { 532 configurations.emplace(pathPair); 533 } 534 } 535 } 536 537 // remove controllers from config that aren't in the current profile(s) 538 std::vector<std::string> selectedProfiles = getSelectedProfiles(bus); 539 if (selectedProfiles.size()) 540 { 541 for (auto pathIt = configurations.begin(); 542 pathIt != configurations.end();) 543 { 544 for (auto confIt = pathIt->second.begin(); 545 confIt != pathIt->second.end();) 546 { 547 auto profilesFind = confIt->second.find("Profiles"); 548 if (profilesFind == confIt->second.end()) 549 { 550 confIt++; 551 continue; // if no profiles selected, apply always 552 } 553 auto profiles = 554 std::get<std::vector<std::string>>(profilesFind->second); 555 if (profiles.empty()) 556 { 557 confIt++; 558 continue; 559 } 560 561 bool found = false; 562 for (const std::string& profile : profiles) 563 { 564 if (std::find(selectedProfiles.begin(), 565 selectedProfiles.end(), profile) != 566 selectedProfiles.end()) 567 { 568 found = true; 569 break; 570 } 571 } 572 if (found) 573 { 574 confIt++; 575 } 576 else 577 { 578 confIt = pathIt->second.erase(confIt); 579 } 580 } 581 if (pathIt->second.empty()) 582 { 583 pathIt = configurations.erase(pathIt); 584 } 585 else 586 { 587 pathIt++; 588 } 589 } 590 } 591 592 // On D-Bus, although not necessary, 593 // having the "zoneID" field can still be useful, 594 // as it is used for diagnostic messages, 595 // logging file names, and so on. 596 // Accept optional "ZoneIndex" parameter to explicitly specify. 597 // If not present, or not unique, auto-assign index, 598 // using 0-based numbering, ensuring uniqueness. 599 std::map<std::string, int64_t> foundZones; 600 for (const auto& configuration : configurations) 601 { 602 auto findZone = 603 configuration.second.find(pidZoneConfigurationInterface); 604 if (findZone != configuration.second.end()) 605 { 606 const auto& zone = findZone->second; 607 608 const std::string& name = std::get<std::string>(zone.at("Name")); 609 610 auto findZoneIndex = zone.find("ZoneIndex"); 611 if (findZoneIndex == zone.end()) 612 { 613 continue; 614 } 615 616 auto ptrZoneIndex = std::get_if<double>(&(findZoneIndex->second)); 617 if (!ptrZoneIndex) 618 { 619 continue; 620 } 621 622 auto desiredIndex = static_cast<int64_t>(*ptrZoneIndex); 623 auto grantedIndex = setZoneIndex(name, foundZones, desiredIndex); 624 std::cout << "Zone " << name << " is at ZoneIndex " << grantedIndex 625 << "\n"; 626 } 627 } 628 629 for (const auto& configuration : configurations) 630 { 631 auto findZone = 632 configuration.second.find(pidZoneConfigurationInterface); 633 if (findZone != configuration.second.end()) 634 { 635 const auto& zone = findZone->second; 636 637 const std::string& name = std::get<std::string>(zone.at("Name")); 638 639 auto index = getZoneIndex(name, foundZones); 640 641 auto& details = zoneDetailsConfig[index]; 642 643 details.minThermalOutput = std::visit(VariantToDoubleVisitor(), 644 zone.at("MinThermalOutput")); 645 646 int failsafepercent = 0; 647 auto findFailSafe = zone.find("FailSafePercent"); 648 if (findFailSafe != zone.end()) 649 { 650 failsafepercent = std::visit(VariantToDoubleVisitor(), 651 zone.at("FailSafePercent")); 652 } 653 details.failsafePercent = failsafepercent; 654 655 getCycleTimeSetting(zone, index, "CycleIntervalTimeMS", 656 details.cycleTime.cycleIntervalTimeMS); 657 getCycleTimeSetting(zone, index, "UpdateThermalsTimeMS", 658 details.cycleTime.updateThermalsTimeMS); 659 660 bool accumulateSetPoint = false; 661 auto findAccSetPoint = zone.find("AccumulateSetPoint"); 662 if (findAccSetPoint != zone.end()) 663 { 664 accumulateSetPoint = std::get<bool>(findAccSetPoint->second); 665 } 666 details.accumulateSetPoint = accumulateSetPoint; 667 } 668 auto findBase = configuration.second.find(pidConfigurationInterface); 669 // loop through all the PID configurations and fill out a sensor config 670 if (findBase != configuration.second.end()) 671 { 672 const auto& base = 673 configuration.second.at(pidConfigurationInterface); 674 const std::string pidName = 675 sensorNameToDbusName(std::get<std::string>(base.at("Name"))); 676 const std::string pidClass = 677 std::get<std::string>(base.at("Class")); 678 const std::vector<std::string>& zones = 679 std::get<std::vector<std::string>>(base.at("Zones")); 680 for (const std::string& zone : zones) 681 { 682 auto index = getZoneIndex(zone, foundZones); 683 684 conf::PIDConf& conf = zoneConfig[index]; 685 std::vector<std::string> inputSensorNames( 686 std::get<std::vector<std::string>>(base.at("Inputs"))); 687 std::vector<std::string> outputSensorNames; 688 std::vector<std::string> missingAcceptableSensorNames; 689 690 auto findMissingAcceptable = base.find("MissingIsAcceptable"); 691 if (findMissingAcceptable != base.end()) 692 { 693 missingAcceptableSensorNames = 694 std::get<std::vector<std::string>>( 695 findMissingAcceptable->second); 696 } 697 698 // assumption: all fan pids must have at least one output 699 if (pidClass == "fan") 700 { 701 outputSensorNames = std::get<std::vector<std::string>>( 702 getPIDAttribute(base, "Outputs")); 703 } 704 705 bool unavailableAsFailed = true; 706 auto findUnavailableAsFailed = 707 base.find("InputUnavailableAsFailed"); 708 if (findUnavailableAsFailed != base.end()) 709 { 710 unavailableAsFailed = 711 std::get<bool>(findUnavailableAsFailed->second); 712 } 713 714 std::vector<SensorInterfaceType> inputSensorInterfaces; 715 std::vector<SensorInterfaceType> outputSensorInterfaces; 716 std::vector<SensorInterfaceType> 717 missingAcceptableSensorInterfaces; 718 719 /* populate an interface list for different sensor direction 720 * types (input,output) 721 */ 722 /* take the Inputs from the configuration and generate 723 * a list of dbus descriptors (path, interface). 724 * Mapping can be many-to-one since an element of Inputs can be 725 * a regex 726 */ 727 for (const std::string& sensorName : inputSensorNames) 728 { 729 findSensors(sensors, sensorNameToDbusName(sensorName), 730 inputSensorInterfaces); 731 } 732 for (const std::string& sensorName : outputSensorNames) 733 { 734 findSensors(sensors, sensorNameToDbusName(sensorName), 735 outputSensorInterfaces); 736 } 737 for (const std::string& sensorName : 738 missingAcceptableSensorNames) 739 { 740 findSensors(sensors, sensorNameToDbusName(sensorName), 741 missingAcceptableSensorInterfaces); 742 } 743 744 inputSensorNames.clear(); 745 for (const SensorInterfaceType& inputSensorInterface : 746 inputSensorInterfaces) 747 { 748 const std::string& dbusInterface = 749 inputSensorInterface.second; 750 const std::string& inputSensorPath = 751 inputSensorInterface.first; 752 753 // Setting timeout to 0 is intentional, as D-Bus passive 754 // sensor updates are pushed in, not pulled by timer poll. 755 // Setting ignoreDbusMinMax is intentional, as this 756 // prevents normalization of values to [0.0, 1.0] range, 757 // which would mess up the PID loop math. 758 // All non-fan PID classes should be initialized this way. 759 // As for why a fan should not use this code path, see 760 // the ed1dafdf168def37c65bfb7a5efd18d9dbe04727 commit. 761 if ((pidClass == "temp") || (pidClass == "margin") || 762 (pidClass == "power") || (pidClass == "powersum")) 763 { 764 std::string inputSensorName = 765 getSensorNameFromPath(inputSensorPath); 766 auto& config = sensorConfig[inputSensorName]; 767 inputSensorNames.push_back(inputSensorName); 768 config.type = pidClass; 769 config.readPath = inputSensorInterface.first; 770 config.timeout = 0; 771 config.ignoreDbusMinMax = true; 772 config.unavailableAsFailed = unavailableAsFailed; 773 } 774 775 if (dbusInterface != sensorInterface) 776 { 777 /* all expected inputs in the configuration are expected 778 * to be sensor interfaces 779 */ 780 throw std::runtime_error( 781 "sensor at dbus path [" + inputSensorPath + 782 "] has an interface [" + dbusInterface + 783 "] that does not match the expected interface of " + 784 sensorInterface); 785 } 786 } 787 788 // MissingIsAcceptable same postprocessing as Inputs 789 missingAcceptableSensorNames.clear(); 790 for (const SensorInterfaceType& 791 missingAcceptableSensorInterface : 792 missingAcceptableSensorInterfaces) 793 { 794 const std::string& dbusInterface = 795 missingAcceptableSensorInterface.second; 796 const std::string& missingAcceptableSensorPath = 797 missingAcceptableSensorInterface.first; 798 799 std::string missingAcceptableSensorName = 800 getSensorNameFromPath(missingAcceptableSensorPath); 801 missingAcceptableSensorNames.push_back( 802 missingAcceptableSensorName); 803 804 if (dbusInterface != sensorInterface) 805 { 806 /* MissingIsAcceptable same error checking as Inputs 807 */ 808 throw std::runtime_error( 809 "sensor at dbus path [" + 810 missingAcceptableSensorPath + 811 "] has an interface [" + dbusInterface + 812 "] that does not match the expected interface of " + 813 sensorInterface); 814 } 815 } 816 817 /* fan pids need to pair up tach sensors with their pwm 818 * counterparts 819 */ 820 if (pidClass == "fan") 821 { 822 /* If a PID is a fan there should be either 823 * (1) one output(pwm) per input(tach) 824 * OR 825 * (2) one putput(pwm) for all inputs(tach) 826 * everything else indicates a bad configuration. 827 */ 828 bool singlePwm = false; 829 if (outputSensorInterfaces.size() == 1) 830 { 831 /* one pwm, set write paths for all fan sensors to it */ 832 singlePwm = true; 833 } 834 else if (inputSensorInterfaces.size() == 835 outputSensorInterfaces.size()) 836 { 837 /* one to one mapping, each fan sensor gets its own pwm 838 * control */ 839 singlePwm = false; 840 } 841 else 842 { 843 throw std::runtime_error( 844 "fan PID has invalid number of Outputs"); 845 } 846 std::string fanSensorName; 847 std::string pwmPath; 848 std::string pwmInterface; 849 std::string pwmSensorName; 850 if (singlePwm) 851 { 852 /* if just a single output(pwm) is provided then use 853 * that pwm control path for all the fan sensor write 854 * path configs 855 */ 856 pwmPath = outputSensorInterfaces.at(0).first; 857 pwmInterface = outputSensorInterfaces.at(0).second; 858 } 859 for (uint32_t idx = 0; idx < inputSensorInterfaces.size(); 860 idx++) 861 { 862 if (!singlePwm) 863 { 864 pwmPath = outputSensorInterfaces.at(idx).first; 865 pwmInterface = 866 outputSensorInterfaces.at(idx).second; 867 } 868 if (defaultPwmInterface != pwmInterface) 869 { 870 throw std::runtime_error( 871 "fan pwm control at dbus path [" + pwmPath + 872 "] has an interface [" + pwmInterface + 873 "] that does not match the expected interface " 874 "of " + 875 defaultPwmInterface); 876 } 877 const std::string& fanPath = 878 inputSensorInterfaces.at(idx).first; 879 fanSensorName = getSensorNameFromPath(fanPath); 880 pwmSensorName = getSensorNameFromPath(pwmPath); 881 std::string fanPwmIndex = fanSensorName + pwmSensorName; 882 inputSensorNames.push_back(fanPwmIndex); 883 auto& fanConfig = sensorConfig[fanPwmIndex]; 884 fanConfig.type = pidClass; 885 fanConfig.readPath = fanPath; 886 fanConfig.writePath = pwmPath; 887 // todo: un-hardcode this if there are fans with 888 // different ranges 889 fanConfig.max = 255; 890 fanConfig.min = 0; 891 } 892 } 893 // if the sensors aren't available in the current state, don't 894 // add them to the configuration. 895 if (inputSensorNames.empty()) 896 { 897 continue; 898 } 899 900 std::string offsetType; 901 902 // SetPointOffset is a threshold value to pull from the sensor 903 // to apply an offset. For upper thresholds this means the 904 // setpoint is usually negative. 905 auto findSetpointOffset = base.find("SetPointOffset"); 906 if (findSetpointOffset != base.end()) 907 { 908 offsetType = 909 std::get<std::string>(findSetpointOffset->second); 910 if (std::find(thresholds::types.begin(), 911 thresholds::types.end(), offsetType) == 912 thresholds::types.end()) 913 { 914 throw std::runtime_error( 915 "Unsupported type: " + offsetType); 916 } 917 } 918 919 std::vector<double> inputTempToMargin; 920 921 auto findTempToMargin = base.find("TempToMargin"); 922 if (findTempToMargin != base.end()) 923 { 924 inputTempToMargin = 925 std::get<std::vector<double>>(findTempToMargin->second); 926 } 927 928 std::vector<pid_control::conf::SensorInput> sensorInputs = 929 spliceInputs(inputSensorNames, inputTempToMargin, 930 missingAcceptableSensorNames); 931 932 if (offsetType.empty()) 933 { 934 conf::ControllerInfo& info = conf[pidName]; 935 info.inputs = std::move(sensorInputs); 936 populatePidInfo(bus, base, info, nullptr, sensorConfig); 937 } 938 else 939 { 940 // we have to split up the inputs, as in practice t-control 941 // values will differ, making setpoints differ 942 for (const pid_control::conf::SensorInput& input : 943 sensorInputs) 944 { 945 conf::ControllerInfo& info = conf[input.name]; 946 info.inputs.emplace_back(input); 947 populatePidInfo(bus, base, info, &offsetType, 948 sensorConfig); 949 } 950 } 951 } 952 } 953 auto findStepwise = 954 configuration.second.find(stepwiseConfigurationInterface); 955 if (findStepwise != configuration.second.end()) 956 { 957 const auto& base = findStepwise->second; 958 const std::string pidName = 959 sensorNameToDbusName(std::get<std::string>(base.at("Name"))); 960 const std::vector<std::string>& zones = 961 std::get<std::vector<std::string>>(base.at("Zones")); 962 for (const std::string& zone : zones) 963 { 964 auto index = getZoneIndex(zone, foundZones); 965 966 conf::PIDConf& conf = zoneConfig[index]; 967 968 std::vector<std::string> inputs; 969 std::vector<std::string> missingAcceptableSensors; 970 std::vector<std::string> missingAcceptableSensorNames; 971 std::vector<std::string> sensorNames = 972 std::get<std::vector<std::string>>(base.at("Inputs")); 973 974 auto findMissingAcceptable = base.find("MissingIsAcceptable"); 975 if (findMissingAcceptable != base.end()) 976 { 977 missingAcceptableSensorNames = 978 std::get<std::vector<std::string>>( 979 findMissingAcceptable->second); 980 } 981 982 bool unavailableAsFailed = true; 983 auto findUnavailableAsFailed = 984 base.find("InputUnavailableAsFailed"); 985 if (findUnavailableAsFailed != base.end()) 986 { 987 unavailableAsFailed = 988 std::get<bool>(findUnavailableAsFailed->second); 989 } 990 991 bool sensorFound = false; 992 for (const std::string& sensorName : sensorNames) 993 { 994 std::vector<std::pair<std::string, std::string>> 995 sensorPathIfacePairs; 996 if (!findSensors(sensors, sensorNameToDbusName(sensorName), 997 sensorPathIfacePairs)) 998 { 999 break; 1000 } 1001 1002 for (const auto& sensorPathIfacePair : sensorPathIfacePairs) 1003 { 1004 std::string shortName = 1005 getSensorNameFromPath(sensorPathIfacePair.first); 1006 1007 inputs.push_back(shortName); 1008 auto& config = sensorConfig[shortName]; 1009 config.readPath = sensorPathIfacePair.first; 1010 config.type = "temp"; 1011 config.ignoreDbusMinMax = true; 1012 config.unavailableAsFailed = unavailableAsFailed; 1013 // todo: maybe un-hardcode this if we run into slower 1014 // timeouts with sensors 1015 1016 config.timeout = 0; 1017 sensorFound = true; 1018 } 1019 } 1020 if (!sensorFound) 1021 { 1022 continue; 1023 } 1024 1025 // MissingIsAcceptable same postprocessing as Inputs 1026 for (const std::string& missingAcceptableSensorName : 1027 missingAcceptableSensorNames) 1028 { 1029 std::vector<std::pair<std::string, std::string>> 1030 sensorPathIfacePairs; 1031 if (!findSensors( 1032 sensors, 1033 sensorNameToDbusName(missingAcceptableSensorName), 1034 sensorPathIfacePairs)) 1035 { 1036 break; 1037 } 1038 1039 for (const auto& sensorPathIfacePair : sensorPathIfacePairs) 1040 { 1041 std::string shortName = 1042 getSensorNameFromPath(sensorPathIfacePair.first); 1043 1044 missingAcceptableSensors.push_back(shortName); 1045 } 1046 } 1047 1048 conf::ControllerInfo& info = conf[pidName]; 1049 1050 std::vector<double> inputTempToMargin; 1051 1052 auto findTempToMargin = base.find("TempToMargin"); 1053 if (findTempToMargin != base.end()) 1054 { 1055 inputTempToMargin = 1056 std::get<std::vector<double>>(findTempToMargin->second); 1057 } 1058 1059 info.inputs = spliceInputs(inputs, inputTempToMargin, 1060 missingAcceptableSensors); 1061 1062 info.type = "stepwise"; 1063 info.stepwiseInfo.ts = 1.0; // currently unused 1064 info.stepwiseInfo.positiveHysteresis = 0.0; 1065 info.stepwiseInfo.negativeHysteresis = 0.0; 1066 std::string subtype = std::get<std::string>(base.at("Class")); 1067 1068 info.stepwiseInfo.isCeiling = (subtype == "Ceiling"); 1069 auto findPosHyst = base.find("PositiveHysteresis"); 1070 auto findNegHyst = base.find("NegativeHysteresis"); 1071 if (findPosHyst != base.end()) 1072 { 1073 info.stepwiseInfo.positiveHysteresis = std::visit( 1074 VariantToDoubleVisitor(), findPosHyst->second); 1075 } 1076 if (findNegHyst != base.end()) 1077 { 1078 info.stepwiseInfo.negativeHysteresis = std::visit( 1079 VariantToDoubleVisitor(), findNegHyst->second); 1080 } 1081 std::vector<double> readings = 1082 std::get<std::vector<double>>(base.at("Reading")); 1083 if (readings.size() > ec::maxStepwisePoints) 1084 { 1085 throw std::invalid_argument("Too many stepwise points."); 1086 } 1087 if (readings.empty()) 1088 { 1089 throw std::invalid_argument( 1090 "Must have one stepwise point."); 1091 } 1092 std::copy(readings.begin(), readings.end(), 1093 info.stepwiseInfo.reading); 1094 if (readings.size() < ec::maxStepwisePoints) 1095 { 1096 info.stepwiseInfo.reading[readings.size()] = 1097 std::numeric_limits<double>::quiet_NaN(); 1098 } 1099 std::vector<double> outputs = 1100 std::get<std::vector<double>>(base.at("Output")); 1101 if (readings.size() != outputs.size()) 1102 { 1103 throw std::invalid_argument( 1104 "Outputs size must match readings"); 1105 } 1106 std::copy(outputs.begin(), outputs.end(), 1107 info.stepwiseInfo.output); 1108 if (outputs.size() < ec::maxStepwisePoints) 1109 { 1110 info.stepwiseInfo.output[outputs.size()] = 1111 std::numeric_limits<double>::quiet_NaN(); 1112 } 1113 } 1114 } 1115 } 1116 if constexpr (pid_control::conf::DEBUG) 1117 { 1118 debugPrint(sensorConfig, zoneConfig, zoneDetailsConfig); 1119 } 1120 if (zoneConfig.empty() || zoneDetailsConfig.empty()) 1121 { 1122 std::cerr 1123 << "No fan zones, application pausing until new configuration\n"; 1124 return false; 1125 } 1126 return true; 1127 } 1128 1129 } // namespace dbus_configuration 1130 } // namespace pid_control 1131