Backend state machine finished, untested

master
James Gao 2014-10-22 12:11:46 -07:00
rodzic af8e6c6e00
commit 7183181d44
8 zmienionych plików z 219 dodań i 134 usunięć

BIN
BOM.ods

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -6,21 +6,25 @@ import warnings
import threading import threading
import traceback import traceback
import logging import logging
import states import states
import PID
logger = logging.getLogger("kiln.manager") logger = logging.getLogger("kiln.manager")
class Manager(threading.Thread): class Manager(threading.Thread):
def __init__(self, start=states.Idle): def __init__(self, start=states.Idle, simulate=False):
""" """
Implement a state machine that cycles through States Implement a state machine that cycles through States
""" """
super(Manager, self).__init__() super(Manager, self).__init__()
self._send = None self._send = None
self.thermocouple = thermo.Monitor(self._send_state)
self.regulator = stepper.Regulator()
self.profile = None self.regulator = stepper.Regulator(simulate=simulate)
if simulate:
self.therm = thermo.Simulate(regulator=self.regulator)
else:
self.therm = thermo.MAX31850()
self.state = start(self) self.state = start(self)
self.state_change = threading.Event() self.state_change = threading.Event()
@ -28,28 +32,14 @@ class Manager(threading.Thread):
self.running = True self.running = True
self.start() self.start()
def register(self, webapp):
self._send = webapp.send
def notify(self, data): def notify(self, data):
if self._send is not None: if self._send is not None:
self._send(data) self._send(data)
else: else:
logging.warn("No notifier set, ignoring message: %s"%data) logging.warn("No notifier set, ignoring message: %s"%data)
def _send_state(self, time, temp): def __del__(self):
profile = None self.manager_stop()
if self.profile is not None:
profile.get_state()
state = dict(
output=self.regulator.output,
profile=profile,
time=time,
temp=temp,
)
if self._send is not None:
self._send(state)
def __getattr__(self, name): def __getattr__(self, name):
"""Mutates the manager to return State actions """Mutates the manager to return State actions
@ -65,23 +55,77 @@ class Manager(threading.Thread):
return attr return attr
def _change_state(self, output): def _change_state(self, output):
if isinstance(output, states.State) : if isinstance(output, type) and issubclass(output, states.State) :
self.state = output() self.state = output(self)
self.state_change.set() self.state_change.set()
self.notify(dict(type="change", state=newstate.__name__)) self.notify(dict(type="change", state=output.__name__))
elif isinstance(output, tuple) and isinstance(output[0], states.State): elif isinstance(output, tuple) and issubclass(output[0], states.State):
newstate, kwargs = output newstate, kwargs = output
self.state = output(**kwargs) self.state = newstate(self, **kwargs)
self.notify(dict(type="change", state=newstate.__name__)) self.notify(dict(type="change", state=newstate.__name__))
elif isinstance(output, dict) and "type" in dict: elif isinstance(output, dict) and "type" in dict:
self.notify(output) self.notify(output)
elif output is not None: elif output is not None:
logger.warn("Unknown state output: %s"%output) logger.warn("Unknown state output: %r"%output)
def run(self): def run(self):
while running: while self.running:
self._change_state(self.state.run()) self._change_state(self.state.run())
def manager_stop(self):
self.running = False
self.state_change.set()
class Profile(threading.Thread):
"""Performs the PID loop required for feedback control"""
def __init__(self, schedule, therm, regulator, interval=5, start_time=None, callback=None,
Kp=.025, Ki=.01, Kd=.005):
self.schedule = schedule
self.therm = therm
self.regulator = regulator
self.interval = interval
self.start_time = start_time
if start_time is None:
self.start_time = time.time()
self.pid = PID.PID(Kp, Ki, Kd)
self.callback = callback
self.running = True
self.start()
@property
def elapsed(self):
''' Returns the elapsed time from start in seconds'''
return time.time() - self.start_time
@property
def completed(self):
return self.elapsed > self.schedule[-1][0]
def stop(self): def stop(self):
self.running = False self.running = False
self.state_change.set()
def run(self):
while not self.completed and self.running:
now = time.time()
ts = self.elapsed
#find epoch
for i in range(len(self.schedule)-1):
if self.schedule[i][0] < ts < self.schedule[i+1][0]:
time0, temp0 = self.schedule[i]
time1, temp1 = self.schedule[i+1]
frac = (ts - time0) / (time1 - time0)
setpoint = frac * (temp1 - temp0) + temp0
self.pid.setPoint(setpoint)
temp = self.thermocouple.temperature
pid_out = self.pid.update(temp)
if pid_out < 0: pid_out = 0
if pid_out > 1: pid_out = 1
self.regulator.set(pid_out)
if self.callback is not None:
self.callback(temp, setpoint, pid_out)
time.sleep(self.interval - (time.time()-now))

