Add stepwise controller

This adds the ability to use stepwise curves alongside
pid control. This creates a base controller class that
pidcontroller and stepwise controller inherit from.

Note: Hysteresis to come in follow-on patch

Tested-by:
Created a stepwise controller and noticed that when it
crossed a threshold that it contributed to the pwm setting.

Change-Id: I6cf842f80eaccafc905d620970afe91e2092d568
Signed-off-by: James Feist <james.feist@linux.intel.com>
diff --git a/Makefile.am b/Makefile.am
index a46ffd0..23f39bb 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -54,9 +54,11 @@
 	sensors/builderconfig.cpp \
 	sensors/manager.cpp \
 	pid/ec/pid.cpp \
-	pid/controller.cpp \
+	pid/ec/stepwise.cpp \
 	pid/fancontroller.cpp \
 	pid/thermalcontroller.cpp \
+	pid/pidcontroller.cpp \
+	pid/stepwisecontroller.cpp \
 	pid/builder.cpp \
 	pid/builderconfig.cpp \
 	pid/zone.cpp \
diff --git a/conf.hpp b/conf.hpp
index 1586187..01968cb 100644
--- a/conf.hpp
+++ b/conf.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include "pid/ec/pid.hpp"
+#include "pid/ec/stepwise.hpp"
 
 #include <map>
 #include <string>
@@ -30,7 +31,11 @@
     std::string type;                // fan or margin or temp?
     std::vector<std::string> inputs; // one or more sensors.
     float setpoint;                  // initial setpoint for thermal.
