1 /* 2 // Copyright (c) 2018 Intel Corporation 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 */ 16 17 #include "conf.hpp" 18 #include "util.hpp" 19 20 #include <algorithm> 21 #include <boost/asio/steady_timer.hpp> 22 #include <chrono> 23 #include <functional> 24 #include <iostream> 25 #include <list> 26 #include <regex> 27 #include <sdbusplus/bus.hpp> 28 #include <sdbusplus/bus/match.hpp> 29 #include <sdbusplus/exception.hpp> 30 #include <set> 31 #include <unordered_map> 32 #include <variant> 33 34 static constexpr bool DEBUG = false; // enable to print found configuration 35 36 extern std::map<std::string, struct conf::SensorConfig> sensorConfig; 37 extern std::map<int64_t, conf::PIDConf> zoneConfig; 38 extern std::map<int64_t, struct conf::ZoneConfig> zoneDetailsConfig; 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* pwmInterface = "xyz.openbmc_project.Control.FanPwm"; 52 53 namespace dbus_configuration 54 { 55 56 bool findSensors(const std::unordered_map<std::string, std::string>& sensors, 57 const std::string& search, 58 std::vector<std::pair<std::string, std::string>>& matches) 59 { 60 std::smatch match; 61 std::regex reg(search); 62 for (const auto& sensor : sensors) 63 { 64 if (std::regex_search(sensor.first, match, reg)) 65 { 66 matches.push_back(sensor); 67 } 68 } 69 70 return matches.size() > 0; 71 } 72 73 // this function prints the configuration into a form similar to the cpp 74 // generated code to help in verification, should be turned off during normal 75 // use 76 void debugPrint(void) 77 { 78 // print sensor config 79 std::cout << "sensor config:\n"; 80 std::cout << "{\n"; 81 for (const auto& pair : sensorConfig) 82 { 83 84 std::cout << "\t{" << pair.first << ",\n\t\t{"; 85 std::cout << pair.second.type << ", "; 86 std::cout << pair.second.readPath << ", "; 87 std::cout << pair.second.writePath << ", "; 88 std::cout << pair.second.min << ", "; 89 std::cout << pair.second.max << ", "; 90 std::cout << pair.second.timeout << "},\n\t},\n"; 91 } 92 std::cout << "}\n\n"; 93 std::cout << "ZoneDetailsConfig\n"; 94 std::cout << "{\n"; 95 for (const auto& zone : zoneDetailsConfig) 96 { 97 std::cout << "\t{" << zone.first << ",\n"; 98 std::cout << "\t\t{" << zone.second.minThermalOutput << ", "; 99 std::cout << zone.second.failsafePercent << "}\n\t},\n"; 100 } 101 std::cout << "}\n\n"; 102 std::cout << "ZoneConfig\n"; 103 std::cout << "{\n"; 104 for (const auto& zone : zoneConfig) 105 { 106 std::cout << "\t{" << zone.first << "\n"; 107 for (const auto& pidconf : zone.second) 108 { 109 std::cout << "\t\t{" << pidconf.first << ",\n"; 110 std::cout << "\t\t\t{" << pidconf.second.type << ",\n"; 111 std::cout << "\t\t\t{"; 112 for (const auto& input : pidconf.second.inputs) 113 { 114 std::cout << "\n\t\t\t" << input << ",\n"; 115 } 116 std::cout << "\t\t\t}\n"; 117 std::cout << "\t\t\t" << pidconf.second.setpoint << ",\n"; 118 std::cout << "\t\t\t{" << pidconf.second.pidInfo.ts << ",\n"; 119 std::cout << "\t\t\t" << pidconf.second.pidInfo.proportionalCoeff 120 << ",\n"; 121 std::cout << "\t\t\t" << pidconf.second.pidInfo.integralCoeff 122 << ",\n"; 123 std::cout << "\t\t\t" << pidconf.second.pidInfo.feedFwdOffset 124 << ",\n"; 125 std::cout << "\t\t\t" << pidconf.second.pidInfo.feedFwdGain 126 << ",\n"; 127 std::cout << "\t\t\t{" << pidconf.second.pidInfo.integralLimit.min 128 << "," << pidconf.second.pidInfo.integralLimit.max 129 << "},\n"; 130 std::cout << "\t\t\t{" << pidconf.second.pidInfo.outLim.min << "," 131 << pidconf.second.pidInfo.outLim.max << "},\n"; 132 std::cout << "\t\t\t" << pidconf.second.pidInfo.slewNeg << ",\n"; 133 std::cout << "\t\t\t" << pidconf.second.pidInfo.slewPos << ",\n"; 134 std::cout << "\t\t\t}\n\t\t}\n"; 135 } 136 std::cout << "\t},\n"; 137 } 138 std::cout << "}\n\n"; 139 } 140 141 size_t getZoneIndex(const std::string& name, std::vector<std::string>& zones) 142 { 143 auto it = std::find(zones.begin(), zones.end(), name); 144 if (it == zones.end()) 145 { 146 zones.emplace_back(name); 147 it = zones.end() - 1; 148 } 149 150 return it - zones.begin(); 151 } 152 153 std::vector<std::string> getSelectedProfiles(sdbusplus::bus::bus& bus) 154 { 155 std::vector<std::string> ret; 156 auto mapper = 157 bus.new_method_call("xyz.openbmc_project.ObjectMapper", 158 "/xyz/openbmc_project/object_mapper", 159 "xyz.openbmc_project.ObjectMapper", "GetSubTree"); 160 mapper.append("/", 0, std::array<const char*, 1>{thermalControlIface}); 161 std::unordered_map< 162 std::string, std::unordered_map<std::string, std::vector<std::string>>> 163 respData; 164 165 try 166 { 167 auto resp = bus.call(mapper); 168 resp.read(respData); 169 } 170 catch (sdbusplus::exception_t&) 171 { 172 // can't do anything without mapper call data 173 throw std::runtime_error("ObjectMapper Call Failure"); 174 } 175 if (respData.empty()) 176 { 177 // if the user has profiles but doesn't expose the interface to select 178 // one, just go ahead without using profiles 179 return ret; 180 } 181 182 // assumption is that we should only have a small handful of selected 183 // profiles at a time (probably only 1), so calling each individually should 184 // not incur a large cost 185 for (const auto& objectPair : respData) 186 { 187 const std::string& path = objectPair.first; 188 for (const auto& ownerPair : objectPair.second) 189 { 190 const std::string& busName = ownerPair.first; 191 auto getProfile = 192 bus.new_method_call(busName.c_str(), path.c_str(), 193 "org.freedesktop.DBus.Properties", "Get"); 194 getProfile.append(thermalControlIface, "Current"); 195 std::variant<std::string> variantResp; 196 try 197 { 198 auto resp = bus.call(getProfile); 199 resp.read(variantResp); 200 } 201 catch (sdbusplus::exception_t&) 202 { 203 throw std::runtime_error("Failure getting profile"); 204 } 205 std::string mode = std::get<std::string>(variantResp); 206 ret.emplace_back(std::move(mode)); 207 } 208 } 209 if constexpr (DEBUG) 210 { 211 std::cout << "Profiles selected: "; 212 for (const auto& profile : ret) 213 { 214 std::cout << profile << " "; 215 } 216 std::cout << "\n"; 217 } 218 return ret; 219 } 220 221 int eventHandler(sd_bus_message*, void* context, sd_bus_error*) 222 { 223 224 if (context == nullptr) 225 { 226 throw std::runtime_error("Invalid match"); 227 } 228 boost::asio::steady_timer* timer = 229 static_cast<boost::asio::steady_timer*>(context); 230 231 // do a brief sleep as we tend to get a bunch of these events at 232 // once 233 timer->expires_after(std::chrono::seconds(2)); 234 timer->async_wait([](const boost::system::error_code ec) { 235 if (ec == boost::asio::error::operation_aborted) 236 { 237 /* another timer started*/ 238 return; 239 } 240 241 std::cout << "New configuration detected, reloading\n."; 242 restartControlLoops(); 243 }); 244 245 return 1; 246 } 247 248 void createMatches(sdbusplus::bus::bus& bus, boost::asio::steady_timer& timer) 249 { 250 // this is a list because the matches can't be moved 251 static std::list<sdbusplus::bus::match::match> matches; 252 253 const std::array<std::string, 4> interfaces = { 254 thermalControlIface, pidConfigurationInterface, 255 pidZoneConfigurationInterface, stepwiseConfigurationInterface}; 256 257 // this list only needs to be created once 258 if (!matches.empty()) 259 { 260 return; 261 } 262 263 // we restart when the configuration changes or there are new sensors 264 for (const auto& interface : interfaces) 265 { 266 matches.emplace_back( 267 bus, 268 "type='signal',member='PropertiesChanged',arg0namespace='" + 269 interface + "'", 270 eventHandler, &timer); 271 } 272 matches.emplace_back( 273 bus, 274 "type='signal',member='InterfacesAdded',arg0path='/xyz/openbmc_project/" 275 "sensors/'", 276 eventHandler, &timer); 277 } 278 279 bool init(sdbusplus::bus::bus& bus, boost::asio::steady_timer& timer) 280 { 281 282 sensorConfig.clear(); 283 zoneConfig.clear(); 284 zoneDetailsConfig.clear(); 285 286 createMatches(bus, timer); 287 288 using DbusVariantType = 289 std::variant<uint64_t, int64_t, double, std::string, 290 std::vector<std::string>, std::vector<double>>; 291 292 using ManagedObjectType = std::unordered_map< 293 sdbusplus::message::object_path, 294 std::unordered_map<std::string, 295 std::unordered_map<std::string, DbusVariantType>>>; 296 297 auto mapper = 298 bus.new_method_call("xyz.openbmc_project.ObjectMapper", 299 "/xyz/openbmc_project/object_mapper", 300 "xyz.openbmc_project.ObjectMapper", "GetSubTree"); 301 mapper.append("/", 0, 302 std::array<const char*, 6>{objectManagerInterface, 303 pidConfigurationInterface, 304 pidZoneConfigurationInterface, 305 stepwiseConfigurationInterface, 306 sensorInterface, pwmInterface}); 307 std::unordered_map< 308 std::string, std::unordered_map<std::string, std::vector<std::string>>> 309 respData; 310 try 311 { 312 auto resp = bus.call(mapper); 313 resp.read(respData); 314 } 315 catch (sdbusplus::exception_t&) 316 { 317 // can't do anything without mapper call data 318 throw std::runtime_error("ObjectMapper Call Failure"); 319 } 320 321 if (respData.empty()) 322 { 323 // can't do anything without mapper call data 324 throw std::runtime_error("No configuration data available from Mapper"); 325 } 326 // create a map of pair of <has pid configuration, ObjectManager path> 327 std::unordered_map<std::string, std::pair<bool, std::string>> owners; 328 // and a map of <path, interface> for sensors 329 std::unordered_map<std::string, std::string> sensors; 330 for (const auto& objectPair : respData) 331 { 332 for (const auto& ownerPair : objectPair.second) 333 { 334 auto& owner = owners[ownerPair.first]; 335 for (const std::string& interface : ownerPair.second) 336 { 337 338 if (interface == objectManagerInterface) 339 { 340 owner.second = objectPair.first; 341 } 342 if (interface == pidConfigurationInterface || 343 interface == pidZoneConfigurationInterface || 344 interface == stepwiseConfigurationInterface) 345 { 346 owner.first = true; 347 } 348 if (interface == sensorInterface || interface == pwmInterface) 349 { 350 // we're not interested in pwm sensors, just pwm control 351 if (interface == sensorInterface && 352 objectPair.first.find("pwm") != std::string::npos) 353 { 354 continue; 355 } 356 sensors[objectPair.first] = interface; 357 } 358 } 359 } 360 } 361 ManagedObjectType configurations; 362 for (const auto& owner : owners) 363 { 364 // skip if no pid configuration (means probably a sensor) 365 if (!owner.second.first) 366 { 367 continue; 368 } 369 auto endpoint = bus.new_method_call( 370 owner.first.c_str(), owner.second.second.c_str(), 371 "org.freedesktop.DBus.ObjectManager", "GetManagedObjects"); 372 ManagedObjectType configuration; 373 try 374 { 375 auto responce = bus.call(endpoint); 376 responce.read(configuration); 377 } 378 catch (sdbusplus::exception_t&) 379 { 380 // this shouldn't happen, probably means daemon crashed 381 throw std::runtime_error("Error getting managed objects from " + 382 owner.first); 383 } 384 385 for (auto& pathPair : configuration) 386 { 387 if (pathPair.second.find(pidConfigurationInterface) != 388 pathPair.second.end() || 389 pathPair.second.find(pidZoneConfigurationInterface) != 390 pathPair.second.end() || 391 pathPair.second.find(stepwiseConfigurationInterface) != 392 pathPair.second.end()) 393 { 394 configurations.emplace(pathPair); 395 } 396 } 397 } 398 399 // remove controllers from config that aren't in the current profile(s) 400 std::vector<std::string> selectedProfiles = getSelectedProfiles(bus); 401 if (selectedProfiles.size()) 402 { 403 for (auto pathIt = configurations.begin(); 404 pathIt != configurations.end();) 405 { 406 for (auto confIt = pathIt->second.begin(); 407 confIt != pathIt->second.end();) 408 { 409 auto profilesFind = confIt->second.find("Profiles"); 410 if (profilesFind == confIt->second.end()) 411 { 412 confIt++; 413 continue; // if no profiles selected, apply always 414 } 415 auto profiles = 416 std::get<std::vector<std::string>>(profilesFind->second); 417 if (profiles.empty()) 418 { 419 confIt++; 420 continue; 421 } 422 423 bool found = false; 424 for (const std::string& profile : profiles) 425 { 426 if (std::find(selectedProfiles.begin(), 427 selectedProfiles.end(), 428 profile) != selectedProfiles.end()) 429 { 430 found = true; 431 break; 432 } 433 } 434 if (found) 435 { 436 confIt++; 437 } 438 else 439 { 440 confIt = pathIt->second.erase(confIt); 441 } 442 } 443 if (pathIt->second.empty()) 444 { 445 pathIt = configurations.erase(pathIt); 446 } 447 else 448 { 449 pathIt++; 450 } 451 } 452 } 453 454 // on dbus having an index field is a bit strange, so randomly 455 // assign index based on name property 456 std::vector<std::string> foundZones; 457 for (const auto& configuration : configurations) 458 { 459 auto findZone = 460 configuration.second.find(pidZoneConfigurationInterface); 461 if (findZone != configuration.second.end()) 462 { 463 const auto& zone = findZone->second; 464 465 const std::string& name = std::get<std::string>(zone.at("Name")); 466 size_t index = getZoneIndex(name, foundZones); 467 468 auto& details = zoneDetailsConfig[index]; 469 details.minThermalOutput = std::visit(VariantToDoubleVisitor(), 470 zone.at("MinThermalOutput")); 471 details.failsafePercent = std::visit(VariantToDoubleVisitor(), 472 zone.at("FailSafePercent")); 473 } 474 auto findBase = configuration.second.find(pidConfigurationInterface); 475 if (findBase != configuration.second.end()) 476 { 477 478 const auto& base = 479 configuration.second.at(pidConfigurationInterface); 480 const std::vector<std::string>& zones = 481 std::get<std::vector<std::string>>(base.at("Zones")); 482 for (const std::string& zone : zones) 483 { 484 size_t index = getZoneIndex(zone, foundZones); 485 conf::PIDConf& conf = zoneConfig[index]; 486 487 std::vector<std::string> sensorNames = 488 std::get<std::vector<std::string>>(base.at("Inputs")); 489 auto findOutputs = 490 base.find("Outputs"); // currently only fans have outputs 491 if (findOutputs != base.end()) 492 { 493 std::vector<std::string> outputs = 494 std::get<std::vector<std::string>>(findOutputs->second); 495 sensorNames.insert(sensorNames.end(), outputs.begin(), 496 outputs.end()); 497 } 498 499 std::vector<std::string> inputs; 500 std::vector<std::pair<std::string, std::string>> 501 sensorInterfaces; 502 for (const std::string& sensorName : sensorNames) 503 { 504 std::string name = sensorName; 505 // replace spaces with underscores to be legal on dbus 506 std::replace(name.begin(), name.end(), ' ', '_'); 507 findSensors(sensors, name, sensorInterfaces); 508 } 509 510 for (const auto& sensorPathIfacePair : sensorInterfaces) 511 { 512 513 if (sensorPathIfacePair.second == sensorInterface) 514 { 515 size_t idx = 516 sensorPathIfacePair.first.find_last_of("/") + 1; 517 std::string shortName = 518 sensorPathIfacePair.first.substr(idx); 519 520 inputs.push_back(shortName); 521 auto& config = sensorConfig[shortName]; 522 config.type = std::get<std::string>(base.at("Class")); 523 config.readPath = sensorPathIfacePair.first; 524 // todo: maybe un-hardcode this if we run into slower 525 // timeouts with sensors 526 if (config.type == "temp") 527 { 528 config.timeout = 0; 529 } 530 } 531 else if (sensorPathIfacePair.second == pwmInterface) 532 { 533 // copy so we can modify it 534 for (std::string otherSensor : sensorNames) 535 { 536 std::replace(otherSensor.begin(), otherSensor.end(), 537 ' ', '_'); 538 if (sensorPathIfacePair.first.find(otherSensor) != 539 std::string::npos) 540 { 541 continue; 542 } 543 544 auto& config = sensorConfig[otherSensor]; 545 config.writePath = sensorPathIfacePair.first; 546 // todo: un-hardcode this if there are fans with 547 // different ranges 548 config.max = 255; 549 config.min = 0; 550 } 551 } 552 } 553 554 // if the sensors aren't available in the current state, don't 555 // add them to the configuration. 556 if (inputs.empty()) 557 { 558 continue; 559 } 560 561 struct conf::ControllerInfo& info = 562 conf[std::get<std::string>(base.at("Name"))]; 563 info.inputs = std::move(inputs); 564 565 info.type = std::get<std::string>(base.at("Class")); 566 // todo: auto generation yaml -> c script seems to discard this 567 // value for fans, verify this is okay 568 if (info.type == "fan") 569 { 570 info.setpoint = 0; 571 } 572 else 573 { 574 info.setpoint = std::visit(VariantToDoubleVisitor(), 575 base.at("SetPoint")); 576 } 577 info.pidInfo.ts = 1.0; // currently unused 578 info.pidInfo.proportionalCoeff = std::visit( 579 VariantToDoubleVisitor(), base.at("PCoefficient")); 580 info.pidInfo.integralCoeff = std::visit( 581 VariantToDoubleVisitor(), base.at("ICoefficient")); 582 info.pidInfo.feedFwdOffset = std::visit( 583 VariantToDoubleVisitor(), base.at("FFOffCoefficient")); 584 info.pidInfo.feedFwdGain = std::visit( 585 VariantToDoubleVisitor(), base.at("FFGainCoefficient")); 586 info.pidInfo.integralLimit.max = 587 std::visit(VariantToDoubleVisitor(), base.at("ILimitMax")); 588 info.pidInfo.integralLimit.min = 589 std::visit(VariantToDoubleVisitor(), base.at("ILimitMin")); 590 info.pidInfo.outLim.max = std::visit(VariantToDoubleVisitor(), 591 base.at("OutLimitMax")); 592 info.pidInfo.outLim.min = std::visit(VariantToDoubleVisitor(), 593 base.at("OutLimitMin")); 594 info.pidInfo.slewNeg = 595 std::visit(VariantToDoubleVisitor(), base.at("SlewNeg")); 596 info.pidInfo.slewPos = 597 std::visit(VariantToDoubleVisitor(), base.at("SlewPos")); 598 double negativeHysteresis = 0; 599 double positiveHysteresis = 0; 600 601 auto findNeg = base.find("NegativeHysteresis"); 602 auto findPos = base.find("PositiveHysteresis"); 603 if (findNeg != base.end()) 604 { 605 negativeHysteresis = 606 std::visit(VariantToDoubleVisitor(), findNeg->second); 607 } 608 609 if (findPos != base.end()) 610 { 611 positiveHysteresis = 612 std::visit(VariantToDoubleVisitor(), findPos->second); 613 } 614 info.pidInfo.negativeHysteresis = negativeHysteresis; 615 info.pidInfo.positiveHysteresis = positiveHysteresis; 616 } 617 } 618 auto findStepwise = 619 configuration.second.find(stepwiseConfigurationInterface); 620 if (findStepwise != configuration.second.end()) 621 { 622 const auto& base = findStepwise->second; 623 const std::vector<std::string>& zones = 624 std::get<std::vector<std::string>>(base.at("Zones")); 625 for (const std::string& zone : zones) 626 { 627 size_t index = getZoneIndex(zone, foundZones); 628 conf::PIDConf& conf = zoneConfig[index]; 629 630 std::vector<std::string> inputs; 631 std::vector<std::string> sensorNames = 632 std::get<std::vector<std::string>>(base.at("Inputs")); 633 634 bool sensorFound = false; 635 for (const std::string& sensorName : sensorNames) 636 { 637 std::string name = sensorName; 638 // replace spaces with underscores to be legal on dbus 639 std::replace(name.begin(), name.end(), ' ', '_'); 640 std::vector<std::pair<std::string, std::string>> 641 sensorPathIfacePairs; 642 643 if (!findSensors(sensors, name, sensorPathIfacePairs)) 644 { 645 break; 646 } 647 648 for (const auto& sensorPathIfacePair : sensorPathIfacePairs) 649 { 650 size_t idx = 651 sensorPathIfacePair.first.find_last_of("/") + 1; 652 std::string shortName = 653 sensorPathIfacePair.first.substr(idx); 654 655 inputs.push_back(shortName); 656 auto& config = sensorConfig[shortName]; 657 config.readPath = sensorPathIfacePair.first; 658 config.type = "temp"; 659 // todo: maybe un-hardcode this if we run into slower 660 // timeouts with sensors 661 662 config.timeout = 0; 663 sensorFound = true; 664 } 665 } 666 if (!sensorFound) 667 { 668 continue; 669 } 670 struct conf::ControllerInfo& info = 671 conf[std::get<std::string>(base.at("Name"))]; 672 info.inputs = std::move(inputs); 673 674 info.type = "stepwise"; 675 info.stepwiseInfo.ts = 1.0; // currently unused 676 info.stepwiseInfo.positiveHysteresis = 0.0; 677 info.stepwiseInfo.negativeHysteresis = 0.0; 678 std::string subtype = std::get<std::string>(base.at("Class")); 679 680 info.stepwiseInfo.isCeiling = (subtype == "Ceiling"); 681 auto findPosHyst = base.find("PositiveHysteresis"); 682 auto findNegHyst = base.find("NegativeHysteresis"); 683 if (findPosHyst != base.end()) 684 { 685 info.stepwiseInfo.positiveHysteresis = std::visit( 686 VariantToDoubleVisitor(), findPosHyst->second); 687 } 688 if (findNegHyst != base.end()) 689 { 690 info.stepwiseInfo.negativeHysteresis = std::visit( 691 VariantToDoubleVisitor(), findNegHyst->second); 692 } 693 std::vector<double> readings = 694 std::get<std::vector<double>>(base.at("Reading")); 695 if (readings.size() > ec::maxStepwisePoints) 696 { 697 throw std::invalid_argument("Too many stepwise points."); 698 } 699 if (readings.empty()) 700 { 701 throw std::invalid_argument( 702 "Must have one stepwise point."); 703 } 704 std::copy(readings.begin(), readings.end(), 705 info.stepwiseInfo.reading); 706 if (readings.size() < ec::maxStepwisePoints) 707 { 708 info.stepwiseInfo.reading[readings.size()] = 709 std::numeric_limits<double>::quiet_NaN(); 710 } 711 std::vector<double> outputs = 712 std::get<std::vector<double>>(base.at("Output")); 713 if (readings.size() != outputs.size()) 714 { 715 throw std::invalid_argument( 716 "Outputs size must match readings"); 717 } 718 std::copy(outputs.begin(), outputs.end(), 719 info.stepwiseInfo.output); 720 if (outputs.size() < ec::maxStepwisePoints) 721 { 722 info.stepwiseInfo.output[outputs.size()] = 723 std::numeric_limits<double>::quiet_NaN(); 724 } 725 } 726 } 727 } 728 if constexpr (DEBUG) 729 { 730 debugPrint(); 731 } 732 if (zoneConfig.empty() || zoneDetailsConfig.empty()) 733 { 734 std::cerr 735 << "No fan zones, application pausing until new configuration\n"; 736 return false; 737 } 738 return true; 739 } 740 } // namespace dbus_configuration 741