Scripts and main daemon

This includes the scripts for the YAML parsing and the
main execution point.

Change-Id: If42154c621353b23370b63d4e58f6c75bca8b356
Signed-off-by: Patrick Venture <venture@google.com>
diff --git a/scripts/README b/scripts/README
new file mode 100644
index 0000000..3b732e8
--- /dev/null
+++ b/scripts/README
@@ -0,0 +1,25 @@
+# Sensor Config
+This program is only meant to control fans given thermal sensor readings.
+
+All sensors in phosphor-dbus-interfaces for OpenBMC use Sensor.Value as their
+accessor.  This provides read-only access to information.  The goal of the
+configuration is to specify how it can be read and if it's a fan, how the PID
+output can be written.  Initially there'll only be sysfs and passive dbus
+access.  If a writepath for a sensor is a dbus path, then the system will need
+to verify which Control.Fan* interfaces is registered and send values to the
+Target property of that interface.
+
+The min/max specified are to range a writePercent to the sensor.  The current
+FanController object outputs the new fan speed goal as a PWM percentage.  Other
+fan PID control objects may not, and they can leave the fields as 0 & 0.
+
+The only requirement for a sensor is that it isn't writeonly.  Only fans are
+expected to have a writepath set, and in this current version non-fan sensors
+are assumed readonly. 
+
+The sensor names are unique across all zones.
+
+# PID Config
+
+The PID configuation is a list of PIDs per zone.
+
diff --git a/scripts/pid-example.txt b/scripts/pid-example.txt
new file mode 100644
index 0000000..3549c8d
--- /dev/null
+++ b/scripts/pid-example.txt
@@ -0,0 +1,24 @@
+0x01:  /* zone ID */
+  fan2-6: /* PID name */
+    type: fan /* Type of PID, fan, temp, or margin. */
+    inputs:   /* Sensor names that are inputs for the PID */
+      fan2
+      fan6
+    /* For temp/margin PIDs this is the set-point, ignored otherwise (float) */
+    set-point: 90.0
+    pid: /* The PID calculation configuration. */
+      sampleperiod: 0.1        /* The input sample period. (float) */
+      p_coefficient: 0.01      /* The proportional coefficient. (float) */
+      i_coefficient: 0.001     /* The integral coefficient. (float) */
+      /* The feed-forward offset coefficient. (float) */
+      ff_off_coefficient: 0.0
+      /* The feed-forward gain coefficient. (float) */
+      ff_gain_coefficient: 0.0
+      i_limit:   /* The integral limit clamp, min, max (float) */
+        min: 0
+        max: 100
+      out_limit: /* the PID output clamp, min, max (float) */
+        min: 0
+        max: 100
+      slew_neg: -100 /* The slew negative value. (float) */
+      slew_pos: 0 /* The slew positive value. (float) */
diff --git a/scripts/pid-example.yaml b/scripts/pid-example.yaml
new file mode 100644
index 0000000..ea89b26
--- /dev/null
+++ b/scripts/pid-example.yaml
@@ -0,0 +1,59 @@
+0x01:
+  fan2-6:
+    type: fan
+    inputs:
+      fan2
+      fan6
+    set-point: 90.0
+    pid:
+      sampleperiod: 0.1
+      p_coefficient: 0.01
+      i_coefficient: 0.001
+      ff_off_coefficient: 0.0
+      ff_gain_coefficient: 0.0
+      i_limit:
+        min: 0
+        max: 100
+      out_limit:
+        min: 0
+        max: 100
+      slew_neg: 0
+      slew_pos: 0
+  temp1:
+    type: temp
+    inputs:
+      temp1
+    set-point: 30.0
+    pid:
+      sampleperiod: 1
+      p_coefficient: 94.0
+      i_coefficient: 2.0
+      ff_off_coefficient: 0.0
+      ff_gain_coefficient: 0.0
+      i_limit:
+        min: 3000
+        max: 10000
+      out_limit:
+        min: 3000
+        max: 10000
+      slew_neg: 0
+      slew_pos: 0
+  sluggish0:
+    type: margin
+    inputs:
+      sluggish0
+    set-point: 50
+    pid:
+      sampleperiod: 1
+      p_coefficient: 94.0
+      i_coefficient: 2.0
+      ff_off_coefficient: 0.0
+      ff_gain_coefficient: 0.0
+      i_limit:
+        min: 3000
+        max: 10000
+      out_limit:
+        min: 3000
+        max: 10000
+      slew_neg: 0
+      slew_pos: 0
diff --git a/scripts/pid_gen.py b/scripts/pid_gen.py
new file mode 100644
index 0000000..cd62199
--- /dev/null
+++ b/scripts/pid_gen.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import yaml
+import argparse
+from mako.template import Template
+
+
+def generate_cpp(sensor_yaml, output_dir):
+    with open(os.path.join(script_dir, sensor_yaml), 'r') as f:
+        ifile = yaml.safe_load(f)
+        if not isinstance(ifile, dict):
+            ifile = {}
+
+        # Render the mako template
+
+        t = Template(filename=os.path.join(
+                     script_dir,
+                     "writepid.mako.cpp"))
+
+        output_cpp = os.path.join(output_dir, "pidlist-gen.cpp")
+        with open(output_cpp, 'w') as fd:
+            fd.write(t.render(PIDDict=ifile))
+
+
+def main():
+
+    valid_commands = {
+        'generate-cpp': generate_cpp
+    }
+    parser = argparse.ArgumentParser(
+        description="IPMI Sensor parser and code generator")
+
+    parser.add_argument(
+        '-i', '--sensor_yaml', dest='sensor_yaml',
+        default='example.yaml', help='input sensor yaml file to parse')
+
+    parser.add_argument(
+        "-o", "--output-dir", dest="outputdir",
+        default=".",
+        help="output directory")
+
+    parser.add_argument(
+        'command', metavar='COMMAND', type=str,
+        choices=valid_commands.keys(),
+        help='Command to run.')
+
+    args = parser.parse_args()
+
+    if (not (os.path.isfile(os.path.join(script_dir, args.sensor_yaml)))):
+        sys.exit("Can not find input yaml file " + args.sensor_yaml)
+
+    function = valid_commands[args.command]
+    function(args.sensor_yaml, args.outputdir)
+
+
+if __name__ == '__main__':
+    script_dir = os.path.dirname(os.path.realpath(__file__))
+    main()
diff --git a/scripts/sensor-example.txt b/scripts/sensor-example.txt
new file mode 100644
index 0000000..0915163
--- /dev/null
+++ b/scripts/sensor-example.txt
@@ -0,0 +1,29 @@
+fan2: /* Name of the sensor. */
+  type: fan /* Type of sensor, fan, temp, margin */
+  /* How the sensor can be read[1] */
+  readpath: /xyz/openbmc_project/sensors/fan_tach/fan2
+  /* How the sensor can be set[2] */
+  writepath: /sys/class/hwmon/hwmon0/pwm1
+  /* The minimum value used for scaling writes (int64) */
+  min: 0
+  /* The maximum value used for scaling writes (int64) */
+  max: 255
+  /* The timeout value for the sensor, used for failsafe, 0 means no timeout
+   * (int64)
+   */
+  timeout: 0
+
+[1] readpath has multiple options:
+* If it has "/xyz/openbmc_project/extsensors/" in it, it's an EXTERNAL or
+  host-provided sensor.
+* If it has "/xyz/openbmc_project/" in it, it's a sensor whose value is
+  received passively over dbus.
+* If it has "/sys/" in it, it's a sensor read directly from sysfs.
+
+[2]
+* This can be left blank if the sensor is read-only.
+* If it has "/sys/" in it, it's a sensor written to sysfs.
+  * If min and max are non-zero, it'll convert the value to within the range.
+    and output that modified value.  So, if it receives a value of .90 and min
+    is 0, and max is 255, it'll convert that to a value of 229.5 that is then
+    cast to int64_t.
diff --git a/scripts/sensor-example.yaml b/scripts/sensor-example.yaml
new file mode 100644
index 0000000..7378bff
--- /dev/null
+++ b/scripts/sensor-example.yaml
@@ -0,0 +1,31 @@
+fan2:
+  type: fan
+  readpath: /xyz/openbmc_project/sensors/fan_tach/fan2
+  writepath: /sys/class/hwmon/hwmon0/pwm1
+  min: 0
+  max: 255
+  timeout: 0
+
+fan6:
+  type: fan
+  readpath: /xyz/openbmc_project/sensors/fan_tach/fan6
+  writepath:
+  min: 0
+  max: 0
+  timeout: 0
+
+temp1:
+  type: temp
+  readpath: /xyz/openbmc_project/sensors/temperature/temp1
+  writepath: 
+  min: 0
+  max: 0
+  timeout: 0
+
+sluggish0:
+  type: margin
+  readpath: /xyz/openbmc_project/extsensors/margin/sluggish0
+  writepath:
+  min: 0
+  max: 0
+  timeout: 0
diff --git a/scripts/sensor_gen.py b/scripts/sensor_gen.py
new file mode 100644
index 0000000..7e9c6ca
--- /dev/null
+++ b/scripts/sensor_gen.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import yaml
+import argparse
+from mako.template import Template
+
+
+def generate_cpp(sensor_yaml, output_dir):
+    with open(os.path.join(script_dir, sensor_yaml), 'r') as f:
+        ifile = yaml.safe_load(f)
+        if not isinstance(ifile, dict):
+            ifile = {}
+
+        # Render the mako template
+
+        t = Template(filename=os.path.join(
+                     script_dir,
+                     "writesensor.mako.cpp"))
+
+        output_cpp = os.path.join(output_dir, "sensorlist-gen.cpp")
+        with open(output_cpp, 'w') as fd:
+            fd.write(t.render(sensorDict=ifile))
+
+
+def main():
+
+    valid_commands = {
+        'generate-cpp': generate_cpp
+    }
+    parser = argparse.ArgumentParser(
+        description="IPMI Sensor parser and code generator")
+
+    parser.add_argument(
+        '-i', '--sensor_yaml', dest='sensor_yaml',
+        default='example.yaml', help='input sensor yaml file to parse')
+
+    parser.add_argument(
+        "-o", "--output-dir", dest="outputdir",
+        default=".",
+        help="output directory")
+
+    parser.add_argument(
+        'command', metavar='COMMAND', type=str,
+        choices=valid_commands.keys(),
+        help='Command to run.')
+
+    args = parser.parse_args()
+
+    if (not (os.path.isfile(os.path.join(script_dir, args.sensor_yaml)))):
+        sys.exit("Can not find input yaml file " + args.sensor_yaml)
+
+    function = valid_commands[args.command]
+    function(args.sensor_yaml, args.outputdir)
+
+
+if __name__ == '__main__':
+    script_dir = os.path.dirname(os.path.realpath(__file__))
+    main()
\ No newline at end of file
diff --git a/scripts/writepid.mako.cpp b/scripts/writepid.mako.cpp
new file mode 100644
index 0000000..f168e10
--- /dev/null
+++ b/scripts/writepid.mako.cpp
@@ -0,0 +1,48 @@
+## This file is a template.  The comment below is emitted
+## into the rendered file; feel free to edit this file.
+
+// !!! WARNING: This is GENERATED Code... Please do NOT edit !!!
+
+#include <map>
+#include "conf.hpp"
+
+std::map<int64_t, PIDConf> ZoneConfig = {
+% for zone in PIDDict.iterkeys():
+    % if zone:
+    {${zone},
+        {
+            % for key, details in PIDDict[zone].iteritems():
+            {"${key}",
+                {"${details['type']}",
+                 {
+                   % for item in details['inputs'].split():
+                   "${item}",
+                   % endfor
+                 },
+                 <%
+                      # If the PID type is a fan, set-point field is unused,
+                      # so just use a default of 0.  If the PID is not a type
+                      # of fan, require the set-point field.
+                      if 'fan' == details['type']:
+                          setpoint = 0
+                      else:
+                          setpoint = details['set-point']
+                  %>
+                 ${setpoint},
+                 {${details['pid']['sampleperiod']},
+                  ${details['pid']['p_coefficient']},
+                  ${details['pid']['i_coefficient']},
+                  ${details['pid']['ff_off_coefficient']},
+                  ${details['pid']['ff_gain_coefficient']},
+                  {${details['pid']['i_limit']['min']}, ${details['pid']['i_limit']['max']}},
+                  {${details['pid']['out_limit']['min']}, ${details['pid']['out_limit']['max']}},
+                  ${details['pid']['slew_neg']},
+                  ${details['pid']['slew_pos']}},
+                },
+            },
+            % endfor
+        },
+    },
+   % endif
+% endfor
+};
diff --git a/scripts/writesensor.mako.cpp b/scripts/writesensor.mako.cpp
new file mode 100644
index 0000000..8be30bb
--- /dev/null
+++ b/scripts/writesensor.mako.cpp
@@ -0,0 +1,36 @@
+## This file is a template.  The comment below is emitted
+## into the rendered file; feel free to edit this file.
+
+// !!! WARNING: This is GENERATED Code... Please do NOT edit !!!
+
+#include <map>
+#include "conf.hpp"
+
+std::map<std::string, struct sensor> SensorConfig = {
+% for key in sensorDict.iterkeys():
+   % if key:
+   <%
+           sensor = sensorDict[key]
+           type = sensor["type"]
+           readpath = sensor["readpath"]
+           writepath = sensor.get("writepath", "")
+           min = sensor.get("min", 0)
+           max = sensor.get("max", 0)
+           # Presently only thermal inputs have their timeout
+           # checked, but we should default it as 2s, which is
+           # the previously hard-coded value.
+           # If it's a fan sensor though, let's set the default
+           # to 0.
+           if type == "fan":
+               timeout = sensor.get("timeout", 0)
+           else:
+               timeout = sensor.get("timeout", 2)
+   %>
+    {"${key}",
+        {"${type}","${readpath}","${writepath}", ${min}, ${max}, ${timeout}},
+    },
+   % endif
+% endfor
+};
+
+
diff --git a/scripts/writezone.mako.cpp b/scripts/writezone.mako.cpp
new file mode 100644
index 0000000..57b9908
--- /dev/null
+++ b/scripts/writezone.mako.cpp
@@ -0,0 +1,22 @@
+## This file is a template.  The comment below is emitted
+## into the rendered file; feel free to edit this file.
+
+// !!! WARNING: This is GENERATED Code... Please do NOT edit !!!
+
+#include <map>
+#include "conf.hpp"
+
+std::map<int64_t, struct zone> ZoneDetailsConfig = {
+% for zone in ZoneDict.iterkeys():
+   % if zone:
+   <%
+           zConf = ZoneDict[zone]
+           min = zConf["minthermalrpm"]
+           percent = zConf["failsafepercent"]
+   %>
+    {${zone},
+        {${min}, ${percent}},
+    },
+   % endif
+% endfor
+};
diff --git a/scripts/zone-example.txt b/scripts/zone-example.txt
new file mode 100644
index 0000000..752fc01
--- /dev/null
+++ b/scripts/zone-example.txt
@@ -0,0 +1,4 @@
+0x01: /* The zone ID */
+  minthermalrpm: 3000.0 /* The minimum thermal RPM value. (float) */
+  /* The percent to use when the zone is in fail-safe mode. (float) */
+  failsafepercent: 90.0
diff --git a/scripts/zone-example.yaml b/scripts/zone-example.yaml
new file mode 100644
index 0000000..ef422a5
--- /dev/null
+++ b/scripts/zone-example.yaml
@@ -0,0 +1,3 @@
+0x01:
+  minthermalrpm: 3000.0
+  failsafepercent: 90.0
diff --git a/scripts/zone_gen.py b/scripts/zone_gen.py
new file mode 100644
index 0000000..a98a3b5
--- /dev/null
+++ b/scripts/zone_gen.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import yaml
+import argparse
+from mako.template import Template
+
+
+def generate_cpp(zoneinfo_yaml, output_dir):
+    with open(os.path.join(script_dir, zoneinfo_yaml), 'r') as f:
+        ifile = yaml.safe_load(f)
+        if not isinstance(ifile, dict):
+            ifile = {}
+
+        # Render the mako template
+
+        t = Template(filename=os.path.join(
+                     script_dir,
+                     "writezone.mako.cpp"))
+
+        output_cpp = os.path.join(output_dir, "zoneinfo-gen.cpp")
+        with open(output_cpp, 'w') as fd:
+            fd.write(t.render(ZoneDict=ifile))
+
+
+def main():
+
+    valid_commands = {
+        'generate-cpp': generate_cpp
+    }
+    parser = argparse.ArgumentParser(
+        description="IPMI Zone info parser and code generator")
+
+    parser.add_argument(
+        '-i', '--zoneinfo_yaml', dest='zoneinfo_yaml',
+        default='example.yaml', help='input zone yaml file to parse')
+
+    parser.add_argument(
+        "-o", "--output-dir", dest="outputdir",
+        default=".",
+        help="output directory")
+
+    parser.add_argument(
+        'command', metavar='COMMAND', type=str,
+        choices=valid_commands.keys(),
+        help='Command to run.')
+
+    args = parser.parse_args()
+
+    if (not (os.path.isfile(os.path.join(script_dir, args.zoneinfo_yaml)))):
+        sys.exit("Can not find input yaml file " + args.zoneinfo_yaml)
+
+    function = valid_commands[args.command]
+    function(args.zoneinfo_yaml, args.outputdir)
+
+
+if __name__ == '__main__':
+    script_dir = os.path.dirname(os.path.realpath(__file__))
+    main()