-    ec::pidinfo info;                // pid details
+    union
+    {
+        ec::pidinfo pidInfo; // pid details
+        ec::StepwiseInfo stepwiseInfo;
+    };
 };
 
 /*
diff --git a/dbus/dbusconfiguration.cpp b/dbus/dbusconfiguration.cpp
index 7f721bf..b912e62 100644
--- a/dbus/dbusconfiguration.cpp
+++ b/dbus/dbusconfiguration.cpp
@@ -21,6 +21,7 @@
 #include <iostream>
 #include <sdbusplus/bus.hpp>
 #include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/exception.hpp>
 #include <set>
 #include <thread>
 #include <unordered_map>
@@ -37,6 +38,8 @@
     "org.freedesktop.DBus.ObjectManager";
 constexpr const char* pidZoneConfigurationInterface =
     "xyz.openbmc_project.Configuration.Pid.Zone";
+constexpr const char* stepwiseConfigurationInterface =
+    "xyz.openbmc_project.Configuration.Stepwise";
 constexpr const char* sensorInterface = "xyz.openbmc_project.Sensor.Value";
 constexpr const char* pwmInterface = "xyz.openbmc_project.Control.FanPwm";
 
@@ -103,17 +106,17 @@
             }
             std::cout << "\t\t\t}\n";
             std::cout << "\t\t\t" << pidconf.second.setpoint << ",\n";
-            std::cout << "\t\t\t{" << pidconf.second.info.ts << ",\n";
-            std::cout << "\t\t\t" << pidconf.second.info.p_c << ",\n";
-            std::cout << "\t\t\t" << pidconf.second.info.i_c << ",\n";
-            std::cout << "\t\t\t" << pidconf.second.info.ff_off << ",\n";
-            std::cout << "\t\t\t" << pidconf.second.info.ff_gain << ",\n";
-            std::cout << "\t\t\t{" << pidconf.second.info.i_lim.min << ","
-                      << pidconf.second.info.i_lim.max << "},\n";
-            std::cout << "\t\t\t{" << pidconf.second.info.out_lim.min << ","
-                      << pidconf.second.info.out_lim.max << "},\n";
-            std::cout << "\t\t\t" << pidconf.second.info.slew_neg << ",\n";
-            std::cout << "\t\t\t" << pidconf.second.info.slew_pos << ",\n";
+            std::cout << "\t\t\t{" << pidconf.second.pidInfo.ts << ",\n";
+            std::cout << "\t\t\t" << pidconf.second.pidInfo.p_c << ",\n";
+            std::cout << "\t\t\t" << pidconf.second.pidInfo.i_c << ",\n";
+            std::cout << "\t\t\t" << pidconf.second.pidInfo.ff_off << ",\n";
+            std::cout << "\t\t\t" << pidconf.second.pidInfo.ff_gain << ",\n";
+            std::cout << "\t\t\t{" << pidconf.second.pidInfo.i_lim.min << ","
+                      << pidconf.second.pidInfo.i_lim.max << "},\n";
+            std::cout << "\t\t\t{" << pidconf.second.pidInfo.out_lim.min << ","
+                      << pidconf.second.pidInfo.out_lim.max << "},\n";
+            std::cout << "\t\t\t" << pidconf.second.pidInfo.slew_neg << ",\n";
+            std::cout << "\t\t\t" << pidconf.second.pidInfo.slew_pos << ",\n";
             std::cout << "\t\t\t}\n\t\t}\n";
         }
         std::cout << "\t},\n";
@@ -123,14 +126,15 @@
 
 void init(sdbusplus::bus::bus& bus)
 {
+    using DbusVariantType =
+        sdbusplus::message::variant<uint64_t, int64_t, double, std::string,
+                                    std::vector<std::string>,
+                                    std::vector<double>>;
+
     using ManagedObjectType = std::unordered_map<
         sdbusplus::message::object_path,
-        std::unordered_map<
-            std::string,
-            std::unordered_map<std::string,
-                               sdbusplus::message::variant<
-                                   uint64_t, int64_t, double, std::string,
-                                   std::vector<std::string>>>>>;
+        std::unordered_map<std::string,
+                           std::unordered_map<std::string, DbusVariantType>>>;
 
     // install watch for properties changed
     std::function<void(sdbusplus::message::message & message)> eventHandler =
@@ -153,22 +157,32 @@
                             "/xyz/openbmc_project/object_mapper",
                             "xyz.openbmc_project.ObjectMapper", "GetSubTree");
     mapper.append("", 0,
-                  std::array<const char*, 5>{objectManagerInterface,
+                  std::array<const char*, 6>{objectManagerInterface,
                                              pidConfigurationInterface,
                                              pidZoneConfigurationInterface,
+                                             stepwiseConfigurationInterface,
                                              sensorInterface, pwmInterface});
-    auto resp = bus.call(mapper);
-    if (resp.is_method_error())
-    {
-        throw std::runtime_error("ObjectMapper Call Failure");
-    }
     std::unordered_map<
         std::string, std::unordered_map<std::string, std::vector<std::string>>>
         respData;
+    try
+    {
+        auto resp = bus.call(mapper);
+        if (resp.is_method_error())
+        {
+            throw std::runtime_error("ObjectMapper Call Failure");
+        }
+        resp.read(respData);
+    }
+    catch (sdbusplus::exception_t&)
+    {
+        // can't do anything without mapper call data
+        throw std::runtime_error("ObjectMapper Call Failure");
+    }
 
-    resp.read(respData);
     if (respData.empty())
     {
+        // can't do anything without mapper call data
         throw std::runtime_error("No configuration data available from Mapper");
     }
     // create a map of pair of <has pid configuration, ObjectManager path>
@@ -188,7 +202,8 @@
                     owner.second = objectPair.first;
                 }
                 if (interface == pidConfigurationInterface ||
-                    interface == pidZoneConfigurationInterface)
+                    interface == pidZoneConfigurationInterface ||
+                    interface == stepwiseConfigurationInterface)
                 {
                     owner.first = true;
                 }
@@ -216,19 +231,31 @@
         auto endpoint = bus.new_method_call(
             owner.first.c_str(), owner.second.second.c_str(),
             "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
-        auto responce = bus.call(endpoint);
-        if (responce.is_method_error())
+        ManagedObjectType configuration;
+        try
         {
+            auto responce = bus.call(endpoint);
+            if (responce.is_method_error())
+            {
+                throw std::runtime_error("Error getting managed objects from " +
+                                         owner.first);
+            }
+            responce.read(configuration);
+        }
+        catch (sdbusplus::exception_t&)
+        {
+            // this shouldn't happen, probably means daemon crashed
             throw std::runtime_error("Error getting managed objects from " +
                                      owner.first);
         }
-        ManagedObjectType configuration;
-        responce.read(configuration);
+
         for (auto& pathPair : configuration)
         {
             if (pathPair.second.find(pidConfigurationInterface) !=
                     pathPair.second.end() ||
                 pathPair.second.find(pidZoneConfigurationInterface) !=
+                    pathPair.second.end() ||
+                pathPair.second.find(stepwiseConfigurationInterface) !=
                     pathPair.second.end())
             {
                 configurations.emplace(pathPair);
@@ -268,125 +295,217 @@
                 VariantToFloatVisitor(), zone.at("FailSafePercent"));
         }
         auto findBase = configuration.second.find(pidConfigurationInterface);
-        if (findBase == configuration.second.end())
+        if (findBase != configuration.second.end())
         {
-            continue;
-        }
 
-        const auto& base = configuration.second.at(pidConfigurationInterface);
-        const std::vector<std::string>& zones =
-            sdbusplus::message::variant_ns::get<std::vector<std::string>>(
-                base.at("Zones"));
-        for (const std::string& zone : zones)
-        {
-            auto it = std::find(zoneIndex.begin(), zoneIndex.end(), zone);
-            size_t index = 1;
-            if (it == zoneIndex.end())
-            {
-                zoneIndex.emplace_back(zone);
-                index = zoneIndex.size();
-            }
-            else
-            {
-                index = zoneIndex.end() - it;
-            }
-            PIDConf& conf = ZoneConfig[index];
-            struct controller_info& info =
-                conf[sdbusplus::message::variant_ns::get<std::string>(
-                    base.at("Name"))];
-            info.type = sdbusplus::message::variant_ns::get<std::string>(
-                base.at("Class"));
-            // todo: auto generation yaml -> c script seems to discard this
-            // value for fans, verify this is okay
-            if (info.type == "fan")
-            {
-                info.setpoint = 0;
-            }
-            else
-            {
-                info.setpoint = mapbox::util::apply_visitor(
-                    VariantToFloatVisitor(), base.at("SetPoint"));
-            }
-            info.info.ts = 1.0; // currently unused
-            info.info.p_c = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("PCoefficient"));
-            info.info.i_c = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("ICoefficient"));
-            info.info.ff_off = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("FFOffCoefficient"));
-            info.info.ff_gain = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("FFGainCoefficient"));
-            info.info.i_lim.max = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("ILimitMax"));
-            info.info.i_lim.min = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("ILimitMin"));
-            info.info.out_lim.max = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("OutLimitMax"));
-            info.info.out_lim.min = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("OutLimitMin"));
-            info.info.slew_neg = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("SlewNeg"));
-            info.info.slew_pos = mapbox::util::apply_visitor(
-                VariantToFloatVisitor(), base.at("SlewPos"));
-
-            std::vector<std::string> sensorNames =
+            const auto& base =
+                configuration.second.at(pidConfigurationInterface);
+            const std::vector<std::string>& zones =
                 sdbusplus::message::variant_ns::get<std::vector<std::string>>(
-                    base.at("Inputs"));
-            auto findOutputs =
-                base.find("Outputs"); // currently only fans have outputs
-            if (findOutputs != base.end())
+                    base.at("Zones"));
+            for (const std::string& zone : zones)
             {
-                std::vector<std::string> outputs =
-                    sdbusplus::message::variant_ns::get<
-                        std::vector<std::string>>(findOutputs->second);
-                sensorNames.insert(sensorNames.end(), outputs.begin(),
-                                   outputs.end());
-            }
-            for (const std::string& sensorName : sensorNames)
-            {
-                std::string name = sensorName;
-                // replace spaces with underscores to be legal on dbus
-                std::replace(name.begin(), name.end(), ' ', '_');
-                std::pair<std::string, std::string> sensorPathIfacePair;
-
-                if (!findSensor(sensors, name, sensorPathIfacePair))
+                auto it = std::find(zoneIndex.begin(), zoneIndex.end(), zone);
+                size_t index = 1;
+                if (it == zoneIndex.end())
                 {
-                    throw std::runtime_error(
-                        "Could not map configuration to sensor " + name);
+                    zoneIndex.emplace_back(zone);
+                    index = zoneIndex.size();
                 }
-                if (sensorPathIfacePair.second == sensorInterface)
+                else
                 {
+                    index = zoneIndex.end() - it;
+                }
+                PIDConf& conf = ZoneConfig[index];
+                struct controller_info& info =
+                    conf[sdbusplus::message::variant_ns::get<std::string>(
+                        base.at("Name"))];
+                info.type = sdbusplus::message::variant_ns::get<std::string>(
+                    base.at("Class"));
+                // todo: auto generation yaml -> c script seems to discard this
+                // value for fans, verify this is okay
+                if (info.type == "fan")
+                {
+                    info.setpoint = 0;
+                }
+                else
+                {
+                    info.setpoint = mapbox::util::apply_visitor(
+                        VariantToFloatVisitor(), base.at("SetPoint"));
+                }
+                info.pidInfo.ts = 1.0; // currently unused
+                info.pidInfo.p_c = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("PCoefficient"));
+                info.pidInfo.i_c = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("ICoefficient"));
+                info.pidInfo.ff_off = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("FFOffCoefficient"));
+                info.pidInfo.ff_gain = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("FFGainCoefficient"));
+                info.pidInfo.i_lim.max = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("ILimitMax"));
+                info.pidInfo.i_lim.min = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("ILimitMin"));
+                info.pidInfo.out_lim.max = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("OutLimitMax"));
+                info.pidInfo.out_lim.min = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("OutLimitMin"));
+                info.pidInfo.slew_neg = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("SlewNeg"));
+                info.pidInfo.slew_pos = mapbox::util::apply_visitor(
+                    VariantToFloatVisitor(), base.at("SlewPos"));
+
+                std::vector<std::string> sensorNames =
+                    sdbusplus::message::variant_ns::get<
+                        std::vector<std::string>>(base.at("Inputs"));
+                auto findOutputs =
+                    base.find("Outputs"); // currently only fans have outputs
+                if (findOutputs != base.end())
+                {
+                    std::vector<std::string> outputs =
+                        sdbusplus::message::variant_ns::get<
+                            std::vector<std::string>>(findOutputs->second);
+                    sensorNames.insert(sensorNames.end(), outputs.begin(),
+                                       outputs.end());
+                }
+                for (const std::string& sensorName : sensorNames)
+                {
+                    std::string name = sensorName;
+                    // replace spaces with underscores to be legal on dbus
+                    std::replace(name.begin(), name.end(), ' ', '_');
+                    std::pair<std::string, std::string> sensorPathIfacePair;
+
+                    if (!findSensor(sensors, name, sensorPathIfacePair))
+                    {
+                        throw std::runtime_error(
+                            "Could not map configuration to sensor " + name);
+                    }
+                    if (sensorPathIfacePair.second == sensorInterface)
+                    {
+                        info.inputs.push_back(name);
+                        auto& config = SensorConfig[name];
+                        config.type =
+                            sdbusplus::message::variant_ns::get<std::string>(
+                                base.at("Class"));
+                        config.readpath = sensorPathIfacePair.first;
+                        // todo: maybe un-hardcode this if we run into slower
+                        // timeouts with sensors
+                        if (config.type == "temp")
+                        {
+                            config.timeout = 500;
+                        }
+                    }
+                    if (sensorPathIfacePair.second == pwmInterface)
+                    {
+                        // copy so we can modify it
+                        for (std::string otherSensor : sensorNames)
+                        {
+                            if (otherSensor == sensorName)
+                            {
+                                continue;
+                            }
+                            std::replace(otherSensor.begin(), otherSensor.end(),
+                                         ' ', '_');
+                            auto& config = SensorConfig[otherSensor];
+                            config.writepath = sensorPathIfacePair.first;
+                            // todo: un-hardcode this if there are fans with
+                            // different ranges
+                            config.max = 255;
+                            config.min = 0;
+                        }
+                    }
+                }
+            }
+        }
+        auto findStepwise =
+            configuration.second.find(stepwiseConfigurationInterface);
+        if (findStepwise != configuration.second.end())
+        {
+            const auto& base = findStepwise->second;
+            const std::vector<std::string>& zones =
+                sdbusplus::message::variant_ns::get<std::vector<std::string>>(
+                    base.at("Zones"));
+            for (const std::string& zone : zones)
+            {
+                auto it = std::find(zoneIndex.begin(), zoneIndex.end(), zone);
+                size_t index = 1;
+                if (it == zoneIndex.end())
+                {
+                    zoneIndex.emplace_back(zone);
+                    index = zoneIndex.size();
+                }
+                else
+                {
+                    index = zoneIndex.end() - it;
+                }
+                PIDConf& conf = ZoneConfig[index];
+                struct controller_info& info =
+                    conf[sdbusplus::message::variant_ns::get<std::string>(
+                        base.at("Name"))];
+                info.type = "stepwise";
+                info.stepwiseInfo.ts = 1.0; // currently unused
+                std::vector<double> readings =
+                    sdbusplus::message::variant_ns::get<std::vector<double>>(
+                        base.at("Reading"));
+                if (readings.size() > ec::maxStepwisePoints)
+                {
+                    throw std::invalid_argument("Too many stepwise points.");
+                }
+                if (readings.empty())
+                {
+                    throw std::invalid_argument(
+                        "Must have one stepwise point.");
+                }
+                std::copy(readings.begin(), readings.end(),
+                          info.stepwiseInfo.reading);
+                if (readings.size() < ec::maxStepwisePoints)
+                {
+                    info.stepwiseInfo.reading[readings.size()] =
+                        std::numeric_limits<float>::quiet_NaN();
+                }
+                std::vector<double> outputs =
+                    sdbusplus::message::variant_ns::get<std::vector<double>>(
+                        base.at("Output"));
+                if (readings.size() != outputs.size())
+                {
+                    throw std::invalid_argument(
+                        "Outputs size must match readings");
+                }
+                std::copy(outputs.begin(), outputs.end(),
+                          info.stepwiseInfo.output);
+                if (outputs.size() < ec::maxStepwisePoints)
+                {
+                    info.stepwiseInfo.output[outputs.size()] =
+                        std::numeric_limits<float>::quiet_NaN();
+                }
+
+                std::vector<std::string> sensorNames =
+                    sdbusplus::message::variant_ns::get<
+                        std::vector<std::string>>(base.at("Inputs"));
+
+                for (const std::string& sensorName : sensorNames)
+                {
+                    std::string name = sensorName;
+                    // replace spaces with underscores to be legal on dbus
+                    std::replace(name.begin(), name.end(), ' ', '_');
+                    std::pair<std::string, std::string> sensorPathIfacePair;
+
+                    if (!findSensor(sensors, name, sensorPathIfacePair))
+                    {
+                        // todo(james): if we can't find a sensor, delete it
+                        // from config, we'll find it on rescan
+                        throw std::runtime_error(
+                            "Could not map configuration to sensor " + name);
+                    }
+
                     info.inputs.push_back(name);
                     auto& config = SensorConfig[name];
-                    config.type =
-                        sdbusplus::message::variant_ns::get<std::string>(
-                            base.at("Class"));
                     config.readpath = sensorPathIfacePair.first;
+                    config.type = "temp";
                     // todo: maybe un-hardcode this if we run into slower
                     // timeouts with sensors
-                    if (config.type == "temp")
-                    {
-                        config.timeout = 500;
-                    }
-                }
-                if (sensorPathIfacePair.second == pwmInterface)
-                {
-                    // copy so we can modify it
-                    for (std::string otherSensor : sensorNames)
-                    {
-                        if (otherSensor == sensorName)
-                        {
-                            continue;
-                        }
-                        std::replace(otherSensor.begin(), otherSensor.end(),
-                                     ' ', '_');
-                        auto& config = SensorConfig[otherSensor];
-                        config.writepath = sensorPathIfacePair.first;
-                        // todo: un-hardcode this if there are fans with
-                        // different ranges
-                        config.max = 255;
-                        config.min = 0;
-                    }
+
+                    config.timeout = 500;
                 }
             }
         }
diff --git a/pid/builder.cpp b/pid/builder.cpp
index 8ffa77c..e98ba22 100644
--- a/pid/builder.cpp
+++ b/pid/builder.cpp
@@ -17,7 +17,9 @@
 #include "pid/builder.hpp"
 
 #include "conf.hpp"
+#include "pid/controller.hpp"
 #include "pid/fancontroller.hpp"
+#include "pid/stepwisecontroller.hpp"
 #include "pid/thermalcontroller.hpp"
 
 #include <iostream>
@@ -89,7 +91,7 @@
                 }
 
                 auto pid = FanController::CreateFanPid(zone.get(), name, inputs,
-                                                       info->info);
+                                                       info->pidInfo);
                 zone->addFanPID(std::move(pid));
             }
             else if (info->type == "temp" || info->type == "margin")
@@ -101,9 +103,21 @@
                 }
 
                 auto pid = ThermalController::CreateThermalPid(
-                    zone.get(), name, inputs, info->setpoint, info->info);
+                    zone.get(), name, inputs, info->setpoint, info->pidInfo);
+
                 zone->addThermalPID(std::move(pid));
             }
+            else if (info->type == "stepwise")
+            {
+                for (auto& i : info->inputs)
+                {
+                    inputs.push_back(i);
+                    zone->addThermalInput(i);
+                }
+                auto stepwise = StepwiseController::CreateStepwiseController(
+                    zone.get(), name, inputs, info->stepwiseInfo);
+                zone->addThermalPID(std::move(stepwise));
+            }
 
             std::cerr << "inputs: ";
             for (auto& i : inputs)
diff --git a/pid/builderconfig.cpp b/pid/builderconfig.cpp
index 0106057..c72ef9e 100644
--- a/pid/builderconfig.cpp
+++ b/pid/builderconfig.cpp
@@ -102,20 +102,22 @@
                 /* 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");
+                info.pidInfo.ts = pid.lookup("pid.sampleperiod");
+                info.pidInfo.p_c = pid.lookup("pid.p_coefficient");
+                info.pidInfo.i_c = pid.lookup("pid.i_coefficient");
+                info.pidInfo.ff_off = pid.lookup("pid.ff_off_coefficient");
+                info.pidInfo.ff_gain = pid.lookup("pid.ff_gain_coefficient");
+                info.pidInfo.i_lim.min = pid.lookup("pid.i_limit.min");
+                info.pidInfo.i_lim.max = pid.lookup("pid.i_limit.max");
+                info.pidInfo.out_lim.min = pid.lookup("pid.out_limit.min");
+                info.pidInfo.out_lim.max = pid.lookup("pid.out_limit.max");
+                info.pidInfo.slew_neg = pid.lookup("pid.slew_neg");
+                info.pidInfo.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";
+                std::cerr << "out_lim.min: " << info.pidInfo.out_lim.min
+                          << "\n";
+                std::cerr << "out_lim.max: " << info.pidInfo.out_lim.max
+                          << "\n";
 
                 const Setting& inputs = pid["inputs"];
                 int icount = inputs.getLength();
diff --git a/pid/controller.hpp b/pid/controller.hpp
index 57ee43f..f52c412 100644
--- a/pid/controller.hpp
+++ b/pid/controller.hpp
@@ -3,57 +3,23 @@
 #include "ec/pid.hpp"
 #include "fan.hpp"
 
-#include <memory>
-#include <vector>
-
-class ZoneInterface;
+#include <string>
 
 /*
- * Base class for PID controllers.  Each PID that implements this needs to
- * provide an input_proc, setpt_proc, and output_proc.
+ * Base class for controllers.  Each controller that implements this needs to
+ * provide an input_proc, process, and output_proc.
  */
