blob: 04b31766573c0b862998b33ecab34153b88dc203 [file] [log] [blame]
/**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Configuration. */
#include "conf.hpp"
#include "zone.hpp"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <fstream>
#include <iostream>
#include <libconfig.h++>
#include <memory>
#include "pid/controller.hpp"
#include "pid/fancontroller.hpp"
#include "pid/thermalcontroller.hpp"
#include "pid/ec/pid.hpp"
using tstamp = std::chrono::high_resolution_clock::time_point;
using namespace std::literals::chrono_literals;
static constexpr bool deferSignals = true;
static constexpr auto objectPath = "/xyz/openbmc_project/settings/fanctrl/zone";
float PIDZone::getMaxRPMRequest(void) const
{
return _maximumRPMSetPt;
}
bool PIDZone::getManualMode(void) const
{
return _manualMode;
}
void PIDZone::setManualMode(bool mode)
{
_manualMode = mode;
}
bool PIDZone::getFailSafeMode(void) const
{
// If any keys are present at least one sensor is in fail safe mode.
return !_failSafeSensors.empty();
}
int64_t PIDZone::getZoneId(void) const
{
return _zoneId;
}
void PIDZone::addRPMSetPoint(float setpoint)
{
_RPMSetPoints.push_back(setpoint);
}
void PIDZone::clearRPMSetPoints(void)
{
_RPMSetPoints.clear();
}
float PIDZone::getFailSafePercent(void) const
{
return _failSafePercent;
}
float PIDZone::getMinThermalRpmSetPt(void) const
{
return _minThermalRpmSetPt;
}
void PIDZone::addFanPID(std::unique_ptr<PIDController> pid)
{
_fans.push_back(std::move(pid));
}
void PIDZone::addThermalPID(std::unique_ptr<PIDController> pid)
{
_thermals.push_back(std::move(pid));
}
double PIDZone::getCachedValue(const std::string& name)
{
return _cachedValuesByName.at(name);
}
void PIDZone::addFanInput(std::string fan)
{
_fanInputs.push_back(fan);
}
void PIDZone::addThermalInput(std::string therm)
{
_thermalInputs.push_back(therm);
}
void PIDZone::determineMaxRPMRequest(void)
{
float max = 0;
std::vector<float>::iterator result;
if (_RPMSetPoints.size() > 0)
{
result = std::max_element(_RPMSetPoints.begin(), _RPMSetPoints.end());
max = *result;
}
/*
* If the maximum RPM set-point output is below the minimum RPM
* set-point, set it to the minimum.
*/
max = std::max(getMinThermalRpmSetPt(), max);
#ifdef __TUNING_LOGGING__
/*
* We received no set-points from thermal sensors.
* This is a case experienced during tuning where they only specify
* fan sensors and one large fan PID for all the fans.
*/
static constexpr auto setpointpath = "/etc/thermal.d/set-point";
try
{
int value;
std::ifstream ifs;
ifs.open(setpointpath);
if (ifs.good()) {
ifs >> value;
max = value; // expecting RPM set-point, not pwm%
}
}
catch (const std::exception& e)
{
/* This exception is uninteresting. */
std::cerr << "Unable to read from '" << setpointpath << "'\n";
}
#endif
_maximumRPMSetPt = max;
return;
}
#ifdef __TUNING_LOGGING__
void PIDZone::initializeLog(void)
{
/* Print header for log file. */
_log << "epoch_ms,setpt";
for (auto& f : _fanInputs)
{
_log << "," << f;
}
_log << std::endl;
return;
}
std::ofstream& PIDZone::getLogHandle(void)
{
return _log;
}
#endif
/*
* TODO(venture) This is effectively updating the cache and should check if the
* values they're using to update it are new or old, or whatnot. For instance,
* if we haven't heard from the host in X time we need to detect this failure.
*
* I haven't decided if the Sensor should have a lastUpdated method or whether
* that should be for the ReadInterface or etc...
*/
/**
* We want the PID loop to run with values cached, so this will get all the
* fan tachs for the loop.
*/
void PIDZone::updateFanTelemetry(void)
{
/* TODO(venture): Should I just make _log point to /dev/null when logging
* is disabled? I think it's a waste to try and log things even if the
* data is just being dropped though.
*/
#ifdef __TUNING_LOGGING__
tstamp now = std::chrono::high_resolution_clock::now();
_log << std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
_log << "," << _maximumRPMSetPt;
#endif
for (auto& f : _fanInputs)
{
auto& sensor = _mgr->getSensor(f);
ReadReturn r = sensor->read();
_cachedValuesByName[f] = r.value;
/*
* TODO(venture): We should check when these were last read.
* However, these are the fans, so if I'm not getting updated values
* for them... what should I do?
*/
#ifdef __TUNING_LOGGING__
_log << "," << r.value;
#endif
}
return;
}
void PIDZone::updateSensors(void)
{
using namespace std::chrono;
/* margin and temp are stored as temp */
tstamp now = high_resolution_clock::now();
for (auto& t : _thermalInputs)
{
auto& sensor = _mgr->getSensor(t);
ReadReturn r = sensor->read();
int64_t timeout = sensor->GetTimeout();
_cachedValuesByName[t] = r.value;
tstamp then = r.updated;
/* Only go into failsafe if the timeout is set for
* the sensor.
*/
if (timeout > 0)
{
auto duration = duration_cast<std::chrono::seconds>
(now - then).count();
auto period = std::chrono::seconds(timeout).count();
if (duration >= period)
{
//std::cerr << "Entering fail safe mode.\n";
_failSafeSensors.insert(t);
}
else
{
// Check if it's in there: remove it.
auto kt = _failSafeSensors.find(t);
if (kt != _failSafeSensors.end())
{
_failSafeSensors.erase(kt);
}
}
}
}
return;
}
void PIDZone::initializeCache(void)
{
for (auto& f : _fanInputs)
{
_cachedValuesByName[f] = 0;
}
for (auto& t : _thermalInputs)
{
_cachedValuesByName[t] = 0;
// Start all sensors in fail-safe mode.
_failSafeSensors.insert(t);
}
}
void PIDZone::dumpCache(void)
{
std::cerr << "Cache values now: \n";
for (auto& k : _cachedValuesByName)
{
std::cerr << k.first << ": " << k.second << "\n";
}
}
void PIDZone::process_fans(void)
{
for (auto& p : _fans)
{
p->pid_process();
}
}
void PIDZone::process_thermals(void)
{
for (auto& p : _thermals)
{
p->pid_process();
}
}
std::unique_ptr<Sensor>& PIDZone::getSensor(std::string name)
{
return _mgr->getSensor(name);
}
bool PIDZone::manual(bool value)
{
std::cerr << "manual: " << value << std::endl;
setManualMode(value);
return ModeObject::manual(value);
}
bool PIDZone::failSafe() const
{
return getFailSafeMode();
}
static std::string GetControlPath(int64_t zone)
{
return std::string(objectPath) + std::to_string(zone);
}
std::map<int64_t, std::shared_ptr<PIDZone>> BuildZones(
std::map<int64_t, PIDConf>& ZonePids,
std::map<int64_t, struct zone>& ZoneConfigs,
std::shared_ptr<SensorManager> mgr,
sdbusplus::bus::bus& ModeControlBus)
{
std::map<int64_t, std::shared_ptr<PIDZone>> zones;
for (auto& zi : ZonePids)
{
auto zoneId = static_cast<int64_t>(zi.first);
/* The above shouldn't be necessary but is, and I am having trouble
* locating my notes on why. If I recall correctly it was casting it
* down to a byte in at least some cases causing weird behaviors.
*/
auto zoneConf = ZoneConfigs.find(zoneId);
if (zoneConf == ZoneConfigs.end())
{
/* The Zone doesn't have a configuration, bail. */
static constexpr auto err =
"Bailing during load, missing Zone Configuration";
std::cerr << err << std::endl;
throw std::runtime_error(err);
}
PIDConf& PIDConfig = zi.second;
auto zone = std::make_shared<PIDZone>(
zoneId,
zoneConf->second.minthermalrpm,
zoneConf->second.failsafepercent,
mgr,
ModeControlBus,
GetControlPath(zi.first).c_str(),
deferSignals);
zones[zoneId] = zone;
std::cerr << "Zone Id: " << zone->getZoneId() << "\n";
// For each PID create a Controller and a Sensor.
PIDConf::iterator pit = PIDConfig.begin();
while (pit != PIDConfig.end())
{
std::vector<std::string> inputs;
std::string name = pit->first;
struct controller_info* info = &pit->second;
std::cerr << "PID name: " << name << "\n";
/*
* TODO(venture): Need to check if input is known to the
* SensorManager.
*/
if (info->type == "fan")
{
for (auto i : info->inputs)
{
inputs.push_back(i);
zone->addFanInput(i);
}
auto pid = FanController::CreateFanPid(
zone,
name,
inputs,
info->info);
zone->addFanPID(std::move(pid));
}
else if (info->type == "temp" || info->type == "margin")
{
for (auto i : info->inputs)
{
inputs.push_back(i);
zone->addThermalInput(i);
}
auto pid = ThermalController::CreateThermalPid(
zone,
name,
inputs,
info->setpoint,
info->info);
zone->addThermalPID(std::move(pid));
}
std::cerr << "inputs: ";
for (auto& i : inputs)
{
std::cerr << i << ", ";
}
std::cerr << "\n";
++pit;
}
zone->emit_object_added();
}
return zones;
}
std::map<int64_t, std::shared_ptr<PIDZone>> BuildZonesFromConfig(
std::string& path,
std::shared_ptr<SensorManager> mgr,
sdbusplus::bus::bus& ModeControlBus)
{
using namespace libconfig;
// zone -> pids
std::map<int64_t, PIDConf> pidConfig;
// zone -> configs
std::map<int64_t, struct zone> zoneConfig;
std::cerr << "entered BuildZonesFromConfig\n";
Config cfg;
/* The load was modeled after the example source provided. */
try
{
cfg.readFile(path.c_str());
}
catch (const FileIOException& fioex)
{
std::cerr << "I/O error while reading file: " << fioex.what() << std::endl;
throw;
}
catch (const ParseException& pex)
{
std::cerr << "Parse error at " << pex.getFile() << ":" << pex.getLine()
<< " - " << pex.getError() << std::endl;
throw;
}
try
{
const Setting& root = cfg.getRoot();
const Setting& zones = root["zones"];
int count = zones.getLength();
/* For each zone. */
for (int i = 0; i < count; ++i)
{
const Setting& zoneSettings = zones[i];
int id;
PIDConf thisZone;
struct zone thisZoneConfig;
zoneSettings.lookupValue("id", id);
thisZoneConfig.minthermalrpm =
zoneSettings.lookup("minthermalrpm");
thisZoneConfig.failsafepercent =
zoneSettings.lookup("failsafepercent");
const Setting& pids = zoneSettings["pids"];
int pidCount = pids.getLength();
for (int j = 0; j < pidCount; ++j)
{
const Setting& pid = pids[j];
std::string name;
controller_info info;
/*
* Mysteriously if you use lookupValue on these, and the type
* is float. It won't work right.
*
* If the configuration file value doesn't look explicitly like
* a float it won't let you assign it to one.
*/
name = pid.lookup("name").c_str();
info.type = pid.lookup("type").c_str();
/* set-point is only required to be set for thermal. */
/* TODO(venture): Verify this works optionally here. */
info.setpoint = pid.lookup("set-point");
info.info.ts = pid.lookup("pid.sampleperiod");
info.info.p_c = pid.lookup("pid.p_coefficient");
info.info.i_c = pid.lookup("pid.i_coefficient");
info.info.ff_off = pid.lookup("pid.ff_off_coefficient");
info.info.ff_gain = pid.lookup("pid.ff_gain_coefficient");
info.info.i_lim.min = pid.lookup("pid.i_limit.min");
info.info.i_lim.max = pid.lookup("pid.i_limit.max");
info.info.out_lim.min = pid.lookup("pid.out_limit.min");
info.info.out_lim.max = pid.lookup("pid.out_limit.max");
info.info.slew_neg = pid.lookup("pid.slew_neg");
info.info.slew_pos = pid.lookup("pid.slew_pos");
std::cerr << "out_lim.min: " << info.info.out_lim.min << "\n";
std::cerr << "out_lim.max: " << info.info.out_lim.max << "\n";
const Setting& inputs = pid["inputs"];
int icount = inputs.getLength();
for (int z = 0; z < icount; ++z)
{
std::string v;
v = pid["inputs"][z].c_str();
info.inputs.push_back(v);
}
thisZone[name] = info;
}
pidConfig[static_cast<int64_t>(id)] = thisZone;
zoneConfig[static_cast<int64_t>(id)] = thisZoneConfig;
}
}
catch (const SettingTypeException &setex)
{
std::cerr << "Setting '" << setex.getPath() << "' type exception!" << std::endl;
throw;
}
catch (const SettingNotFoundException& snex)
{
std::cerr << "Setting '" << snex.getPath() << "' not found!" << std::endl;
throw;
}
return BuildZones(pidConfig, zoneConfig, mgr, ModeControlBus);
}