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/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;
                 }
             }
         }