-class PIDController
-{
-  public:
-    PIDController(const std::string& id, ZoneInterface* owner) :
-        _owner(owner), _setpoint(0), _id(id)
-    {
-    }
+class ZoneInterface;
 
-    virtual ~PIDController()
-    {
-    }
+struct Controller
+{
+    virtual ~Controller() = default;
 
     virtual float input_proc(void) = 0;
-    virtual float setpt_proc(void) = 0;
+
     virtual void output_proc(float value) = 0;
 
-    void pid_process(void);
+    virtual void process(void) = 0;
 
-    std::string get_id(void)
-    {
-        return _id;
-    }
-    float get_setpoint(void)
-    {
-        return _setpoint;
-    }
-    void set_setpoint(float setpoint)
-    {
-        _setpoint = setpoint;
-    }
-
-    ec::pid_info_t* get_pid_info(void)
-    {
-        return &_pid_info;
-    }
-
-  protected:
-    ZoneInterface* _owner;
-
-  private:
-    // parameters
-    ec::pid_info_t _pid_info;
-    float _setpoint;
-    std::string _id;
+    virtual std::string get_id(void) = 0;
 };
diff --git a/pid/ec/stepwise.cpp b/pid/ec/stepwise.cpp
new file mode 100644
index 0000000..4246fb3
--- /dev/null
+++ b/pid/ec/stepwise.cpp
@@ -0,0 +1,45 @@
+/*
+// Copyright (c) 2018 Intel Corporation
+//
+// 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.
+*/
+
+#include "stepwise.hpp"
+
+#include <cstddef>
+#include <limits>
+
+namespace ec
+{
+float stepwise(const ec::StepwiseInfo& info, float input)
+{
+    float value = info.output[0]; // if we are below the lowest
+                                  // point, we set the lowest value
+
+    for (size_t ii = 1; ii < ec::maxStepwisePoints; ii++)
+    {
+
+        if (info.reading[ii] == std::numeric_limits<float>::quiet_NaN())
+        {
+            break;
+        }
+        if (info.reading[ii] > input)
+        {
+            break;
+        }
+        value = info.output[ii];
+    }
+
+    return value;
+}
+} // namespace ec
\ No newline at end of file
diff --git a/pid/ec/stepwise.hpp b/pid/ec/stepwise.hpp
new file mode 100644
index 0000000..ed07b44
--- /dev/null
+++ b/pid/ec/stepwise.hpp
@@ -0,0 +1,36 @@
+
+/*
+// Copyright (c) 2018 Intel Corporation
+//
+// 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.
+*/
+
+#pragma once
+
+#include <cstddef>
+#include <vector>
+
+namespace ec
+{
+constexpr size_t maxStepwisePoints = 20;
+
+struct StepwiseInfo
+{
+    float ts; // sample time in seconds
+    float reading[maxStepwisePoints];
+    float output[maxStepwisePoints];
+};
+
+float stepwise(const ec::StepwiseInfo& info, float value);
+
+} // namespace ec
\ No newline at end of file
diff --git a/pid/fancontroller.hpp b/pid/fancontroller.hpp
index aaccd4b..521d638 100644
--- a/pid/fancontroller.hpp
+++ b/pid/fancontroller.hpp
@@ -1,8 +1,8 @@
 #pragma once
 
-#include "controller.hpp"
 #include "ec/pid.hpp"
 #include "fan.hpp"
+#include "pidcontroller.hpp"
 
 #include <memory>
 #include <string>
diff --git a/pid/controller.cpp b/pid/pidcontroller.cpp
similarity index 94%
rename from pid/controller.cpp
rename to pid/pidcontroller.cpp
index 138268b..524b0ef 100644
--- a/pid/controller.cpp
+++ b/pid/pidcontroller.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "controller.hpp"
+#include "pidcontroller.hpp"
 
 #include "ec/pid.hpp"
 
@@ -26,7 +26,7 @@
 #include <thread>
 #include <vector>
 
-void PIDController::pid_process(void)
+void PIDController::process(void)
 {
     float input;
     float setpt;
diff --git a/pid/pidcontroller.hpp b/pid/pidcontroller.hpp
new file mode 100644
index 0000000..18a448e
--- /dev/null
+++ b/pid/pidcontroller.hpp
@@ -0,0 +1,60 @@
+#pragma once
+
+#include "controller.hpp"
+#include "ec/pid.hpp"
+#include "fan.hpp"
+
+#include <memory>
+#include <vector>
+
+class ZoneInterface;
+
+/*
+ * Base class for PID controllers.  Each PID that implements this needs to
+ * provide an input_proc, setpt_proc, and output_proc.
+ */
+class PIDController : public Controller
+{
+  public:
+    PIDController(const std::string& id, ZoneInterface* owner) :
+        Controller(), _owner(owner), _setpoint(0), _id(id)
+    {
+    }
+
+    virtual ~PIDController()
+    {
+    }
+
+    virtual float input_proc(void) = 0;
+    virtual float setpt_proc(void) = 0;
+    virtual void output_proc(float value) = 0;
+
+    void process(void);
+
+    std::string get_id(void)
+    {
+        return _id;
+    }
+    float get_setpoint(void)
+    {
+        return _setpoint;
+    }
+    void set_setpoint(float setpoint)
+    {
+        _setpoint = setpoint;
+    }
+
+    ec::pid_info_t* get_pid_info(void)
+    {
+        return &_pid_info;
+    }
+
+  protected:
+    ZoneInterface* _owner;
+
+  private:
+    // parameters
+    ec::pid_info_t _pid_info;
+    float _setpoint;
+    std::string _id;
+};
diff --git a/pid/pidthread.cpp b/pid/pidthread.cpp
index 490b70e..4dfc22a 100644
--- a/pid/pidthread.cpp
+++ b/pid/pidthread.cpp
@@ -16,7 +16,7 @@
 
 #include "pidthread.hpp"
 
-#include "pid/controller.hpp"
+#include "pid/pidcontroller.hpp"
 #include "sensors/sensor.hpp"
 
 #include <chrono>
diff --git a/pid/stepwisecontroller.cpp b/pid/stepwisecontroller.cpp
new file mode 100644
index 0000000..cea21e5
--- /dev/null
+++ b/pid/stepwisecontroller.cpp
@@ -0,0 +1,75 @@
+/*
+// Copyright (c) 2018 Intel Corporation
+//
+// 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.
+*/
+
+#include "stepwisecontroller.hpp"
+
+#include "ec/stepwise.hpp"
+#include "util.hpp"
+#include "zone.hpp"
+
+#include <algorithm>
+#include <chrono>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <thread>
+#include <vector>
+
+void StepwiseController::process(void)
+{
+    // Get input value
+    float input = input_proc();
+
+    // Calculate new output
+    float output = ec::stepwise(get_stepwise_info(), input);
+
+    // Output new value
+    output_proc(output);
+
+    return;
+}
+
+std::unique_ptr<Controller> StepwiseController::CreateStepwiseController(
+    ZoneInterface* owner, const std::string& id,
+    const std::vector<std::string>& inputs, const ec::StepwiseInfo& initial)
+{
+    // StepwiseController currently only supports precisely one input.
+    if (inputs.size() != 1)
+    {
+        return nullptr;
+    }
+
+    auto thermal = std::make_unique<StepwiseController>(id, inputs, owner);
+
+    ec::StepwiseInfo& info = thermal->get_stepwise_info();
+
+    info = initial;
+
+    return thermal;
+}
+
+float StepwiseController::input_proc(void)
+{
+    double value = _owner->getCachedValue(_inputs.at(0));
+    return static_cast<float>(value);
+}
+
+void StepwiseController::output_proc(float value)
+{
+    _owner->addRPMSetPoint(value);
+
+    return;
+}
diff --git a/pid/stepwisecontroller.hpp b/pid/stepwisecontroller.hpp
new file mode 100644
index 0000000..c3af1a1
--- /dev/null
+++ b/pid/stepwisecontroller.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "controller.hpp"
+#include "ec/stepwise.hpp"
+#include "fan.hpp"
+
+#include <memory>
+#include <vector>
+
+class ZoneInterface;
+
+class StepwiseController : public Controller
+{
+  public:
+    static std::unique_ptr<Controller>
+        CreateStepwiseController(ZoneInterface* owner, const std::string& id,
+                                 const std::vector<std::string>& inputs,
+                                 const ec::StepwiseInfo& initial);
+
+    StepwiseController(const std::string& id,
+                       const std::vector<std::string>& inputs,
+                       ZoneInterface* owner) :
+        Controller(),
+        _owner(owner), _id(id), _inputs(inputs)
+    {
+    }
+
+    float input_proc(void) override;
+
+    void output_proc(float value) override;
+
+    void process(void) override;
+
+    std::string get_id(void)
+    {
+        return _id;
+    }
+
+    ec::StepwiseInfo& get_stepwise_info(void)
+    {
+        return _stepwise_info;
+    }
+
+  protected:
+    ZoneInterface* _owner;
+
+  private:
+    // parameters
+    ec::StepwiseInfo _stepwise_info;
+    std::string _id;
+    std::vector<std::string> _inputs;
+};
diff --git a/pid/thermalcontroller.hpp b/pid/thermalcontroller.hpp
index ed94acb..85c3a2b 100644
--- a/pid/thermalcontroller.hpp
+++ b/pid/thermalcontroller.hpp
@@ -1,7 +1,7 @@
 #pragma once
 
-#include "controller.hpp"
 #include "ec/pid.hpp"
+#include "pidcontroller.hpp"
 
 #include <memory>
 #include <string>
diff --git a/pid/zone.cpp b/pid/zone.cpp
index 9e948f0..92a332e 100644
--- a/pid/zone.cpp
+++ b/pid/zone.cpp
@@ -21,6 +21,7 @@
 #include "pid/controller.hpp"
 #include "pid/ec/pid.hpp"
 #include "pid/fancontroller.hpp"
+#include "pid/stepwisecontroller.hpp"
 #include "pid/thermalcontroller.hpp"
 
 #include <algorithm>
@@ -80,12 +81,12 @@
     return _minThermalRpmSetPt;
 }
 
-void PIDZone::addFanPID(std::unique_ptr<PIDController> pid)
+void PIDZone::addFanPID(std::unique_ptr<Controller> pid)
 {
     _fans.push_back(std::move(pid));
 }
 
-void PIDZone::addThermalPID(std::unique_ptr<PIDController> pid)
+void PIDZone::addThermalPID(std::unique_ptr<Controller> pid)
 {
     _thermals.push_back(std::move(pid));
 }
@@ -305,7 +306,7 @@
 {
     for (auto& p : _fans)
     {
-        p->pid_process();
+        p->process();
     }
 }
 
@@ -313,7 +314,7 @@
 {
     for (auto& p : _thermals)
     {
-        p->pid_process();
+        p->process();
     }
 }
 
diff --git a/pid/zone.hpp b/pid/zone.hpp
index 99a8f14..6d5f6cc 100644
--- a/pid/zone.hpp
+++ b/pid/zone.hpp
@@ -2,6 +2,7 @@
 
 #include "conf.hpp"
 #include "controller.hpp"
+#include "pidcontroller.hpp"
 #include "sensors/manager.hpp"
 #include "sensors/sensor.hpp"
 #include "xyz/openbmc_project/Control/Mode/server.hpp"
@@ -76,8 +77,8 @@
     void process_fans(void);
     void process_thermals(void);
 
-    void addFanPID(std::unique_ptr<PIDController> pid);
-    void addThermalPID(std::unique_ptr<PIDController> pid);
+    void addFanPID(std::unique_ptr<Controller> pid);
+    void addThermalPID(std::unique_ptr<Controller> pid);
     double getCachedValue(const std::string& name) override;
     void addFanInput(std::string fan);
     void addThermalInput(std::string therm);
@@ -111,6 +112,6 @@
     std::map<std::string, double> _cachedValuesByName;
     const SensorManager& _mgr;
 
-    std::vector<std::unique_ptr<PIDController>> _fans;
-    std::vector<std::unique_ptr<PIDController>> _thermals;
+    std::vector<std::unique_ptr<Controller>> _fans;
+    std::vector<std::unique_ptr<Controller>> _thermals;
 };
