blob: eb816e8141eaef3e52caa2a62aef7ae9277808cc [file] [log] [blame]
James Feistbc896df2018-11-26 16:28:17 -08001/*
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
Ed Tanous8a57ec02020-10-09 12:46:52 -070017#include <ExitAirTempSensor.hpp>
18#include <Utils.hpp>
19#include <VariantVisitors.hpp>
James Feistbc896df2018-11-26 16:28:17 -080020#include <boost/algorithm/string/replace.hpp>
Patrick Venture96e97db2019-10-31 13:44:38 -070021#include <boost/container/flat_map.hpp>
James Feist38fb5982020-05-28 10:09:54 -070022#include <sdbusplus/asio/connection.hpp>
23#include <sdbusplus/asio/object_server.hpp>
24#include <sdbusplus/bus/match.hpp>
25
26#include <array>
James Feistbc896df2018-11-26 16:28:17 -080027#include <chrono>
Patrick Venture96e97db2019-10-31 13:44:38 -070028#include <cmath>
29#include <functional>
James Feistbc896df2018-11-26 16:28:17 -080030#include <iostream>
31#include <limits>
Patrick Venture96e97db2019-10-31 13:44:38 -070032#include <memory>
James Feistbc896df2018-11-26 16:28:17 -080033#include <numeric>
Patrick Venture96e97db2019-10-31 13:44:38 -070034#include <stdexcept>
35#include <utility>
36#include <variant>
James Feistbc896df2018-11-26 16:28:17 -080037#include <vector>
38
Ed Tanous8a57ec02020-10-09 12:46:52 -070039constexpr const double altitudeFactor = 1.14;
Zev Weiss054aad82022-08-18 01:37:34 -070040constexpr const char* exitAirType = "ExitAirTempSensor";
41constexpr const char* cfmType = "CFMSensor";
James Feistbc896df2018-11-26 16:28:17 -080042
43// todo: this *might* need to be configurable
44constexpr const char* inletTemperatureSensor = "temperature/Front_Panel_Temp";
James Feist13452092019-03-07 16:38:12 -080045constexpr const char* pidConfigurationType =
46 "xyz.openbmc_project.Configuration.Pid";
47constexpr const char* settingsDaemon = "xyz.openbmc_project.Settings";
48constexpr const char* cfmSettingPath = "/xyz/openbmc_project/control/cfm_limit";
49constexpr const char* cfmSettingIface = "xyz.openbmc_project.Control.CFMLimit";
James Feistbc896df2018-11-26 16:28:17 -080050
Ed Tanous8a57ec02020-10-09 12:46:52 -070051static constexpr bool debug = false;
James Feistbc896df2018-11-26 16:28:17 -080052
James Feistb2eb3f52018-12-04 16:17:50 -080053static constexpr double cfmMaxReading = 255;
54static constexpr double cfmMinReading = 0;
55
James Feist13452092019-03-07 16:38:12 -080056static constexpr size_t minSystemCfm = 50;
57
Zev Weiss054aad82022-08-18 01:37:34 -070058constexpr const auto monitorTypes{
59 std::to_array<const char*>({exitAirType, cfmType})};
James Feist655f3762020-10-05 15:28:15 -070060
James Feist9a25ed42019-10-15 15:43:44 -070061static std::vector<std::shared_ptr<CFMSensor>> cfmSensors;
62
James Feistb2eb3f52018-12-04 16:17:50 -080063static void setupSensorMatch(
Patrick Williams92f8f512022-07-22 19:26:55 -050064 std::vector<sdbusplus::bus::match_t>& matches, sdbusplus::bus_t& connection,
65 const std::string& type,
66 std::function<void(const double&, sdbusplus::message_t&)>&& callback)
James Feistb2eb3f52018-12-04 16:17:50 -080067{
68
Patrick Williams92f8f512022-07-22 19:26:55 -050069 std::function<void(sdbusplus::message_t & message)> eventHandler =
70 [callback{std::move(callback)}](sdbusplus::message_t& message) {
Ed Tanousbb679322022-05-16 16:10:00 -070071 std::string objectName;
72 boost::container::flat_map<std::string, std::variant<double, int64_t>>
73 values;
74 message.read(objectName, values);
75 auto findValue = values.find("Value");
76 if (findValue == values.end())
77 {
78 return;
79 }
80 double value = std::visit(VariantToDoubleVisitor(), findValue->second);
81 if (std::isnan(value))
82 {
83 return;
84 }
James Feist9566bfa2019-01-29 15:31:23 -080085
Ed Tanousbb679322022-05-16 16:10:00 -070086 callback(value, message);
87 };
James Feistb2eb3f52018-12-04 16:17:50 -080088 matches.emplace_back(connection,
89 "type='signal',"
90 "member='PropertiesChanged',interface='org."
91 "freedesktop.DBus.Properties',path_"
92 "namespace='/xyz/openbmc_project/sensors/" +
93 std::string(type) +
94 "',arg0='xyz.openbmc_project.Sensor.Value'",
95 std::move(eventHandler));
96}
97
James Feist13452092019-03-07 16:38:12 -080098static void setMaxPWM(const std::shared_ptr<sdbusplus::asio::connection>& conn,
99 double value)
100{
101 using GetSubTreeType = std::vector<std::pair<
102 std::string,
103 std::vector<std::pair<std::string, std::vector<std::string>>>>>;
104
105 conn->async_method_call(
106 [conn, value](const boost::system::error_code ec,
107 const GetSubTreeType& ret) {
Ed Tanousbb679322022-05-16 16:10:00 -0700108 if (ec)
109 {
110 std::cerr << "Error calling mapper\n";
111 return;
112 }
113 for (const auto& [path, objDict] : ret)
114 {
115 if (objDict.empty())
James Feist13452092019-03-07 16:38:12 -0800116 {
James Feist13452092019-03-07 16:38:12 -0800117 return;
118 }
Ed Tanousbb679322022-05-16 16:10:00 -0700119 const std::string& owner = objDict.begin()->first;
120
121 conn->async_method_call(
122 [conn, value, owner,
123 path{path}](const boost::system::error_code ec,
124 const std::variant<std::string>& classType) {
125 if (ec)
126 {
127 std::cerr << "Error getting pid class\n";
128 return;
129 }
Ed Tanous2049bd22022-07-09 07:20:26 -0700130 const auto* classStr = std::get_if<std::string>(&classType);
Ed Tanousbb679322022-05-16 16:10:00 -0700131 if (classStr == nullptr || *classStr != "fan")
James Feist13452092019-03-07 16:38:12 -0800132 {
133 return;
134 }
James Feist13452092019-03-07 16:38:12 -0800135 conn->async_method_call(
Ed Tanousbb679322022-05-16 16:10:00 -0700136 [](boost::system::error_code& ec) {
137 if (ec)
138 {
139 std::cerr << "Error setting pid class\n";
140 return;
141 }
James Feist13452092019-03-07 16:38:12 -0800142 },
Ed Tanousbb679322022-05-16 16:10:00 -0700143 owner, path, "org.freedesktop.DBus.Properties", "Set",
144 pidConfigurationType, "OutLimitMax",
145 std::variant<double>(value));
146 },
147 owner, path, "org.freedesktop.DBus.Properties", "Get",
148 pidConfigurationType, "Class");
149 }
James Feist13452092019-03-07 16:38:12 -0800150 },
James Feista5e58722019-04-22 14:43:11 -0700151 mapper::busName, mapper::path, mapper::interface, mapper::subtree, "/",
152 0, std::array<std::string, 1>{pidConfigurationType});
James Feist13452092019-03-07 16:38:12 -0800153}
154
James Feistb2eb3f52018-12-04 16:17:50 -0800155CFMSensor::CFMSensor(std::shared_ptr<sdbusplus::asio::connection>& conn,
156 const std::string& sensorName,
157 const std::string& sensorConfiguration,
158 sdbusplus::asio::object_server& objectServer,
James Feistb839c052019-05-15 10:25:24 -0700159 std::vector<thresholds::Threshold>&& thresholdData,
James Feistb2eb3f52018-12-04 16:17:50 -0800160 std::shared_ptr<ExitAirTempSensor>& parent) :
Zhikui Renda98f092021-11-01 09:41:08 -0700161 Sensor(escapeName(sensorName), std::move(thresholdData),
Zev Weiss054aad82022-08-18 01:37:34 -0700162 sensorConfiguration, "CFMSensor", false, false, cfmMaxReading,
163 cfmMinReading, conn, PowerState::on),
Ed Tanous2049bd22022-07-09 07:20:26 -0700164 parent(parent), objServer(objectServer)
James Feistb2eb3f52018-12-04 16:17:50 -0800165{
Basheer Ahmed Muddebihale5b867b2021-07-26 08:32:19 -0700166 sensorInterface = objectServer.add_interface(
167 "/xyz/openbmc_project/sensors/airflow/" + name,
168 "xyz.openbmc_project.Sensor.Value");
James Feistb2eb3f52018-12-04 16:17:50 -0800169
Jayashree Dhanapal56678082022-01-04 17:27:20 +0530170 for (const auto& threshold : thresholds)
James Feistb2eb3f52018-12-04 16:17:50 -0800171 {
Jayashree Dhanapal56678082022-01-04 17:27:20 +0530172 std::string interface = thresholds::getInterface(threshold.level);
173 thresholdInterfaces[static_cast<size_t>(threshold.level)] =
174 objectServer.add_interface(
175 "/xyz/openbmc_project/sensors/airflow/" + name, interface);
James Feistb2eb3f52018-12-04 16:17:50 -0800176 }
James Feist078f2322019-03-08 11:09:05 -0800177
178 association = objectServer.add_interface(
Basheer Ahmed Muddebihale5b867b2021-07-26 08:32:19 -0700179 "/xyz/openbmc_project/sensors/airflow/" + name, association::interface);
James Feist078f2322019-03-08 11:09:05 -0800180
Andrei Kartashev39287412022-02-04 16:04:47 +0300181 setInitialProperties(sensor_paths::unitCFM);
James Feist9a25ed42019-10-15 15:43:44 -0700182
James Feist13452092019-03-07 16:38:12 -0800183 pwmLimitIface =
184 objectServer.add_interface("/xyz/openbmc_project/control/pwm_limit",
185 "xyz.openbmc_project.Control.PWMLimit");
186 cfmLimitIface =
187 objectServer.add_interface("/xyz/openbmc_project/control/MaxCFM",
188 "xyz.openbmc_project.Control.CFMLimit");
James Feist9a25ed42019-10-15 15:43:44 -0700189}
James Feist13452092019-03-07 16:38:12 -0800190
James Feist9a25ed42019-10-15 15:43:44 -0700191void CFMSensor::setupMatches()
192{
193
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700194 std::weak_ptr<CFMSensor> weakRef = weak_from_this();
Ed Tanous8a17c302021-09-02 15:07:11 -0700195 setupSensorMatch(
196 matches, *dbusConnection, "fan_tach",
Patrick Williams92f8f512022-07-22 19:26:55 -0500197 [weakRef](const double& value, sdbusplus::message_t& message) {
Ed Tanousbb679322022-05-16 16:10:00 -0700198 auto self = weakRef.lock();
199 if (!self)
200 {
201 return;
202 }
203 self->tachReadings[message.get_path()] = value;
204 if (self->tachRanges.find(message.get_path()) == self->tachRanges.end())
205 {
206 // calls update reading after updating ranges
207 self->addTachRanges(message.get_sender(), message.get_path());
208 }
209 else
210 {
211 self->updateReading();
212 }
Ed Tanous8a17c302021-09-02 15:07:11 -0700213 });
James Feist9a25ed42019-10-15 15:43:44 -0700214
215 dbusConnection->async_method_call(
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700216 [weakRef](const boost::system::error_code ec,
217 const std::variant<double> cfmVariant) {
Ed Tanousbb679322022-05-16 16:10:00 -0700218 auto self = weakRef.lock();
219 if (!self)
220 {
221 return;
222 }
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700223
Ed Tanousbb679322022-05-16 16:10:00 -0700224 uint64_t maxRpm = 100;
225 if (!ec)
226 {
James Feist13452092019-03-07 16:38:12 -0800227
Ed Tanous2049bd22022-07-09 07:20:26 -0700228 const auto* cfm = std::get_if<double>(&cfmVariant);
Ed Tanousbb679322022-05-16 16:10:00 -0700229 if (cfm != nullptr && *cfm >= minSystemCfm)
230 {
231 maxRpm = self->getMaxRpm(*cfm);
James Feist13452092019-03-07 16:38:12 -0800232 }
Ed Tanousbb679322022-05-16 16:10:00 -0700233 }
234 self->pwmLimitIface->register_property("Limit", maxRpm);
235 self->pwmLimitIface->initialize();
236 setMaxPWM(self->dbusConnection, maxRpm);
James Feist13452092019-03-07 16:38:12 -0800237 },
238 settingsDaemon, cfmSettingPath, "org.freedesktop.DBus.Properties",
239 "Get", cfmSettingIface, "Limit");
240
Ed Tanousbb679322022-05-16 16:10:00 -0700241 matches.emplace_back(*dbusConnection,
242 "type='signal',"
243 "member='PropertiesChanged',interface='org."
244 "freedesktop.DBus.Properties',path='" +
245 std::string(cfmSettingPath) + "',arg0='" +
246 std::string(cfmSettingIface) + "'",
Patrick Williams92f8f512022-07-22 19:26:55 -0500247 [weakRef](sdbusplus::message_t& message) {
Ed Tanousbb679322022-05-16 16:10:00 -0700248 auto self = weakRef.lock();
249 if (!self)
250 {
251 return;
252 }
253 boost::container::flat_map<std::string, std::variant<double>> values;
254 std::string objectName;
255 message.read(objectName, values);
256 const auto findValue = values.find("Limit");
257 if (findValue == values.end())
258 {
259 return;
260 }
Ed Tanous2049bd22022-07-09 07:20:26 -0700261 auto* const reading = std::get_if<double>(&(findValue->second));
Ed Tanousbb679322022-05-16 16:10:00 -0700262 if (reading == nullptr)
263 {
264 std::cerr << "Got CFM Limit of wrong type\n";
265 return;
266 }
267 if (*reading < minSystemCfm && *reading != 0)
268 {
269 std::cerr << "Illegal CFM setting detected\n";
270 return;
271 }
272 uint64_t maxRpm = self->getMaxRpm(*reading);
273 self->pwmLimitIface->set_property("Limit", maxRpm);
274 setMaxPWM(self->dbusConnection, maxRpm);
275 });
James Feistb2eb3f52018-12-04 16:17:50 -0800276}
277
James Feist9566bfa2019-01-29 15:31:23 -0800278CFMSensor::~CFMSensor()
279{
Jayashree Dhanapal56678082022-01-04 17:27:20 +0530280 for (const auto& iface : thresholdInterfaces)
281 {
282 objServer.remove_interface(iface);
283 }
James Feist9566bfa2019-01-29 15:31:23 -0800284 objServer.remove_interface(sensorInterface);
James Feist078f2322019-03-08 11:09:05 -0800285 objServer.remove_interface(association);
James Feist13452092019-03-07 16:38:12 -0800286 objServer.remove_interface(cfmLimitIface);
287 objServer.remove_interface(pwmLimitIface);
288}
289
290void CFMSensor::createMaxCFMIface(void)
291{
James Feistb6c0b912019-07-09 12:21:44 -0700292 cfmLimitIface->register_property("Limit", c2 * maxCFM * tachs.size());
James Feist13452092019-03-07 16:38:12 -0800293 cfmLimitIface->initialize();
James Feist9566bfa2019-01-29 15:31:23 -0800294}
295
James Feistb2eb3f52018-12-04 16:17:50 -0800296void CFMSensor::addTachRanges(const std::string& serviceName,
297 const std::string& path)
298{
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700299 std::weak_ptr<CFMSensor> weakRef = weak_from_this();
James Feistb2eb3f52018-12-04 16:17:50 -0800300 dbusConnection->async_method_call(
Zev Weissafd15042022-07-18 12:28:40 -0700301 [weakRef, path](const boost::system::error_code ec,
302 const SensorBaseConfigMap& data) {
Ed Tanousbb679322022-05-16 16:10:00 -0700303 if (ec)
304 {
305 std::cerr << "Error getting properties from " << path << "\n";
306 return;
307 }
308 auto self = weakRef.lock();
309 if (!self)
310 {
311 return;
312 }
313 double max = loadVariant<double>(data, "MaxValue");
314 double min = loadVariant<double>(data, "MinValue");
315 self->tachRanges[path] = std::make_pair(min, max);
316 self->updateReading();
James Feistb2eb3f52018-12-04 16:17:50 -0800317 },
318 serviceName, path, "org.freedesktop.DBus.Properties", "GetAll",
319 "xyz.openbmc_project.Sensor.Value");
320}
321
322void CFMSensor::checkThresholds(void)
323{
324 thresholds::checkThresholds(this);
325}
326
327void CFMSensor::updateReading(void)
328{
329 double val = 0.0;
330 if (calculate(val))
331 {
332 if (value != val && parent)
333 {
334 parent->updateReading();
335 }
336 updateValue(val);
337 }
338 else
339 {
340 updateValue(std::numeric_limits<double>::quiet_NaN());
341 }
342}
343
Ed Tanous2049bd22022-07-09 07:20:26 -0700344uint64_t CFMSensor::getMaxRpm(uint64_t cfmMaxSetting) const
James Feist13452092019-03-07 16:38:12 -0800345{
346 uint64_t pwmPercent = 100;
347 double totalCFM = std::numeric_limits<double>::max();
348 if (cfmMaxSetting == 0)
349 {
350 return pwmPercent;
351 }
352
James Feist52427952019-04-05 14:23:35 -0700353 bool firstLoop = true;
James Feist13452092019-03-07 16:38:12 -0800354 while (totalCFM > cfmMaxSetting)
355 {
James Feist52427952019-04-05 14:23:35 -0700356 if (firstLoop)
357 {
358 firstLoop = false;
359 }
360 else
361 {
362 pwmPercent--;
363 }
364
James Feist13452092019-03-07 16:38:12 -0800365 double ci = 0;
366 if (pwmPercent == 0)
367 {
368 ci = 0;
369 }
370 else if (pwmPercent < tachMinPercent)
371 {
372 ci = c1;
373 }
374 else if (pwmPercent > tachMaxPercent)
375 {
376 ci = c2;
377 }
378 else
379 {
380 ci = c1 + (((c2 - c1) * (pwmPercent - tachMinPercent)) /
381 (tachMaxPercent - tachMinPercent));
382 }
383
384 // Now calculate the CFM for this tach
385 // CFMi = Ci * Qmaxi * TACHi
386 totalCFM = ci * maxCFM * pwmPercent;
387 totalCFM *= tachs.size();
388 // divide by 100 since pwm is in percent
389 totalCFM /= 100;
390
James Feist13452092019-03-07 16:38:12 -0800391 if (pwmPercent <= 0)
392 {
393 break;
394 }
395 }
James Feist52427952019-04-05 14:23:35 -0700396
James Feist13452092019-03-07 16:38:12 -0800397 return pwmPercent;
398}
399
James Feistb2eb3f52018-12-04 16:17:50 -0800400bool CFMSensor::calculate(double& value)
401{
402 double totalCFM = 0;
403 for (const std::string& tachName : tachs)
404 {
James Feist9566bfa2019-01-29 15:31:23 -0800405
James Feistb2eb3f52018-12-04 16:17:50 -0800406 auto findReading = std::find_if(
Zev Weiss6c106d62022-08-17 20:50:00 -0700407 tachReadings.begin(), tachReadings.end(),
408 [&](const auto& item) { return item.first.ends_with(tachName); });
Ed Tanousbb679322022-05-16 16:10:00 -0700409 auto findRange = std::find_if(tachRanges.begin(), tachRanges.end(),
410 [&](const auto& item) {
Zev Weiss6c106d62022-08-17 20:50:00 -0700411 return item.first.ends_with(tachName);
Ed Tanousbb679322022-05-16 16:10:00 -0700412 });
James Feistb2eb3f52018-12-04 16:17:50 -0800413 if (findReading == tachReadings.end())
414 {
Ed Tanous8a57ec02020-10-09 12:46:52 -0700415 if constexpr (debug)
James Feista96329f2019-01-24 10:08:27 -0800416 {
417 std::cerr << "Can't find " << tachName << "in readings\n";
418 }
James Feist9566bfa2019-01-29 15:31:23 -0800419 continue; // haven't gotten a reading
James Feistb2eb3f52018-12-04 16:17:50 -0800420 }
421
422 if (findRange == tachRanges.end())
423 {
James Feist523828e2019-03-04 14:38:37 -0800424 std::cerr << "Can't find " << tachName << " in ranges\n";
James Feistb2eb3f52018-12-04 16:17:50 -0800425 return false; // haven't gotten a max / min
426 }
427
428 // avoid divide by 0
429 if (findRange->second.second == 0)
430 {
431 std::cerr << "Tach Max Set to 0 " << tachName << "\n";
432 return false;
433 }
434
435 double rpm = findReading->second;
436
437 // for now assume the min for a fan is always 0, divide by max to get
438 // percent and mult by 100
439 rpm /= findRange->second.second;
440 rpm *= 100;
441
Ed Tanous8a57ec02020-10-09 12:46:52 -0700442 if constexpr (debug)
James Feistb2eb3f52018-12-04 16:17:50 -0800443 {
444 std::cout << "Tach " << tachName << "at " << rpm << "\n";
445 }
446
447 // Do a linear interpolation to get Ci
448 // Ci = C1 + (C2 - C1)/(RPM2 - RPM1) * (TACHi - TACH1)
449
450 double ci = 0;
451 if (rpm == 0)
452 {
453 ci = 0;
454 }
455 else if (rpm < tachMinPercent)
456 {
457 ci = c1;
458 }
459 else if (rpm > tachMaxPercent)
460 {
461 ci = c2;
462 }
463 else
464 {
465 ci = c1 + (((c2 - c1) * (rpm - tachMinPercent)) /
466 (tachMaxPercent - tachMinPercent));
467 }
468
469 // Now calculate the CFM for this tach
470 // CFMi = Ci * Qmaxi * TACHi
471 totalCFM += ci * maxCFM * rpm;
Ed Tanous8a57ec02020-10-09 12:46:52 -0700472 if constexpr (debug)
James Feista5e58722019-04-22 14:43:11 -0700473 {
474 std::cerr << "totalCFM = " << totalCFM << "\n";
475 std::cerr << "Ci " << ci << " MaxCFM " << maxCFM << " rpm " << rpm
476 << "\n";
477 std::cerr << "c1 " << c1 << " c2 " << c2 << " max "
478 << tachMaxPercent << " min " << tachMinPercent << "\n";
479 }
James Feistb2eb3f52018-12-04 16:17:50 -0800480 }
481
482 // divide by 100 since rpm is in percent
483 value = totalCFM / 100;
Ed Tanous8a57ec02020-10-09 12:46:52 -0700484 if constexpr (debug)
James Feista5e58722019-04-22 14:43:11 -0700485 {
486 std::cerr << "cfm value = " << value << "\n";
487 }
James Feist9566bfa2019-01-29 15:31:23 -0800488 return true;
James Feistb2eb3f52018-12-04 16:17:50 -0800489}
490
491static constexpr double exitAirMaxReading = 127;
492static constexpr double exitAirMinReading = -128;
James Feistbc896df2018-11-26 16:28:17 -0800493ExitAirTempSensor::ExitAirTempSensor(
494 std::shared_ptr<sdbusplus::asio::connection>& conn,
James Feistb2eb3f52018-12-04 16:17:50 -0800495 const std::string& sensorName, const std::string& sensorConfiguration,
James Feistbc896df2018-11-26 16:28:17 -0800496 sdbusplus::asio::object_server& objectServer,
James Feistb839c052019-05-15 10:25:24 -0700497 std::vector<thresholds::Threshold>&& thresholdData) :
Zhikui Renda98f092021-11-01 09:41:08 -0700498 Sensor(escapeName(sensorName), std::move(thresholdData),
Zev Weiss054aad82022-08-18 01:37:34 -0700499 sensorConfiguration, "ExitAirTemp", false, false, exitAirMaxReading,
500 exitAirMinReading, conn, PowerState::on),
Ed Tanous2049bd22022-07-09 07:20:26 -0700501 objServer(objectServer)
James Feistbc896df2018-11-26 16:28:17 -0800502{
503 sensorInterface = objectServer.add_interface(
504 "/xyz/openbmc_project/sensors/temperature/" + name,
505 "xyz.openbmc_project.Sensor.Value");
506
Jayashree Dhanapal56678082022-01-04 17:27:20 +0530507 for (const auto& threshold : thresholds)
James Feistbc896df2018-11-26 16:28:17 -0800508 {
Jayashree Dhanapal56678082022-01-04 17:27:20 +0530509 std::string interface = thresholds::getInterface(threshold.level);
510 thresholdInterfaces[static_cast<size_t>(threshold.level)] =
511 objectServer.add_interface(
512 "/xyz/openbmc_project/sensors/temperature/" + name, interface);
James Feistbc896df2018-11-26 16:28:17 -0800513 }
James Feist078f2322019-03-08 11:09:05 -0800514 association = objectServer.add_interface(
515 "/xyz/openbmc_project/sensors/temperature/" + name,
James Feist2adc95c2019-09-30 14:55:28 -0700516 association::interface);
Andrei Kartashev39287412022-02-04 16:04:47 +0300517 setInitialProperties(sensor_paths::unitDegreesC);
James Feistbc896df2018-11-26 16:28:17 -0800518}
519
520ExitAirTempSensor::~ExitAirTempSensor()
521{
Jayashree Dhanapal56678082022-01-04 17:27:20 +0530522 for (const auto& iface : thresholdInterfaces)
523 {
524 objServer.remove_interface(iface);
525 }
James Feist523828e2019-03-04 14:38:37 -0800526 objServer.remove_interface(sensorInterface);
James Feist078f2322019-03-08 11:09:05 -0800527 objServer.remove_interface(association);
James Feistbc896df2018-11-26 16:28:17 -0800528}
529
530void ExitAirTempSensor::setupMatches(void)
531{
Brandon Kim66558232021-11-09 16:53:08 -0800532 constexpr const auto matchTypes{
533 std::to_array<const char*>({"power", inletTemperatureSensor})};
James Feistbc896df2018-11-26 16:28:17 -0800534
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700535 std::weak_ptr<ExitAirTempSensor> weakRef = weak_from_this();
Ed Tanous13b63f82021-05-11 16:12:52 -0700536 for (const std::string type : matchTypes)
James Feistbc896df2018-11-26 16:28:17 -0800537 {
James Feistb2eb3f52018-12-04 16:17:50 -0800538 setupSensorMatch(matches, *dbusConnection, type,
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700539 [weakRef, type](const double& value,
Patrick Williams92f8f512022-07-22 19:26:55 -0500540 sdbusplus::message_t& message) {
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700541 auto self = weakRef.lock();
542 if (!self)
543 {
544 return;
545 }
Ed Tanousbb679322022-05-16 16:10:00 -0700546 if (type == "power")
547 {
548 std::string path = message.get_path();
549 if (path.find("PS") != std::string::npos &&
Zev Weiss6c106d62022-08-17 20:50:00 -0700550 path.ends_with("Input_Power"))
Ed Tanousbb679322022-05-16 16:10:00 -0700551 {
552 self->powerReadings[message.get_path()] = value;
553 }
554 }
555 else if (type == inletTemperatureSensor)
556 {
557 self->inletTemp = value;
558 }
559 self->updateReading();
560 });
561 }
562 dbusConnection->async_method_call(
563 [weakRef](boost::system::error_code ec,
564 const std::variant<double>& value) {
565 if (ec)
566 {
567 // sensor not ready yet
568 return;
569 }
570 auto self = weakRef.lock();
571 if (!self)
572 {
573 return;
574 }
575 self->inletTemp = std::visit(VariantToDoubleVisitor(), value);
James Feist9566bfa2019-01-29 15:31:23 -0800576 },
577 "xyz.openbmc_project.HwmonTempSensor",
578 std::string("/xyz/openbmc_project/sensors/") + inletTemperatureSensor,
James Feista5e58722019-04-22 14:43:11 -0700579 properties::interface, properties::get, sensorValueInterface, "Value");
580 dbusConnection->async_method_call(
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700581 [weakRef](boost::system::error_code ec, const GetSubTreeType& subtree) {
Ed Tanousbb679322022-05-16 16:10:00 -0700582 if (ec)
583 {
584 std::cerr << "Error contacting mapper\n";
585 return;
586 }
587 auto self = weakRef.lock();
588 if (!self)
589 {
590 return;
591 }
Zev Weiss72f322f2022-08-12 18:21:01 -0700592 for (const auto& [path, matches] : subtree)
Ed Tanousbb679322022-05-16 16:10:00 -0700593 {
Zev Weiss72f322f2022-08-12 18:21:01 -0700594 size_t lastSlash = path.rfind('/');
595 if (lastSlash == std::string::npos || lastSlash == path.size() ||
596 matches.empty())
James Feista5e58722019-04-22 14:43:11 -0700597 {
Ed Tanousbb679322022-05-16 16:10:00 -0700598 continue;
James Feista5e58722019-04-22 14:43:11 -0700599 }
Zev Weiss72f322f2022-08-12 18:21:01 -0700600 std::string sensorName = path.substr(lastSlash + 1);
Zev Weiss6c106d62022-08-17 20:50:00 -0700601 if (sensorName.starts_with("PS") &&
602 sensorName.ends_with("Input_Power"))
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700603 {
Zev Weiss72f322f2022-08-12 18:21:01 -0700604 // lambda capture requires a proper variable (not a structured
605 // binding)
606 const std::string& cbPath = path;
Ed Tanousbb679322022-05-16 16:10:00 -0700607 self->dbusConnection->async_method_call(
Zev Weiss72f322f2022-08-12 18:21:01 -0700608 [weakRef, cbPath](boost::system::error_code ec,
609 const std::variant<double>& value) {
Ed Tanousbb679322022-05-16 16:10:00 -0700610 if (ec)
611 {
Zev Weiss72f322f2022-08-12 18:21:01 -0700612 std::cerr << "Error getting value from " << cbPath
Ed Tanousbb679322022-05-16 16:10:00 -0700613 << "\n";
614 }
615 auto self = weakRef.lock();
616 if (!self)
617 {
618 return;
619 }
620 double reading =
621 std::visit(VariantToDoubleVisitor(), value);
622 if constexpr (debug)
623 {
Zev Weiss72f322f2022-08-12 18:21:01 -0700624 std::cerr << cbPath << "Reading " << reading << "\n";
Ed Tanousbb679322022-05-16 16:10:00 -0700625 }
Zev Weiss72f322f2022-08-12 18:21:01 -0700626 self->powerReadings[cbPath] = reading;
Ed Tanousbb679322022-05-16 16:10:00 -0700627 },
Zev Weiss72f322f2022-08-12 18:21:01 -0700628 matches[0].first, cbPath, properties::interface,
Ed Tanousbb679322022-05-16 16:10:00 -0700629 properties::get, sensorValueInterface, "Value");
Zhikui Rendbb73aa2021-04-02 13:39:04 -0700630 }
Ed Tanousbb679322022-05-16 16:10:00 -0700631 }
James Feista5e58722019-04-22 14:43:11 -0700632 },
633 mapper::busName, mapper::path, mapper::interface, mapper::subtree,
634 "/xyz/openbmc_project/sensors/power", 0,
635 std::array<const char*, 1>{sensorValueInterface});
James Feistbc896df2018-11-26 16:28:17 -0800636}
637
638void ExitAirTempSensor::updateReading(void)
639{
640
641 double val = 0.0;
642 if (calculate(val))
643 {
James Feist18af4232019-03-13 11:14:00 -0700644 val = std::floor(val + 0.5);
James Feistbc896df2018-11-26 16:28:17 -0800645 updateValue(val);
646 }
647 else
648 {
649 updateValue(std::numeric_limits<double>::quiet_NaN());
650 }
651}
652
James Feistb2eb3f52018-12-04 16:17:50 -0800653double ExitAirTempSensor::getTotalCFM(void)
James Feistbc896df2018-11-26 16:28:17 -0800654{
James Feistb2eb3f52018-12-04 16:17:50 -0800655 double sum = 0;
656 for (auto& sensor : cfmSensors)
James Feistbc896df2018-11-26 16:28:17 -0800657 {
James Feistb2eb3f52018-12-04 16:17:50 -0800658 double reading = 0;
659 if (!sensor->calculate(reading))
James Feistbc896df2018-11-26 16:28:17 -0800660 {
James Feistbc896df2018-11-26 16:28:17 -0800661 return -1;
662 }
James Feistb2eb3f52018-12-04 16:17:50 -0800663 sum += reading;
James Feistbc896df2018-11-26 16:28:17 -0800664 }
James Feistb2eb3f52018-12-04 16:17:50 -0800665
666 return sum;
James Feistbc896df2018-11-26 16:28:17 -0800667}
668
669bool ExitAirTempSensor::calculate(double& val)
670{
Zhikui Ren12e3d672020-12-03 15:14:49 -0800671 constexpr size_t maxErrorPrint = 5;
James Feistbc896df2018-11-26 16:28:17 -0800672 static bool firstRead = false;
James Feistae11cfc2019-05-07 15:01:20 -0700673 static size_t errorPrint = maxErrorPrint;
674
James Feistbc896df2018-11-26 16:28:17 -0800675 double cfm = getTotalCFM();
676 if (cfm <= 0)
677 {
678 std::cerr << "Error getting cfm\n";
679 return false;
680 }
681
Zhikui Ren12e3d672020-12-03 15:14:49 -0800682 // Though cfm is not expected to be less than qMin normally,
683 // it is not a hard limit for exit air temp calculation.
684 // 50% qMin is chosen as a generic limit between providing
685 // a valid derived exit air temp and reporting exit air temp not available.
686 constexpr const double cfmLimitFactor = 0.5;
687 if (cfm < (qMin * cfmLimitFactor))
688 {
689 if (errorPrint > 0)
690 {
691 errorPrint--;
692 std::cerr << "cfm " << cfm << " is too low, expected qMin " << qMin
693 << "\n";
694 }
695 val = 0;
696 return false;
697 }
698
James Feistbc896df2018-11-26 16:28:17 -0800699 // if there is an error getting inlet temp, return error
700 if (std::isnan(inletTemp))
701 {
James Feistae11cfc2019-05-07 15:01:20 -0700702 if (errorPrint > 0)
703 {
704 errorPrint--;
705 std::cerr << "Cannot get inlet temp\n";
706 }
James Feistbc896df2018-11-26 16:28:17 -0800707 val = 0;
708 return false;
709 }
710
711 // if fans are off, just make the exit temp equal to inlet
James Feist71d31b22019-01-02 16:57:54 -0800712 if (!isPowerOn())
James Feistbc896df2018-11-26 16:28:17 -0800713 {
714 val = inletTemp;
715 return true;
716 }
717
718 double totalPower = 0;
Zev Weiss72f322f2022-08-12 18:21:01 -0700719 for (const auto& [path, reading] : powerReadings)
James Feistbc896df2018-11-26 16:28:17 -0800720 {
Zev Weiss72f322f2022-08-12 18:21:01 -0700721 if (std::isnan(reading))
James Feistbc896df2018-11-26 16:28:17 -0800722 {
723 continue;
724 }
Zev Weiss72f322f2022-08-12 18:21:01 -0700725 totalPower += reading;
James Feistbc896df2018-11-26 16:28:17 -0800726 }
727
728 // Calculate power correction factor
729 // Ci = CL + (CH - CL)/(QMax - QMin) * (CFM - QMin)
Ed Tanous8a57ec02020-10-09 12:46:52 -0700730 double powerFactor = 0.0;
James Feistbc896df2018-11-26 16:28:17 -0800731 if (cfm <= qMin)
732 {
733 powerFactor = powerFactorMin;
734 }
735 else if (cfm >= qMax)
736 {
737 powerFactor = powerFactorMax;
738 }
739 else
740 {
741 powerFactor = powerFactorMin + ((powerFactorMax - powerFactorMin) /
742 (qMax - qMin) * (cfm - qMin));
743 }
744
Ed Tanous8a57ec02020-10-09 12:46:52 -0700745 totalPower *= powerFactor;
James Feistbc896df2018-11-26 16:28:17 -0800746 totalPower += pOffset;
747
748 if (totalPower == 0)
749 {
James Feistae11cfc2019-05-07 15:01:20 -0700750 if (errorPrint > 0)
751 {
752 errorPrint--;
753 std::cerr << "total power 0\n";
754 }
James Feistbc896df2018-11-26 16:28:17 -0800755 val = 0;
756 return false;
757 }
758
Ed Tanous8a57ec02020-10-09 12:46:52 -0700759 if constexpr (debug)
James Feistbc896df2018-11-26 16:28:17 -0800760 {
761 std::cout << "Power Factor " << powerFactor << "\n";
762 std::cout << "Inlet Temp " << inletTemp << "\n";
763 std::cout << "Total Power" << totalPower << "\n";
764 }
765
766 // Calculate the exit air temp
767 // Texit = Tfp + (1.76 * TotalPower / CFM * Faltitude)
Ed Tanous8a57ec02020-10-09 12:46:52 -0700768 double reading = 1.76 * totalPower * altitudeFactor;
James Feistbc896df2018-11-26 16:28:17 -0800769 reading /= cfm;
770 reading += inletTemp;
771
Ed Tanous8a57ec02020-10-09 12:46:52 -0700772 if constexpr (debug)
James Feistbc896df2018-11-26 16:28:17 -0800773 {
774 std::cout << "Reading 1: " << reading << "\n";
775 }
776
777 // Now perform the exponential average
778 // Calculate alpha based on SDR values and CFM
779 // Ai = As + (Af - As)/(QMax - QMin) * (CFM - QMin)
780
781 double alpha = 0.0;
782 if (cfm < qMin)
783 {
784 alpha = alphaS;
785 }
786 else if (cfm >= qMax)
787 {
788 alpha = alphaF;
789 }
790 else
791 {
792 alpha = alphaS + ((alphaF - alphaS) * (cfm - qMin) / (qMax - qMin));
793 }
794
Zhikui Ren12e3d672020-12-03 15:14:49 -0800795 auto time = std::chrono::steady_clock::now();
James Feistbc896df2018-11-26 16:28:17 -0800796 if (!firstRead)
797 {
798 firstRead = true;
799 lastTime = time;
800 lastReading = reading;
801 }
802 double alphaDT =
803 std::chrono::duration_cast<std::chrono::seconds>(time - lastTime)
804 .count() *
805 alpha;
806
807 // cap at 1.0 or the below fails
808 if (alphaDT > 1.0)
809 {
810 alphaDT = 1.0;
811 }
812
Ed Tanous8a57ec02020-10-09 12:46:52 -0700813 if constexpr (debug)
James Feistbc896df2018-11-26 16:28:17 -0800814 {
815 std::cout << "AlphaDT: " << alphaDT << "\n";
816 }
817
818 reading = ((reading * alphaDT) + (lastReading * (1.0 - alphaDT)));
819
Ed Tanous8a57ec02020-10-09 12:46:52 -0700820 if constexpr (debug)
James Feistbc896df2018-11-26 16:28:17 -0800821 {
822 std::cout << "Reading 2: " << reading << "\n";
823 }
824
825 val = reading;
826 lastReading = reading;
827 lastTime = time;
James Feistae11cfc2019-05-07 15:01:20 -0700828 errorPrint = maxErrorPrint;
James Feistbc896df2018-11-26 16:28:17 -0800829 return true;
830}
831
832void ExitAirTempSensor::checkThresholds(void)
833{
834 thresholds::checkThresholds(this);
835}
836
Zev Weissafd15042022-07-18 12:28:40 -0700837static void loadVariantPathArray(const SensorBaseConfigMap& data,
838 const std::string& key,
839 std::vector<std::string>& resp)
James Feistbc896df2018-11-26 16:28:17 -0800840{
841 auto it = data.find(key);
842 if (it == data.end())
843 {
844 std::cerr << "Configuration missing " << key << "\n";
845 throw std::invalid_argument("Key Missing");
846 }
847 BasicVariantType copy = it->second;
James Feist3eb82622019-02-08 13:10:22 -0800848 std::vector<std::string> config = std::get<std::vector<std::string>>(copy);
James Feistbc896df2018-11-26 16:28:17 -0800849 for (auto& str : config)
850 {
851 boost::replace_all(str, " ", "_");
852 }
853 resp = std::move(config);
854}
855
856void createSensor(sdbusplus::asio::object_server& objectServer,
James Feistb2eb3f52018-12-04 16:17:50 -0800857 std::shared_ptr<ExitAirTempSensor>& exitAirSensor,
James Feistbc896df2018-11-26 16:28:17 -0800858 std::shared_ptr<sdbusplus::asio::connection>& dbusConnection)
859{
860 if (!dbusConnection)
861 {
862 std::cerr << "Connection not created\n";
863 return;
864 }
James Feist655f3762020-10-05 15:28:15 -0700865 auto getter = std::make_shared<GetSensorConfiguration>(
Ed Tanousbb679322022-05-16 16:10:00 -0700866 dbusConnection,
867 [&objectServer, &dbusConnection,
868 &exitAirSensor](const ManagedObjectType& resp) {
869 cfmSensors.clear();
Zev Weiss72f322f2022-08-12 18:21:01 -0700870 for (const auto& [path, interfaces] : resp)
Ed Tanousbb679322022-05-16 16:10:00 -0700871 {
Zev Weiss72f322f2022-08-12 18:21:01 -0700872 for (const auto& [intf, cfg] : interfaces)
James Feistbc896df2018-11-26 16:28:17 -0800873 {
Zev Weiss054aad82022-08-18 01:37:34 -0700874 if (intf == configInterfaceName(exitAirType))
James Feistbc896df2018-11-26 16:28:17 -0800875 {
Ed Tanousbb679322022-05-16 16:10:00 -0700876 // thresholds should be under the same path
877 std::vector<thresholds::Threshold> sensorThresholds;
Zev Weiss72f322f2022-08-12 18:21:01 -0700878 parseThresholdsFromConfig(interfaces, sensorThresholds);
James Feistbc896df2018-11-26 16:28:17 -0800879
Zev Weiss72f322f2022-08-12 18:21:01 -0700880 std::string name = loadVariant<std::string>(cfg, "Name");
Ed Tanousbb679322022-05-16 16:10:00 -0700881 exitAirSensor = std::make_shared<ExitAirTempSensor>(
Zev Weiss72f322f2022-08-12 18:21:01 -0700882 dbusConnection, name, path.str, objectServer,
Ed Tanousbb679322022-05-16 16:10:00 -0700883 std::move(sensorThresholds));
884 exitAirSensor->powerFactorMin =
Zev Weiss72f322f2022-08-12 18:21:01 -0700885 loadVariant<double>(cfg, "PowerFactorMin");
Ed Tanousbb679322022-05-16 16:10:00 -0700886 exitAirSensor->powerFactorMax =
Zev Weiss72f322f2022-08-12 18:21:01 -0700887 loadVariant<double>(cfg, "PowerFactorMax");
888 exitAirSensor->qMin = loadVariant<double>(cfg, "QMin");
889 exitAirSensor->qMax = loadVariant<double>(cfg, "QMax");
890 exitAirSensor->alphaS = loadVariant<double>(cfg, "AlphaS");
891 exitAirSensor->alphaF = loadVariant<double>(cfg, "AlphaF");
Ed Tanousbb679322022-05-16 16:10:00 -0700892 }
Zev Weiss054aad82022-08-18 01:37:34 -0700893 else if (intf == configInterfaceName(cfmType))
Ed Tanousbb679322022-05-16 16:10:00 -0700894 {
895 // thresholds should be under the same path
896 std::vector<thresholds::Threshold> sensorThresholds;
Zev Weiss72f322f2022-08-12 18:21:01 -0700897 parseThresholdsFromConfig(interfaces, sensorThresholds);
898 std::string name = loadVariant<std::string>(cfg, "Name");
Ed Tanousbb679322022-05-16 16:10:00 -0700899 auto sensor = std::make_shared<CFMSensor>(
Zev Weiss72f322f2022-08-12 18:21:01 -0700900 dbusConnection, name, path.str, objectServer,
Ed Tanousbb679322022-05-16 16:10:00 -0700901 std::move(sensorThresholds), exitAirSensor);
Zev Weiss72f322f2022-08-12 18:21:01 -0700902 loadVariantPathArray(cfg, "Tachs", sensor->tachs);
903 sensor->maxCFM = loadVariant<double>(cfg, "MaxCFM");
James Feistbc896df2018-11-26 16:28:17 -0800904
Ed Tanousbb679322022-05-16 16:10:00 -0700905 // change these into percent upon getting the data
Zev Weiss72f322f2022-08-12 18:21:01 -0700906 sensor->c1 = loadVariant<double>(cfg, "C1") / 100;
907 sensor->c2 = loadVariant<double>(cfg, "C2") / 100;
Ed Tanousbb679322022-05-16 16:10:00 -0700908 sensor->tachMinPercent =
Zev Weiss72f322f2022-08-12 18:21:01 -0700909 loadVariant<double>(cfg, "TachMinPercent");
Ed Tanousbb679322022-05-16 16:10:00 -0700910 sensor->tachMaxPercent =
Zev Weiss72f322f2022-08-12 18:21:01 -0700911 loadVariant<double>(cfg, "TachMaxPercent");
Ed Tanousbb679322022-05-16 16:10:00 -0700912 sensor->createMaxCFMIface();
913 sensor->setupMatches();
James Feistbc896df2018-11-26 16:28:17 -0800914
Ed Tanousbb679322022-05-16 16:10:00 -0700915 cfmSensors.emplace_back(std::move(sensor));
James Feistbc896df2018-11-26 16:28:17 -0800916 }
917 }
Ed Tanousbb679322022-05-16 16:10:00 -0700918 }
919 if (exitAirSensor)
920 {
921 exitAirSensor->setupMatches();
922 exitAirSensor->updateReading();
923 }
Ed Tanous8a17c302021-09-02 15:07:11 -0700924 });
James Feist655f3762020-10-05 15:28:15 -0700925 getter->getConfiguration(
Zev Weiss054aad82022-08-18 01:37:34 -0700926 std::vector<std::string>(monitorTypes.begin(), monitorTypes.end()));
James Feistbc896df2018-11-26 16:28:17 -0800927}
928
James Feistb6c0b912019-07-09 12:21:44 -0700929int main()
James Feistbc896df2018-11-26 16:28:17 -0800930{
931
932 boost::asio::io_service io;
933 auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
934 systemBus->request_name("xyz.openbmc_project.ExitAirTempSensor");
935 sdbusplus::asio::object_server objectServer(systemBus);
936 std::shared_ptr<ExitAirTempSensor> sensor =
937 nullptr; // wait until we find the config
James Feistbc896df2018-11-26 16:28:17 -0800938
939 io.post([&]() { createSensor(objectServer, sensor, systemBus); });
940
Ed Tanous9b4a20e2022-09-06 08:47:11 -0700941 boost::asio::steady_timer configTimer(io);
James Feistbc896df2018-11-26 16:28:17 -0800942
Patrick Williams92f8f512022-07-22 19:26:55 -0500943 std::function<void(sdbusplus::message_t&)> eventHandler =
944 [&](sdbusplus::message_t&) {
Ed Tanous9b4a20e2022-09-06 08:47:11 -0700945 configTimer.expires_from_now(std::chrono::seconds(1));
Ed Tanousbb679322022-05-16 16:10:00 -0700946 // create a timer because normally multiple properties change
947 configTimer.async_wait([&](const boost::system::error_code& ec) {
948 if (ec == boost::asio::error::operation_aborted)
949 {
950 return; // we're being canceled
951 }
952 createSensor(objectServer, sensor, systemBus);
953 if (!sensor)
954 {
955 std::cout << "Configuration not detected\n";
956 }
957 });
958 };
Zev Weiss214d9712022-08-12 12:54:31 -0700959 std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
Zev Weiss054aad82022-08-18 01:37:34 -0700960 setupPropertiesChangedMatches(*systemBus, monitorTypes, eventHandler);
James Feistbc896df2018-11-26 16:28:17 -0800961
Bruce Lee913d4d02021-07-22 10:18:42 +0800962 setupManufacturingModeMatch(*systemBus);
James Feistbc896df2018-11-26 16:28:17 -0800963 io.run();
Zhikui Ren8685b172021-06-29 15:16:52 -0700964 return 0;
James Feistbc896df2018-11-26 16:28:17 -0800965}