]> hydra-www.ietfng.org Git - acmetensortoys-snakecontrol/commitdiff
Initial commit
authorNathaniel Wesley Filardo <nwfilardo@gmail.com>
Mon, 10 Apr 2017 17:06:30 +0000 (17:06 +0000)
committerNathaniel Wesley Filardo <nwfilardo@gmail.com>
Mon, 10 Apr 2017 17:06:30 +0000 (17:06 +0000)
13 files changed:
bin/monitor-graph-cgi.sh [new file with mode: 0755]
bin/monitor-log-cgi.sh [new file with mode: 0755]
bin/monitor.py [new file with mode: 0755]
bin/pidloop.py [new file with mode: 0644]
bin/pidmonitor.py [new file with mode: 0755]
bin/rpb.expect [new file with mode: 0755]
bin/rrdtool-creates.sh [new file with mode: 0755]
bin/serial.wrap [new file with mode: 0755]
data/.gitignore [new file with mode: 0644]
remind [new file with mode: 0644]
runit/pi-sc-monitor/log/run [new file with mode: 0755]
runit/pi-sc-monitor/run [new file with mode: 0755]
runit/pi-sc-remind/run [new file with mode: 0755]

diff --git a/bin/monitor-graph-cgi.sh b/bin/monitor-graph-cgi.sh
new file mode 100755 (executable)
index 0000000..fcb2fc6
--- /dev/null
@@ -0,0 +1,52 @@
+#!/bin/zsh
+
+DMX_SCALE=5
+DMX_SHIFT=20
+
+COMMON_ARGS=(
+       --end now
+        --width=960 --height=480
+        --lazy
+        --step=60 -v "degrees C" --upper-limit 32 --lower-limit 19 --rigid
+       --right-axis-label "DMX"
+       --right-axis ${DMX_SCALE}:-${DMX_SHIFT}
+        DEF:tempH=/home/pi/sc/data/heater-temp.rrd:temp:AVERAGE
+        DEF:tempHN=/home/pi/sc/data/hide-near-temp.rrd:temp:AVERAGE
+        DEF:dmxHN=/home/pi/sc/data/hide-near-dmx.rrd:dmx:AVERAGE
+        DEF:tempHF=/home/pi/sc/data/hide-far-temp.rrd:temp:AVERAGE
+        DEF:tempTN=/home/pi/sc/data/tank-near-temp.rrd:temp:AVERAGE
+        DEF:tempTF=/home/pi/sc/data/tank-far-temp.rrd:temp:AVERAGE
+       CDEF:scaled_dmxHN=dmxHN,${DMX_SHIFT},+,${DMX_SCALE},/
+       LINE:scaled_dmxHN\#000000:"dmx near"
+        LINE2:tempH\#FF0000:"heater"
+        LINE2:tempHN\#808000:"hide near"
+        LINE2:tempHF\#FF8000:"hide far"
+        LINE2:tempTN\#00FF00:"tank near"
+        LINE2:tempTF\#00FFFF:"tank far"
+        HRULE:30#800000
+        HRULE:25#8000FF
+        HRULE:21#0000FF
+)
+
+case "$QUERY_STRING" in
+       4h)  OUTFILE="4h.png";  PARAMS=(--start now-4h  --title "Melman Vivarium Sensor Data 4h" ) ;;
+       24h) OUTFILE="24h.png"; PARAMS=(--start now-24h --title "Melman Vivarium Sensor Data 24h") ;;
+       7d)  OUTFILE="7d.png";  PARAMS=(--start now-7d  --title "Melman Vivarium Sensor Data 7d" ) ;;
+       30d) OUTFILE="30d.png"; PARAMS=(--start now-30d --title "Melman Vivarium Sensor Data 30d") ;;
+       *)
+               cat <<HERE
+Content-Type: text/html
+Status: 404 Not Found
+
+<html><body>Invalid query parameter '${QUERY_STRING}'</body></html>
+HERE
+       exit 1
+esac
+
+rrdtool graph /home/pi/public_html/${OUTFILE} ${PARAMS[@]} ${COMMON_ARGS[@]} >&2
+
+cat <<HERE
+Location: /~pi/${OUTFILE}
+
+HERE
+
diff --git a/bin/monitor-log-cgi.sh b/bin/monitor-log-cgi.sh
new file mode 100755 (executable)
index 0000000..dbfa8c3
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/zsh
+
+echo "Content-Type: text/plain"
+echo
+exec tail -n 40 /home/pi/sc/data/monitor.log/current
diff --git a/bin/monitor.py b/bin/monitor.py
new file mode 100755 (executable)
index 0000000..087db67
--- /dev/null
@@ -0,0 +1,91 @@
+#!/usr/bin/python
+
+# Inspired by code found at http://wannabe.guru.org/scott/hobbies/temperature/
+
+import os
+import rrdtool
+import sched
+import time
+
+owdev_logname = {
+    "/sys/bus/w1/devices/28-011620c718ee/w1_slave" : "/home/pi/sc/data/hide-far-temp.rrd",
+    "/sys/bus/w1/devices/28-011620f10dee/w1_slave" : "/home/pi/sc/data/hide-near-temp.rrd",
+    "/sys/bus/w1/devices/28-02161e26acee/w1_slave" : "/home/pi/sc/data/tank-far-temp.rrd",
+    "/sys/bus/w1/devices/28-03164712aaff/w1_slave" : "/home/pi/sc/data/heater-temp.rrd",
+    "/sys/bus/w1/devices/28-0416526de6ff/w1_slave" : "/home/pi/sc/data/tank-near-temp.rrd",
+}
+
+owdev_thresholds_heater1 = {
+     "/sys/bus/w1/devices/28-011620c718ee/w1_slave" : (23.0, 25.0), # hide far
+     "/sys/bus/w1/devices/28-02161e26acee/w1_slave" : (21.0, 25.0), # tank air far
+     "/sys/bus/w1/devices/28-0416526de6ff/w1_slave" : (23.0, 27.0), # tank air near
+}
+
+# owdev_thresholds_heater2 = {
+#     "/sys/bus/w1/devices/28-011620f10dee/w1_slave" : (29.0, 29.5), # hide near
+# }
+
+def nop (*arg, **kwarg):
+    pass
+
+def with_ow_temp_fk_id(devfn, x, *arg, **kwarg):
+    print("WARNING: failed to read %s" % devfn)
+    return x
+
+def with_ow_temp(devfn, sk, fk, *arg, **kwarg):
+    with open(devfn) as devf:
+        devstr = devf.read()
+        devlines = devstr.split("\n")
+        if devlines[0].find("YES") > 0:
+            return sk(devfn,float((devlines[1].split(" ")[9])[2:]) / 1000, *arg, **kwarg)
+    return fk(devfn, *arg, **kwarg)
+
+
+def check_temps(sc):
+    print ("check temps init")
+    def check(devfn, temp, threshs, desire):
+        # Anything too hot: turn off the heater
+        if (temp > threshs[devfn][1]): desire = "OFF"
+        # No stated preference and something too cold, turn on the heater
+        elif (temp <= threshs[devfn][0] and desire == None): desire = "ON"
+        return desire
+
+    did_interact = False
+    desire = None
+    for devfn in owdev_thresholds_heater1:
+        desire = with_ow_temp(devfn, check, with_ow_temp_fk_id, owdev_thresholds_heater1, desire)
+    if desire is not None:
+        did_interact = True
+        print("Set heater 1 %s" % desire)
+        os.system("/home/pi/sc/bin/rpb.expect 4 %s | grep -A 5 -e 'Plug ' | tr -d '\015'" % desire)
+
+    # desire = None
+    # for devfn in owdev_thresholds_heater2:
+    #     desire = with_ow_temp(devfn, check, with_ow_temp_fk_id, owdev_thresholds_heater2, desire)
+    # if desire is not None:
+    #     did_interact = True
+    #     print("Set heater 2 %s" % desire)
+    #     os.system("/home/pi/sc/bin/rpb.expect 5 %s | grep -A 5 -e 'Plug ' | tr -d '\015'" % desire)
+
+    if not did_interact:
+        os.system("/home/pi/sc/bin/rpb.expect | grep -A 5 -e 'Plug ' | tr -d '\015'")
+
+    sc.enter(300, 1, check_temps, (sc,))
+    print ("check temps fini")
+
+def do_log_temp(devfn, temp):
+    print("temp log: %s => %f" % (devfn, temp))
+    rrdtool.update(owdev_logname[devfn], "N:" + ("%f" % temp))
+
+def read_temps(sc,itime): 
+    for devfn in owdev_logname: with_ow_temp(devfn, do_log_temp, nop)
+    itime = itime + 60
+    sc.enterabs(itime, 2, read_temps, (sc,itime))
+
+itime = time.time()
+s = sched.scheduler(time.time, time.sleep)
+s.enterabs(itime, 1, check_temps, (s,))
+s.enterabs(itime, 2, read_temps, (s,itime))
+
+print("Monitor starting...")
+s.run()
diff --git a/bin/pidloop.py b/bin/pidloop.py
new file mode 100644 (file)
index 0000000..6dd8317
--- /dev/null
@@ -0,0 +1,129 @@
+# Based on https://github.com/ivmech/ivPID/blob/master/PID.py
+# used under GPLv3 (or later)
+
+class PIDLoop(object):
+
+    def __init__ (self) :
+        self.kP = 0.0
+        self.kI = 0.0
+        self.kD = 0.0
+        self.kDDecay = 0.0
+        self.smooth_error_denom = 1.0
+
+        self.hard_max = None
+        self.hard_min = None
+
+        self.clear()
+
+        pass
+
+    def clear (self) :
+        self.last_upd_time    = None
+        self.last_error       = 0.0
+        self.sum_error        = 0.0
+        self.smooth_error     = 0.0
+
+    def setKP (self, kP) :
+        self.kP = kP
+
+    def setKD (self, kD, kDDecay) :
+        self.kD = kD
+        self.kDDecay = kDDecay
+        self.smooth_error_denom = 1 / (1 - kDDecay)
+
+    def setKI (self, kI) :
+        self.kI = kI
+
+    def setPoint (self, v) :
+        self.setpoint = v
+
+    def setHardMax (self, v) :
+        self.hard_max = v
+
+    def setHardMin (self, v) :
+        self.hard_min = v
+
+    def _value (self, error, edeltasmooth) :
+
+        print ("PID LOOP CONTRIBUTIONS: p=%r d=%r i=%r" % (self.kP * error, self.kD * edeltasmooth, self.kI * self.sum_error))
+
+        return (self.kP * error) + (self.kD * edeltasmooth) + (self.kI * self.sum_error)
+
+    def update (self, value, when) :
+        if self.setpoint is None :
+            raise ValueError("Setpoint not set")
+
+        # P
+        error = self.setpoint - value
+
+        # Time
+        tdelta = 0.0
+        if self.last_upd_time is not None :
+            tdelta = when - self.last_upd_time
+        if tdelta < 0 :
+            tdelta = 0.0
+
+        # D
+        edeltasmooth = 0.0
+        if tdelta > 0:
+            edeltasmooth = (error - (self.smooth_error/self.smooth_error_denom))/tdelta
+
+        # I (trapezoidal integration) with optional tests against a hard stop
+        ov = self._value(error, edeltasmooth)
+        sum_error_delta = tdelta*(self.last_error + error)/2.0
+
+        if self.hard_max is not None and ov >= self.hard_max and sum_error_delta > 0:
+            # Do not increment error; we're already slammed up against the hard limit
+            pass
+        elif self.hard_min is not None and ov <= self.hard_min and sum_error_delta < 0:
+            # Do not decrement error; we're already slammed up against the hard limit
+            pass
+        else if sum_error_delta != 0:
+            # Update sum_error and recompute the output value
+            self.sum_error += sum_error_delta
+            ov = self._value(error, edeltasmooth)
+
+        # Advance time
+        self.last_error = error
+        self.smooth_error *= self.kDDecay
+        self.smooth_error += error
+        self.last_upd_time = when
+
+        return ov
+
+    def __str__ (self) :
+        return "laste=%r smoothe=%r sume=%r" % (self.last_error, self.smooth_error/self.smooth_error_denom, self.sum_error)
+
+
+# A PID loop with hard limits on its behavior.  Designed as a kind of fail-safe should
+# oscillations get out of hand or such.
+class PIDThresh(PIDLoop):
+
+    def __init__ (self, lowO, lowH, lowS, midO, highS, highH, highO) :
+        self.low_out    = lowO
+        self.low_hard   = lowH
+        self.low_soft   = lowS
+        self.mid_out    = midO
+        self.high_soft  = highS
+        self.high_hard  = highH
+        self.high_out   = highO
+        self.override   = None
+        super(PIDThresh,self).__init__()
+
+    def clear (self) :
+        super(PIDThresh,self).clear()
+
+    def update (self, value, when) :
+        if value > self.high_hard :
+            self.override = self.high_out
+        elif value < self.low_hard :
+            self.override = self.low_out
+        elif self.override is not None and self.low_soft <= value <= self.high_soft :
+            self.override = None
+            super(PIDThresh,self).clear()
+
+        if self.override : return self.override
+        else : return super(PIDThresh, self).update(value,when)
+
+    def __str__ (self) :
+            return super(PIDThresh,self).__str__()
diff --git a/bin/pidmonitor.py b/bin/pidmonitor.py
new file mode 100755 (executable)
index 0000000..d15ca74
--- /dev/null
@@ -0,0 +1,74 @@
+#!/usr/bin/python
+
+# Inspired by code found at http://wannabe.guru.org/scott/hobbies/temperature/
+
+import os
+import sched
+import time
+import serial
+import pidloop
+import rrdtool
+
+dmxdev = serial.Serial("/dev/serial/by-id/usb-DMXking.com_DMX_USB_PRO_6A0SVM7J-if00-port0", 57600);
+
+loop_hidenear = pidloop.PIDThresh(128,26,28,0,31,33,-128)
+loop_hidenear.setPoint(30)
+loop_hidenear.setHardMax(128)
+loop_hidenear.setHardMin(-128)
+loop_hidenear.setKP(60.0)
+loop_hidenear.setKI(0.004)
+loop_hidenear.setKD(1000.0,0.95) 
+loop_hidenear.sum_error = -3000.0 # XXX Initialize offset point a bit
+
+def with_ow_temp_fk_id(devfn, loop, s, *arg, **kwarg):
+    print("WARNING: failed to read %s" % devfn)
+    return s # an ugly default
+
+def with_ow_temp(devfn, sk, fk, *arg, **kwarg):
+    with open(devfn) as devf:
+        devstr = devf.read()
+        devlines = devstr.split("\n")
+        if devlines[0].find("YES") > 0:
+            return sk(devfn,float((devlines[1].split(" ")[9])[2:]) / 1000, *arg, **kwarg)
+    return fk(devfn, *arg, **kwarg)
+
+def check_temps(sc):
+    sc.enter(10, 1, check_temps, (sc,))
+
+    def check(devfn, temp, loop, s, offset, rrd):
+        desire = 128.0 + loop.update(temp, time.time())
+
+        if desire < 0 :
+            desire = 0
+            loop.output = 0
+        elif desire > 255 :
+            desire = 255
+            loop.output = 255
+
+        rrdtool.update(rrd, "N:" + ("%f" % desire))
+
+        return s[:offset] + chr(int(desire+0.5)) + s[offset+1:]
+
+    # DMX conttrol string; initialize to all channels full off
+    # 7E -- header
+    # 06 -- type
+    # 03 00 -- payload length
+    # 00 -- channel 0 value (ignored by hardware)
+    # 00 -- channel 1 value
+    # 00 -- channel 2 value
+    # E7 -- footer
+    s = "\x7E\x06\x03\x00\x00\x00\x00\xE7"
+
+    # Drive loop
+    s = with_ow_temp("/sys/bus/w1/devices/28-011620f10dee/w1_slave",
+            check, with_ow_temp_fk_id, loop_hidenear, s, 5, "/home/pi/sc/data/hide-near-dmx.rrd")
+
+    print ("check temps fini: out=%r lhn=(%s)" % (s, loop_hidenear))
+    assert(dmxdev.write(s) == 8)
+
+itime = time.time()
+s = sched.scheduler(time.time, time.sleep)
+s.enterabs(itime, 1, check_temps, (s,))
+
+print("Monitor starting...")
+s.run()
diff --git a/bin/rpb.expect b/bin/rpb.expect
new file mode 100755 (executable)
index 0000000..01c0c9a
--- /dev/null
@@ -0,0 +1,32 @@
+#!/usr/bin/expect
+
+proc help {} {
+  global argv0
+  global actionnames
+  send_user "usage: $argv0 outlet {on|off|boot}\n"
+  exit 1
+}
+
+proc waitprompt {} {
+  expect {
+    "RPB+> " { }
+    timeout {
+      send_user " ===> TIMEOUT <===\n"
+      exit 1
+    }
+  } 
+}
+
+spawn /home/pi/sc/bin/serial.wrap [ exec readlink -f /dev/serial/by-path/platform-20980000.usb-usb-0:1.2:1.0-port0 ]
+set timeout 70
+send "\n"
+waitprompt
+
+if {$argc == 2} {
+  set outlet [lindex $argv 0]
+  set cmd [string toupper [lindex $argv 1]]
+
+  send "/$outlet $cmd\n"
+  set timeout 30
+  waitprompt
+}
diff --git a/bin/rrdtool-creates.sh b/bin/rrdtool-creates.sh
new file mode 100755 (executable)
index 0000000..78bc5fb
--- /dev/null
@@ -0,0 +1,41 @@
+#!/bin/zsh
+
+RRDS=(
+ heater-temp
+ hide-near-temp
+ hide-far-temp
+ tank-near-temp
+ tank-far-temp
+)
+
+ARGS=(
+ --no-overwrite
+ --step 60
+ DS:temp:GAUGE:900:-10:50
+ RRA:AVERAGE:0.5:1:525600
+ RRA:AVERAGE:0.25:60:87600
+ RRA:MIN:0.025:60:87600
+ RRA:MAX:0.025:60:87600
+)
+
+for rrd in ${=RRDS[@]}; do
+  rrdtool create /home/pi/sc/data/${rrd}.rrd ${=ARGS[@]}
+done
+
+RRDS=(
+  hide-near-dmx
+)
+
+ARGS=(
+ --no-overwrite
+ --step 10
+ DS:dmx:GAUGE:900:0:255
+ RRA:AVERAGE:0.5:6:525600
+ RRA:AVERAGE:0.25:360:87600
+ RRA:MIN:0.025:360:87600
+ RRA:MAX:0.025:360:87600
+)
+
+for rrd in ${=RRDS[@]}; do
+  rrdtool create /home/pi/sc/data/${rrd}.rrd ${=ARGS[@]}
+done
diff --git a/bin/serial.wrap b/bin/serial.wrap
new file mode 100755 (executable)
index 0000000..4b347d7
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+set -e -u
+
+stty -F $1 sane -echo clocal crtscts hupcl -icanon icrnl inlcr 9600
+stty -F $1 0 2>/dev/null || true
+stty -F $1 sane -echo clocal crtscts hupcl -icanon icrnl inlcr 9600
+exec flock -e -w 60 /var/lock/LCK..`basename $1` socat $1 STDIO
diff --git a/data/.gitignore b/data/.gitignore
new file mode 100644 (file)
index 0000000..a46a4f0
--- /dev/null
@@ -0,0 +1,2 @@
+*.rrd
+monitor.log
diff --git a/remind b/remind
new file mode 100644 (file)
index 0000000..c4337b8
--- /dev/null
+++ b/remind
@@ -0,0 +1,12 @@
+SET $LongDeg 76
+SET $LongMin 36
+SET $LongSec 33
+SET $LatDeg 39
+SET $LatMin 17
+SET $LatSec 33
+
+REM AT [sunrise()] MSG Lights on...
+REM AT [sunrise()] RUN /home/pi/sc/bin/rpb.expect 1 on > /dev/null
+
+REM AT [sunset()]  MSG Lights off...
+REM AT [sunset()]  RUN /home/pi/sc/bin/rpb.expect 1 off > /dev/null
diff --git a/runit/pi-sc-monitor/log/run b/runit/pi-sc-monitor/log/run
new file mode 100755 (executable)
index 0000000..e6b7e17
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec chpst -u pi svlogd -tt /home/pi/sc/data/monitor.log
diff --git a/runit/pi-sc-monitor/run b/runit/pi-sc-monitor/run
new file mode 100755 (executable)
index 0000000..10a6247
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec chpst -u pi:pi:dialout python -u /home/pi/sc/bin/monitor.py
diff --git a/runit/pi-sc-remind/run b/runit/pi-sc-remind/run
new file mode 100755 (executable)
index 0000000..01f2bf1
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec chpst -u pi:pi:dialout remind -z /home/pi/sc/remind