Implementing the TempToMargin feature

Wrapping the input name std::string in a new structure SensorInput, so
that the TempToMargin information can be cleanly carried along with
it, all the way down to the PID input processing layer where it is
needed. This allows the conversion to be done just-in-time before the
temperature reading is interpreted, minimizing the blast radius of
this change. Nonetheless, because of the type change, there was a
somewhat large blast radius to implement this feature.

The design, and the documentation, is already here:
https://github.com/openbmc/phosphor-pid-control/issues/23

Tested: Added unit tests for JSON parsing and for proper execution
of the TempToMargin feature. They pass. Ran it locally, on our
appropriately-configured system, and it seems to work for me.

Change-Id: I598ba485195aaa70c26e91a1da3ab88fff8c3a4c
Signed-off-by: Josh Lehan <krellan@google.com>
diff --git a/conf.hpp b/conf.hpp
index f3811e4..0e26f55 100644
--- a/conf.hpp
+++ b/conf.hpp
@@ -3,12 +3,14 @@
 #include "pid/ec/pid.hpp"
 #include "pid/ec/stepwise.hpp"
 
+#include <limits>
 #include <map>
 #include <string>
 #include <vector>
 
 namespace pid_control
 {
+
 namespace conf
 {
 
@@ -31,12 +33,23 @@
 };
 
 /*
+ * Structure for decorating an input sensor's name with additional
+ * information, to help out with TempToMargin conversion.
+ */
+struct SensorInput
+{
+    std::string name;
+    double convertMarginZero = std::numeric_limits<double>::quiet_NaN();
+    bool convertTempToMargin = false;
+};
+
+/*
  * Structure for holding the configuration of a PID.
  */
 struct ControllerInfo
 {
     std::string type;                // fan or margin or temp?
-    std::vector<std::string> inputs; // one or more sensors.
+    std::vector<SensorInput> inputs; // one or more sensors.
     double setpoint;                 // initial setpoint for thermal.
     ec::pidinfo pidInfo;             // pid details
     ec::StepwiseInfo stepwiseInfo;
@@ -74,4 +87,5 @@
 constexpr bool DEBUG = false; // enable to print found configuration
 
 } // namespace conf
+
 } // namespace pid_control
diff --git a/dbus/dbusconfiguration.cpp b/dbus/dbusconfiguration.cpp
index d784b2c..2473252 100644
--- a/dbus/dbusconfiguration.cpp
+++ b/dbus/dbusconfiguration.cpp
@@ -338,7 +338,13 @@
         {
             interface = thresholds::criticalInterface;
         }
-        const std::string& path = sensorConfig.at(info.inputs.front()).readPath;
+
+        // Although this checks only the first vector element for the
+        // named threshold, it is OK, because the SetPointOffset parser
+        // splits up the input into individual vectors, each with only a
+        // single element, if it detects that SetPointOffset is in use.
+        const std::string& path =
+            sensorConfig.at(info.inputs.front().name).readPath;
 
         DbusHelper helper(sdbusplus::bus::new_system());
         std::string service = helper.getService(interface, path);
@@ -848,19 +854,32 @@
                     }
                 }
 