Wyświetl plik

@ -1,6 +1,7 @@
import time import time
import os import os
import json import json
import traceback
import tornado.ioloop import tornado.ioloop
import tornado.web import tornado.web
from tornado import websocket from tornado import websocket
@ -22,7 +23,7 @@ class DataRequest(tornado.web.RequestHandler):
self.manager = manager self.manager = manager
def get(self): def get(self):
data = self.manager.thermocouple.history data = list(self.manager.history)
output = [dict(time=ts.time, temp=ts.temp) for ts in data] output = [dict(time=ts.time, temp=ts.temp) for ts in data]
self.write(json.dumps(output)) self.write(json.dumps(output))
@ -31,7 +32,20 @@ class DoAction(tornado.web.RequestHandler):
self.manager = manager self.manager = manager
def get(self, action): def get(self, action):
pass try:
func = getattr(self.manager, action)
func()
self.write(json.dumps(dict(type="success")))
except:
self.write(json.dumps(dict(type="error", msg=traceback.format_exc())))
def post(self, action):
try:
func = getattr(self.manager, action)
func()
self.write(json.dumps(dict(type="success")))
except:
self.write(json.dumps(dict(type="error", msg=traceback.format_exc())))
class WebApp(object): class WebApp(object):
def __init__(self, manager, port=8888): def __init__(self, manager, port=8888):
@ -57,11 +71,10 @@ class WebApp(object):
if __name__ == "__main__": if __name__ == "__main__":
try: try:
import manager import manager
kiln = manager.Manager() kiln = manager.Manager(simulate=True)
app = WebApp(kiln) app = WebApp(kiln)
kiln.register(app) kiln._send = app.send
kiln.start()
app.run() app.run()
except KeyboardInterrupt: except KeyboardInterrupt:
kiln.stop() kiln.manager_stop()

Wyświetl plik