diff --git a/test/Makefile.am b/test/Makefile.am
index 6622169..4a01994 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -32,17 +32,17 @@
 
 pid_zone_unittest_SOURCES = pid_zone_unittest.cpp
 pid_zone_unittest_LDADD = $(top_builddir)/pid/ec/pid.o \
- $(top_builddir)/sensors/manager.o $(top_builddir)/pid/controller.o \
+ $(top_builddir)/sensors/manager.o $(top_builddir)/pid/pidcontroller.o \
  $(top_builddir)/pid/zone.o
 
 pid_thermalcontroller_unittest_SOURCES = pid_thermalcontroller_unittest.cpp
 pid_thermalcontroller_unittest_LDADD = $(top_builddir)/pid/ec/pid.o \
- $(top_builddir)/pid/util.o $(top_builddir)/pid/controller.o \
+ $(top_builddir)/pid/util.o $(top_builddir)/pid/pidcontroller.o \
  $(top_builddir)/pid/thermalcontroller.o
 
 pid_fancontroller_unittest_SOURCES = pid_fancontroller_unittest.cpp
 pid_fancontroller_unittest_LDADD = $(top_builddir)/pid/ec/pid.o \
- $(top_builddir)/pid/util.o $(top_builddir)/pid/controller.o \
+ $(top_builddir)/pid/util.o $(top_builddir)/pid/pidcontroller.o \
  $(top_builddir)/pid/fancontroller.o
 
 dbus_passive_unittest_SOURCES = dbus_passive_unittest.cpp