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