@ -3,7 +3,8 @@ http://python-3-patterns-idioms-test.readthedocs.org/en/latest/StateMachine.html
""" """
import time import time
import traceback import traceback
import PID import manager
from collections import deque
class State(object): class State(object):
def __init__(self, machine): def __init__(self, machine):
@ -15,15 +16,29 @@ class State(object):
self.parent.state_change.wait() self.parent.state_change.wait()
class Idle(State): class Idle(State):
def __init__(self, parent):
super(Idle, self).__init__(parent)
self.history = deque(maxlen=1024) #about 10 minutes worth
def run(self):
ts = self.parent.therm.get()
self.history.append(ts)
self.parent.notify(dict(type="temperature", time=ts.time, temp=ts.temp))
def ignite(self): def ignite(self):
_ignite(self.parent.regulator, self.parent.notify) _ignite(self.parent.regulator, self.parent.notify)
return Lit return Lit, dict(history=self.history)
def start(self, **kwargs): def start(self, **kwargs):
_ignite(self.parent.regulator, self.parent.notify) _ignite(self.parent.regulator, self.parent.notify)
kwargs['history'] = deque(self.history)
return Running, kwargs return Running, kwargs
class Lit(State): class Lit(State):
def __init__(self, parent, history):
super(Lit, self).__init__(parent)
self.history = deque(history)
def set(self, value): def set(self, value):
try: try:
self.parent.regulator.set(value) self.parent.regulator.set(value)
@ -31,71 +46,62 @@ class Lit(State):
except: except:
self.parent.notify(dict(type="error", msg=traceback.format_exc())) self.parent.notify(dict(type="error", msg=traceback.format_exc()))
def run(self):
ts = self.parent.therm.get()
self.history.append(ts)
def start(self, **kwargs): def start(self, **kwargs):
kwargs['history'] = self.history
return Running, kwargs return Running, kwargs
def stop(self): def stop(self):
_shutoff(self.parent.regulator, self.parent.notify) _shutoff(self.parent.regulator, self.parent.notify)
return Idle return Idle
class Running(State): class Cooling(State):
def __init__(self, parent, schedule, interval=5, start_time=None, Kp=.025, Ki=.01, Kd=.005): def __init__(self, parent, history):
self.schedule = schedule super(Cooling, self).__init__(parent)
self.thermocouple = parent.thermocouple self.history = history
self.interval = interval
self.start_time = start_time
if start_time is None:
self.start_time = time.time()
self.regulator = parent.regulator
self.pid = PID.PID(Kp, Ki, Kd)
self.running = True
super(Running, self).__init__(parent)
@property
def elapsed(self):
''' Returns the elapsed time from start in seconds'''
return time.time() - self.start_time
@property
def _info(self):
return dict(type="profile",
output=pid_out,
start_time=self.start_time,
elapsed=self.elapsed,
)
def run(self): def run(self):
now = time.time() ts = self.parent.therm.get()
ts = self.elapsed self.history.append(ts)
#find epoch if ts.temp < 50:
for i in range(len(self.schedule)-1): #TODO: save temperature log somewhere
if self.schedule[i][0] < ts < self.schedule[i+1][0]: return Idle
time0, temp0 = self.schedule[i]
time1, temp1 = self.schedule[i+1]
frac = (ts - time0) / (time1 - time0)
setpoint = frac * (temp1 - temp0) + temp0
self.pid.setPoint(setpoint)
temp = self.thermocouple.temperature class Running(State):
pid_out = self.pid.update(temp) def __init__(self, parent, history, **kwargs):
if pid_out < 0: pid_out = 0 super(Running, self).__init__(parent)
if pid_out > 1: pid_out = 1 self.profile = manager.Profile(therm=self.parent.therm, regulator=self.parent.regulator,
self.regulator.set(pid_out, block=True) callback=self._notify, **kwargs)
self.parent.notify(self.info) self.history = history
if ts > self.schedule[-1][0]: def _notify(self, therm, setpoint, out):
self.parent.notify(dict(
type="profile",
temp=therm,
setpoint=setpoint,
output=out,
ts=self.profile.elapsed,
))
def run(self):
if self.profile.completed:
self.parent.notify(dict(type="profile",status="complete")) self.parent.notify(dict(type="profile",status="complete"))
_shutoff(self.parent.regulator, self.parent.notify) _shutoff(self.parent.regulator, self.parent.notify)
return Idle, return Cooling, dict(history=self.history)
time.sleep(self.interval - (time.time()-now)) self.history.append(self.parent.therm.get())
def pause(self): def pause(self):
return Lit self.profile.stop()
return Lit, dict(history=self.history)
def stop(self): def stop(self):
self.profile.stop()
_shutoff(self.parent.regulator, self.parent.notify) _shutoff(self.parent.regulator, self.parent.notify)
return Idle return Cooling, dict(history=self.history)
def _ignite(regulator, notify): def _ignite(regulator, notify):

Wyświetl plik