+                std::vector<double> inputTempToMargin;
+
+                auto findTempToMargin = base.find("TempToMargin");
+                if (findTempToMargin != base.end())
+                {
+                    inputTempToMargin =
+                        std::get<std::vector<double>>(findTempToMargin->second);
+                }
+
+                std::vector<pid_control::conf::SensorInput> sensorInputs =
+                    spliceInputs(inputSensorNames, inputTempToMargin);
+
                 if (offsetType.empty())
                 {
                     conf::ControllerInfo& info = conf[pidName];
-                    info.inputs = std::move(inputSensorNames);
+                    info.inputs = std::move(sensorInputs);
                     populatePidInfo(bus, base, info, nullptr, sensorConfig);
                 }
                 else
                 {
                     // we have to split up the inputs, as in practice t-control
                     // values will differ, making setpoints differ
-                    for (const std::string& input : inputSensorNames)
+                    for (const pid_control::conf::SensorInput& input :
+                         sensorInputs)
                     {
-                        conf::ControllerInfo& info = conf[input];
+                        conf::ControllerInfo& info = conf[input.name];
                         info.inputs.emplace_back(input);
                         populatePidInfo(bus, base, info, &offsetType,
                                         sensorConfig);
@@ -932,7 +951,17 @@
                     continue;
                 }
                 conf::ControllerInfo& info = conf[pidName];
-                info.inputs = std::move(inputs);
+
+                std::vector<double> inputTempToMargin;
+
+                auto findTempToMargin = base.find("TempToMargin");
+                if (findTempToMargin != base.end())
+                {
+                    inputTempToMargin =
+                        std::get<std::vector<double>>(findTempToMargin->second);
+                }
+
+                info.inputs = spliceInputs(inputs, inputTempToMargin);
 
                 info.type = "stepwise";
                 info.stepwiseInfo.ts = 1.0; // currently unused
diff --git a/pid/builder.cpp b/pid/builder.cpp
index 8525073..bae0e81 100644
--- a/pid/builder.cpp
+++ b/pid/builder.cpp
@@ -23,6 +23,7 @@
 #include "pid/thermalcontroller.hpp"
 #include "pid/zone.hpp"
 #include "pid/zone_interface.hpp"
+#include "util.hpp"
 
 #include <sdbusplus/bus.hpp>
 
@@ -82,7 +83,7 @@
         // For each PID create a Controller and a Sensor.
         for (const auto& [name, info] : pidConfig)
         {
-            std::vector<std::string> inputs;
+            std::vector<pid_control::conf::SensorInput> inputs;
             std::cerr << "PID name: " << name << "\n";
 
             /*
@@ -94,11 +95,11 @@
                 for (const auto& i : info.inputs)
                 {
                     inputs.push_back(i);
-                    zone->addFanInput(i);
+                    zone->addFanInput(i.name);
                 }
 
-                auto pid = FanController::createFanPid(zone.get(), name, inputs,
-                                                       info.pidInfo);
+                auto pid = FanController::createFanPid(
+                    zone.get(), name, splitNames(inputs), info.pidInfo);
                 zone->addFanPID(std::move(pid));
                 zone->addPidFailSafePercent(name, info.failSafePercent);
             }
@@ -107,7 +108,7 @@
                 for (const auto& i : info.inputs)
                 {
                     inputs.push_back(i);
-                    zone->addThermalInput(i);
+                    zone->addThermalInput(i.name);
                 }
 
                 auto pid = ThermalController::createThermalPid(
@@ -125,10 +126,10 @@
                 for (const auto& i : info.inputs)
                 {
                     inputs.push_back(i);
-                    zone->addThermalInput(i);
+                    zone->addThermalInput(i.name);
                 }
                 auto stepwise = StepwiseController::createStepwiseController(
-                    zone.get(), name, inputs, info.stepwiseInfo);
+                    zone.get(), name, splitNames(inputs), info.stepwiseInfo);
                 zone->addThermalPID(std::move(stepwise));
                 zone->addPidControlProcess(
                     name, info.type, info.setpoint, modeControlBus,
@@ -139,7 +140,12 @@
             std::cerr << "inputs: ";
             for (const auto& i : inputs)
             {
-                std::cerr << i << ", ";
+                std::cerr << i.name;
+                if (i.convertTempToMargin)
+                {
+                    std::cerr << "[" << i.convertMarginZero << "]";
+                }
+                std::cerr << ", ";
             }
             std::cerr << "\n";
         }
diff --git a/pid/buildjson.cpp b/pid/buildjson.cpp
index 57ea943..8396338 100644
--- a/pid/buildjson.cpp
+++ b/pid/buildjson.cpp
@@ -17,10 +17,12 @@
 #include "pid/buildjson.hpp"
 
 #include "conf.hpp"
+#include "util.hpp"
 
 #include <nlohmann/json.hpp>
 
 #include <iostream>
+#include <limits>
 #include <map>
 #include <tuple>
 
@@ -31,12 +33,25 @@
 
 namespace conf
 {
+
 void from_json(const json& j, conf::ControllerInfo& c)
 {
+    std::vector<std::string> inputNames;
+
     j.at("type").get_to(c.type);
-    j.at("inputs").get_to(c.inputs);
+    j.at("inputs").get_to(inputNames);
     j.at("setpoint").get_to(c.setpoint);
 
+    std::vector<double> inputTempToMargin;
+
+    auto findTempToMargin = j.find("tempToMargin");
+    if (findTempToMargin != j.end())
+    {
+        findTempToMargin->get_to(inputTempToMargin);
+    }
+
+    c.inputs = spliceInputs(inputNames, inputTempToMargin);
+
     /* TODO: We need to handle parsing other PID controller configurations.
      * We can do that by checking for different keys and making the decision
      * accordingly.
@@ -135,6 +150,7 @@
         c.stepwiseInfo.negativeHysteresis = negativeHysteresisValue;
     }
 }
+
 } // namespace conf
 
 inline void getCycleTimeSetting(const auto& zone, const int id,
diff --git a/pid/ec/pid.hpp b/pid/ec/pid.hpp
index 378f93c..9dac6a4 100644
--- a/pid/ec/pid.hpp
+++ b/pid/ec/pid.hpp
@@ -15,14 +15,14 @@
 } limits_t;
 
 /* Note: If you update these structs you need to update the copy code in
- * pid/util.cpp.
+ * pid/util.cpp and the initialization code in pid/buildjson.hpp files.
  */
 typedef struct
 {
     bool initialized;         // has pid been initialized
 
     double ts;                // sample time in seconds
-    double integral;          // intergal of error
+    double integral;          // integral of error
     double lastOutput;        // value of last output
     double lastError;         // value of last error
 
diff --git a/pid/pidcontroller.hpp b/pid/pidcontroller.hpp
index c3e9999..05cbd71 100644
--- a/pid/pidcontroller.hpp
+++ b/pid/pidcontroller.hpp
@@ -29,6 +29,7 @@
         _pid_info.lastOutput = static_cast<double>(0.0);
         _pid_info.proportionalCoeff = static_cast<double>(0.0);
         _pid_info.integralCoeff = static_cast<double>(0.0);
+        _pid_info.derivativeCoeff = static_cast<double>(0.0);
         _pid_info.feedFwdOffset = static_cast<double>(0.0);
         _pid_info.feedFwdGain = static_cast<double>(0.0);
         _pid_info.integralLimit.min = static_cast<double>(0.0);
diff --git a/pid/thermalcontroller.cpp b/pid/thermalcontroller.cpp
index 357437b..ddb5893 100644
--- a/pid/thermalcontroller.cpp
+++ b/pid/thermalcontroller.cpp
@@ -55,7 +55,7 @@
 
 std::unique_ptr<PIDController> ThermalController::createThermalPid(
     ZoneInterface* owner, const std::string& id,
-    const std::vector<std::string>& inputs, double setpoint,
+    const std::vector<pid_control::conf::SensorInput>& inputs, double setpoint,
     const ec::pidinfo& initial, const ThermalType& type)
 {
     // ThermalController requires at least 1 input
@@ -101,12 +101,12 @@
         throw ControllerBuildException("Unrecognized ThermalType");
     }
 
-    std::string leaderName = *(_inputs.begin());
+    std::string leaderName = _inputs.begin()->name;
 
     bool acceptable = false;
     for (const auto& in : _inputs)
     {
-        double cachedValue = _owner->getCachedValue(in);
+        double cachedValue = _owner->getCachedValue(in.name);
 
         // Less than 0 is perfectly OK for temperature, but must not be NAN
         if (!(std::isfinite(cachedValue)))
@@ -114,6 +114,30 @@
             continue;
         }
 
+        // Perform TempToMargin conversion before further processing
+        if (type == ThermalType::margin)
+        {
+            if (in.convertTempToMargin)
+            {
+                if (!(std::isfinite(in.convertMarginZero)))
+                {
+                    throw ControllerBuildException("Unrecognized TempToMargin");
+                }
+
+                double marginValue = in.convertMarginZero - cachedValue;
+
+                if (debugEnabled)
+                {
+                    std::cerr << "Converting temp to margin: temp "
+                              << cachedValue << ", Tjmax "
+                              << in.convertMarginZero << ", margin "
+                              << marginValue << "\n";
+                }
+
+                cachedValue = marginValue;
+            }
+        }
+
         double oldValue = value;
 
         if (doSummation)
@@ -127,7 +151,7 @@
 
         if (oldValue != value)
         {
-            leaderName = in;
+            leaderName = in.name;
             _owner->updateThermalPowerDebugInterface(_id, leaderName, value, 0);
         }
 
@@ -136,8 +160,11 @@
 
     if (!acceptable)
     {
-        // While not optimal, zero is better than garbage
-        value = 0;
+        // If none of the inputs were acceptable, use the setpoint as
+        // the input value. This will continue to run the PID loop, but
+        // make it a no-op, as the error will be zero. This provides safe
+        // behavior until the inputs become acceptable.
+        value = setptProc();
     }
 
     if (debugEnabled)
diff --git a/pid/thermalcontroller.hpp b/pid/thermalcontroller.hpp
index 752c105..ac551d2 100644
--- a/pid/thermalcontroller.hpp
+++ b/pid/thermalcontroller.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "conf.hpp"
 #include "ec/pid.hpp"
 #include "pidcontroller.hpp"
 
@@ -44,14 +45,13 @@
 class ThermalController : public PIDController
 {
   public:
-    static std::unique_ptr<PIDController>
-        createThermalPid(ZoneInterface* owner, const std::string& id,
-                         const std::vector<std::string>& inputs,
-                         double setpoint, const ec::pidinfo& initial,
-                         const ThermalType& type);
+    static std::unique_ptr<PIDController> createThermalPid(
+        ZoneInterface* owner, const std::string& id,
+        const std::vector<pid_control::conf::SensorInput>& inputs,
+        double setpoint, const ec::pidinfo& initial, const ThermalType& type);
 
     ThermalController(const std::string& id,
-                      const std::vector<std::string>& inputs,
+                      const std::vector<pid_control::conf::SensorInput>& inputs,
                       const ThermalType& type, ZoneInterface* owner) :
         PIDController(id, owner),
         _inputs(inputs), type(type)
@@ -62,7 +62,7 @@
     void outputProc(double value) override;
 
   private:
-    std::vector<std::string> _inputs;
+    std::vector<pid_control::conf::SensorInput> _inputs;
     ThermalType type;
 };
 
diff --git a/test/meson.build b/test/meson.build
index 966334e..2513278 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -42,7 +42,8 @@
                               '../dbus/dbusutil.cpp'],
     'dbus_util_unittest': ['../dbus/dbusutil.cpp'],
     'json_parse_unittest': ['../buildjson/buildjson.cpp'],
-    'pid_json_unittest': ['../pid/buildjson.cpp'],
+    'pid_json_unittest': ['../pid/buildjson.cpp',
+                          '../util.cpp'],
     'pid_fancontroller_unittest': ['../pid/ec/pid.cpp',
                                    '../pid/ec/logging.cpp',
                                    '../pid/fancontroller.cpp',
diff --git a/test/pid_json_unittest.cpp b/test/pid_json_unittest.cpp
index c76849a..59db343 100644
--- a/test/pid_json_unittest.cpp
+++ b/test/pid_json_unittest.cpp
@@ -73,6 +73,71 @@
     EXPECT_DOUBLE_EQ(zoneConfig[1].minThermalOutput, 3000.0);
 }
 
+TEST(ZoneFromJson, marginZone)
+{
+    // Parse a valid configuration with one zone and one PID.
+    // This is a margin zone, and has both kinds of temperature
+    // sensors in it, absolute temperature and margin temperature.
+    // Tests that TempToMargin is parsed correctly.
+
+    std::map<int64_t, conf::PIDConf> pidConfig;
+    std::map<int64_t, conf::ZoneConfig> zoneConfig;
+
+    auto j2 = R"(
+      {
+        "zones" : [{
+          "id": 1,
+          "minThermalOutput": 3000.0,
+          "failsafePercent": 75.0,
+          "pids": [{
+            "name": "myPid",
+            "type": "margin",
+            "inputs": ["absolute0", "absolute1", "margin0", "margin1"],
+            "tempToMargin": [
+              85.0,
+              100.0
+            ],
+            "setpoint": 10.0,
+            "pid": {
+              "samplePeriod": 0.1,
+              "proportionalCoeff": 0.0,
+              "integralCoeff": 0.0,
+              "feedFwdOffsetCoeff": 0.0,
+              "feedFwdGainCoeff": 0.010,
+              "integralLimit_min": 0.0,
+              "integralLimit_max": 0.0,
+              "outLim_min": 30.0,
+              "outLim_max": 100.0,
+              "slewNeg": 0.0,
+              "slewPos": 0.0
+            }
+          }]
+        }]
+      }
+    )"_json;
+
+    std::tie(pidConfig, zoneConfig) = buildPIDsFromJson(j2);
+    EXPECT_EQ(pidConfig.size(), static_cast<u_int64_t>(1));
+    EXPECT_EQ(zoneConfig.size(), static_cast<u_int64_t>(1));
+
+    EXPECT_EQ(pidConfig[1]["myPid"].type, "margin");
+    EXPECT_DOUBLE_EQ(zoneConfig[1].minThermalOutput, 3000.0);
+
+    EXPECT_EQ(pidConfig[1]["myPid"].inputs[0].name, "absolute0");
+    EXPECT_DOUBLE_EQ(pidConfig[1]["myPid"].inputs[0].convertMarginZero, 85.0);
+    EXPECT_EQ(pidConfig[1]["myPid"].inputs[0].convertTempToMargin, true);
+
+    EXPECT_EQ(pidConfig[1]["myPid"].inputs[1].name, "absolute1");
+    EXPECT_DOUBLE_EQ(pidConfig[1]["myPid"].inputs[1].convertMarginZero, 100.0);
+    EXPECT_EQ(pidConfig[1]["myPid"].inputs[1].convertTempToMargin, true);
+
+    EXPECT_EQ(pidConfig[1]["myPid"].inputs[2].name, "margin0");
+    EXPECT_EQ(pidConfig[1]["myPid"].inputs[2].convertTempToMargin, false);
+
+    EXPECT_EQ(pidConfig[1]["myPid"].inputs[3].name, "margin1");
+    EXPECT_EQ(pidConfig[1]["myPid"].inputs[3].convertTempToMargin, false);
+}
+
 TEST(ZoneFromJson, oneZoneOnePidWithHysteresis)
 {
     // Parse a valid configuration with one zone and one PID and the PID uses
diff --git a/test/pid_thermalcontroller_unittest.cpp b/test/pid_thermalcontroller_unittest.cpp
index 6069154..7aee1e1 100644
--- a/test/pid_thermalcontroller_unittest.cpp
+++ b/test/pid_thermalcontroller_unittest.cpp
@@ -1,3 +1,4 @@
+#include "conf.hpp"
 #include "pid/ec/logging.hpp"
 #include "pid/ec/pid.hpp"
 #include "pid/thermalcontroller.hpp"
@@ -25,7 +26,7 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {"fleeting0"};
+    std::vector<pid_control::conf::SensorInput> inputs = {{"fleeting0"}};
     double setpoint = 10.0;
     ec::pidinfo initial;
 
@@ -41,7 +42,7 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {};
+    std::vector<pid_control::conf::SensorInput> inputs = {};
     double setpoint = 10.0;
     ec::pidinfo initial;
     std::unique_ptr<PIDController> p;
@@ -60,7 +61,7 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {"fleeting0"};
+    std::vector<pid_control::conf::SensorInput> inputs = {{"fleeting0"}};
     double setpoint = 10.0;
     ec::pidinfo initial;
 
@@ -79,7 +80,7 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {"fleeting0"};
+    std::vector<pid_control::conf::SensorInput> inputs = {{"fleeting0"}};
     double setpoint = 10.0;
     ec::pidinfo initial;
 
@@ -96,7 +97,7 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {"fleeting0"};
+    std::vector<pid_control::conf::SensorInput> inputs = {{"fleeting0"}};
     double setpoint = 10.0;
     ec::pidinfo initial;
 
@@ -117,7 +118,8 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {"fleeting0", "fleeting1"};
+    std::vector<pid_control::conf::SensorInput> inputs = {{"fleeting0"},
+                                                          {"fleeting1"}};
     double setpoint = 10.0;
     ec::pidinfo initial;
 
@@ -138,7 +140,8 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {"fleeting0", "fleeting1"};
+    std::vector<pid_control::conf::SensorInput> inputs = {{"fleeting0"},
+                                                          {"fleeting1"}};
     double setpoint = 10.0;
     ec::pidinfo initial;
 
@@ -159,7 +162,8 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {"fleeting0", "fleeting1"};
+    std::vector<pid_control::conf::SensorInput> inputs = {{"fleeting0"},
+                                                          {"fleeting1"}};
     double setpoint = 10.0;
     ec::pidinfo initial;
 
@@ -173,6 +177,29 @@
     EXPECT_EQ(15.0, p->inputProc());
 }
 
+TEST(ThermalControllerTest, InputProc_MultipleInputsTempToMargin)
+{
+    // This test verifies inputProc behaves as expected with multiple margin
+    // inputs and TempToMargin in use.
+
+    ZoneMock z;
+
+    std::vector<pid_control::conf::SensorInput> inputs = {
+        {"absolute0", 85.0, true}, {"margin1"}};
+    double setpoint = 10.0;
+    ec::pidinfo initial;
+
+    std::unique_ptr<PIDController> p = ThermalController::createThermalPid(
+        &z, "therm1", inputs, setpoint, initial, ThermalType::margin);
+    EXPECT_FALSE(p == nullptr);
+
+    EXPECT_CALL(z, getCachedValue(StrEq("absolute0"))).WillOnce(Return(82.0));
+    EXPECT_CALL(z, getCachedValue(StrEq("margin1"))).WillOnce(Return(5.0));
+
+    // 82 degrees temp, 85 degrees Tjmax => 3 degrees of safety margin
+    EXPECT_EQ(3.0, p->inputProc());
+}
+
 TEST(ThermalControllerTest, NegHysteresis_BehavesAsExpected)
 {
     // This test verifies Negative hysteresis behaves as expected by
@@ -181,7 +208,7 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {"fleeting0"};
+    std::vector<pid_control::conf::SensorInput> inputs = {{"fleeting0"}};
     double setpoint = 10.0;
     ec::pidinfo initial;
     initial.negativeHysteresis = 4.0;
@@ -214,7 +241,7 @@
 
     ZoneMock z;
 
-    std::vector<std::string> inputs = {"fleeting0"};
+    std::vector<pid_control::conf::SensorInput> inputs = {{"fleeting0"}};
     double setpoint = 10.0;
     ec::pidinfo initial;
     initial.positiveHysteresis = 5.0;
diff --git a/util.cpp b/util.cpp
index 411b761..87c4deb 100644
--- a/util.cpp
+++ b/util.cpp
@@ -69,7 +69,12 @@
             std::cout << "\t\t\t{";
             for (const auto& input : pidconf.second.inputs)
             {
-                std::cout << "\n\t\t\t" << input << ",\n";
+                std::cout << "\n\t\t\t" << input.name;
+                if (input.convertTempToMargin)
+                {
+                    std::cout << "[" << input.convertMarginZero << "]";
+                }
+                std::cout << ",\n";
             }
             std::cout << "\t\t\t}\n";
             std::cout << "\t\t\t" << pidconf.second.setpoint << ",\n";
@@ -96,4 +101,51 @@
     std::cout << "}\n\n";
 }
 
+std::vector<conf::SensorInput>
+    spliceInputs(const std::vector<std::string>& inputNames,
+                 const std::vector<double>& inputTempToMargin)
+{
+    std::vector<conf::SensorInput> results;
+
+    // Default to the TempToMargin feature disabled
+    for (const auto& inputName : inputNames)
+    {
+        conf::SensorInput newInput{
+            inputName, std::numeric_limits<double>::quiet_NaN(), false};
+
+        results.emplace_back(newInput);
+    }
+
+    size_t resultSize = results.size();
+    size_t marginSize = inputTempToMargin.size();
+
+    for (size_t index = 0; index < resultSize; ++index)
+    {
+        // If fewer doubles than strings, and vice versa, ignore remainder
+        if (index >= marginSize)
+        {
+            break;
+        }
+
+        // Both vectors have this index, combine both into SensorInput
+        results[index].convertMarginZero = inputTempToMargin[index];
+        results[index].convertTempToMargin = true;
+    }
+
+    return results;
+}
+
+std::vector<std::string>
+    splitNames(const std::vector<conf::SensorInput>& sensorInputs)
+{
+    std::vector<std::string> results;
+
+    for (const auto& sensorInput : sensorInputs)
+    {
+        results.emplace_back(sensorInput.name);
+    }
+
+    return results;
+}
+
 } // namespace pid_control
diff --git a/util.hpp b/util.hpp
index 362d517..0588934 100644
--- a/util.hpp
+++ b/util.hpp
@@ -41,6 +41,20 @@
 std::string FixupPath(std::string original);
 
 /*
+ * Splice together two vectors, "Inputs" and "TempToMargin" from JSON,
+ * into one vector of SensorInput structures containing info from both.
+ */
+std::vector<conf::SensorInput>
+    spliceInputs(const std::vector<std::string>& inputNames,
+                 const std::vector<double>& inputTempToMargin);
+
+/*
+ * Recovers the original "Inputs" vector from spliceInputs().
+ */
+std::vector<std::string>
+    splitNames(const std::vector<conf::SensorInput>& sensorInputs);
+
+/*
  * Dump active configuration.
  */
 void debugPrint(const std::map<std::string, conf::SensorConfig>& sensorConfig,