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