@ -189,7 +189,7 @@ class Regulator(threading.Thread):
if self.current != 0: if self.current != 0:
raise ValueError("Must be off to ignite") raise ValueError("Must be off to ignite")
logger.log("Ignition start") logger.info("Ignition start")
self.stepper.step(start, self.speed, block=True) self.stepper.step(start, self.speed, block=True)
if self.ignite_pin is not None: if self.ignite_pin is not None:
GPIO.output(self.ignite_pin, True) GPIO.output(self.ignite_pin, True)
@ -198,10 +198,10 @@ class Regulator(threading.Thread):
GPIO.output(self.ignite_pin, False) GPIO.output(self.ignite_pin, False)
self.stepper.step(self.min - start, self.speed, block=True) self.stepper.step(self.min - start, self.speed, block=True)
self.current = self.min self.current = self.min
logger.log("Ignition complete") logger.info("Ignition complete")
def off(self, block=True): def off(self, block=True):
logger.log("Shutting off gas") logger.info("Shutting off gas")
self.stepper.step(-self.current, self.speed, block=block) self.stepper.step(-self.current, self.speed, block=block)
self.current = 0 self.current = 0
@ -218,7 +218,10 @@ class Regulator(threading.Thread):
@property @property
def output(self): def output(self):
return (self.current - self.min) / (self.max - self.min) out = (self.current - self.min) / float(self.max - self.min)
if out < 0:
return -1
return out
def run(self): def run(self):
"""Check the status of the flame sensor""" """Check the status of the flame sensor"""

Wyświetl plik

@ -17,9 +17,11 @@ var tempgraph = (function(module) {
var now = new Date(data.time*1000.); var now = new Date(data.time*1000.);
var temp = this.scalefunc(data.temp); var temp = this.scalefunc(data.temp);
var hourstr = now.getHours() % 12;
hourstr = hourstr == 0 ? 12 : houstr;
var minstr = now.getMinutes(); var minstr = now.getMinutes();
minstr = minstr.length < 2 ? "0"+minstr : minstr; minstr = minstr.length < 2 ? "0"+minstr : minstr;
var nowstr = now.getHours() % 12 + ":" + minstr + (now.getHours() > 12 ? " pm" : " am"); var nowstr = hourstr + ":" + minstr + (now.getHours() >= 12 ? " pm" : " am");
$("#current_time").text(nowstr); $("#current_time").text(nowstr);
$("#current_temp").text(this.temp_prefix+temp+this.temp_suffix); $("#current_temp").text(this.temp_prefix+temp+this.temp_suffix);
@ -93,13 +95,18 @@ var tempgraph = (function(module) {
module.Monitor.prototype._map_temp = function(d) { module.Monitor.prototype._map_temp = function(d) {
return {x:new Date(d.time*1000), y:this.scalefunc(d.temp)}; return {x:new Date(d.time*1000), y:this.scalefunc(d.temp)};
} }
module.Monitor.prototype.setState = function(name) {
}
module.Monitor.prototype._bindUI = function() { module.Monitor.prototype._bindUI = function() {
try { try {
var sock = new WebSocket("ws://"+window.location.hostname+":"+window.location.port+"/ws/", "protocolOne"); var sock = new WebSocket("ws://"+window.location.hostname+":"+window.location.port+"/ws/", "protocolOne");
sock.onmessage = function(event) { sock.onmessage = function(event) {
var data = JSON.parse(event.data); var data = JSON.parse(event.data);
this.update_temp(data); if (data.type == "temperature")
this.update_temp(data);
}.bind(this); }.bind(this);
} catch (e) {} } catch (e) {}
@ -120,7 +127,7 @@ var tempgraph = (function(module) {
return module; return module;
}(tempgraph || {})); }(tempgraph || {}));
d3.json("data2.json", function(error, data) { d3.json("temperature.json", function(error, data) {
monitor = new tempgraph.Monitor(data); monitor = new tempgraph.Monitor(data);
}); });

Wyświetl plik

@ -109,9 +109,6 @@
<div class="row"> <div class="row">
<div class="col-sm-8 col-md-8 row-space"> <div class="col-sm-8 col-md-8 row-space">
<svg id="graph" class="row-space"></svg> <svg id="graph" class="row-space"></svg>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="temp_graph.js"></script>
<script type="text/javascript" src="temp_monitor.js"></script>
<div class="btn-group btn-group-justified row-space"> <div class="btn-group btn-group-justified row-space">
<div class="btn-group"> <div class="btn-group">
@ -159,5 +156,8 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="temp_graph.js"></script>
<script type="text/javascript" src="temp_monitor.js"></script>
</body> </body>
</html> </html>

Wyświetl plik

@ -20,26 +20,11 @@ def temp_to_cone(temp):
tempsample = namedtuple("tempsample", ['time', 'temp']) tempsample = namedtuple("tempsample", ['time', 'temp'])
class Monitor(threading.Thread): class MAX31850(object):
def __init__(self, name="3b-000000182b57", callback=None): def __init__(self, name="3b-000000182b57", smooth_window=4):
super(Monitor, self).__init__()
self.daemon = True
self.device = "/sys/bus/w1/devices/%s/w1_slave"%name self.device = "/sys/bus/w1/devices/%s/w1_slave"%name
self.history = deque(maxlen=1048576) self.temps = deque(maxlen=smooth_window)
self.callback = callback self.last = None
try:
from Adafruit_alphanumeric import AlphaScroller
self.display = AlphaScroller(interval=.4)
self.display.start()
self.display.hide()
except ImportError:
logger.info("Could not start AlphaScroller")
self.display = None
self.running = True
self.start()
def _read_temp(self): def _read_temp(self):
with open(self.device, 'r') as f: with open(self.device, 'r') as f:
@ -54,35 +39,62 @@ class Monitor(threading.Thread):
if match is not None: if match is not None:
return float(match.group(1)) / 1000.0 return float(match.group(1)) / 1000.0
def stop(self): def get(self):
self.running = False """Blocking call to retrieve latest temperature sample"""
if self.display is not None: temp = self._read_temp()
self.display.stop() self.last = time.time()
self.history.append(temp)
return self.temperature
@property @property
def temperature(self): def temperature(self):
return self.history[-1][1] if self.last is None or time.time() - self.last > 5:
return self.get()
return tempsample(self.last, sum(self.history) / float(len(self.history)))
class Simulate(object):
def __init__(self, regulator):
self.regulator = regulator
def get(self):
time.sleep(.8)
return self.temperature
@property
def temperature(self):
return tempsample(time.time(), self.regulator.output * 1000. + 15)
class Monitor(threading.Thread):
def __init__(self, cls=MAX31850, **kwargs):
self.therm = cls(**kwargs)
self.running = True
from Adafruit_alphanumeric import AlphaScroller
self.display = AlphaScroller(interval=.4)
self.display.start()
self.display.hide()
def run(self): def run(self):
while self.running: while self.running:
temp = self._read_temp() _, temp = self.therm.get()
now = time.time()
self.history.append(tempsample(now, temp)) if temp > 50:
if self.callback is not None: if not self.display.shown:
self.callback(now, temp) self.display.show()
fahr = temp * 9. / 5. + 32.
text = list('%0.0f'%temp) + ['degree'] + list('C %0.0f'%fahr)+['degree'] + list("F")
if 600 <= temp:
text += [' ', ' ', 'cone']+list(temp_to_cone(temp))
self.display.set_text(text, reset=False)
elif self.display.shown:
self.display.hide()
def stop(self):
self.running = False
self.display.stop()
if self.display is not None:
if temp > 50:
if not self.display.shown:
self.display.show()
fahr = temp * 9. / 5. + 32.
text = list('%0.0f'%temp) + ['degree'] + list('C %0.0f'%fahr)+['degree'] + list("F")
if 600 <= temp:
text += [' ', ' ', 'cone']+list(temp_to_cone(temp))
self.display.set_text(text, reset=False)
elif self.display.shown:
self.display.hide()
if __name__ == "__main__": if __name__ == "__main__":
mon = Monitor() monitor = Monitor()
mon.join() monitor.start()