kopia lustrzana https://github.com/jamesgao/kiln_controller
State machine done: #3. Manual override works
rodzic
7183181d44
commit
3ac85ba18b
|
@ -2,7 +2,6 @@ import stepper
|
|||
import time
|
||||
import random
|
||||
import thermo
|
||||
import warnings
|
||||
import threading
|
||||
import traceback
|
||||
import logging
|
||||
|
@ -36,10 +35,7 @@ class Manager(threading.Thread):
|
|||
if self._send is not None:
|
||||
self._send(data)
|
||||
else:
|
||||
logging.warn("No notifier set, ignoring message: %s"%data)
|
||||
|
||||
def __del__(self):
|
||||
self.manager_stop()
|
||||
logger.info("No notifier set, ignoring message: %s"%data)
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Mutates the manager to return State actions
|
||||
|
@ -58,12 +54,12 @@ class Manager(threading.Thread):
|
|||
if isinstance(output, type) and issubclass(output, states.State) :
|
||||
self.state = output(self)
|
||||
self.state_change.set()
|
||||
self.notify(dict(type="change", state=output.__name__))
|
||||
self.notify(dict(type="state", state=output.__name__))
|
||||
elif isinstance(output, tuple) and issubclass(output[0], states.State):
|
||||
newstate, kwargs = output
|
||||
self.state = newstate(self, **kwargs)
|
||||
self.notify(dict(type="change", state=newstate.__name__))
|
||||
elif isinstance(output, dict) and "type" in dict:
|
||||
self.notify(dict(type="state", state=newstate.__name__))
|
||||
elif isinstance(output, dict) and "type" in output:
|
||||
self.notify(output)
|
||||
elif output is not None:
|
||||
logger.warn("Unknown state output: %r"%output)
|
||||
|
|
|
@ -2,65 +2,89 @@ import time
|
|||
import os
|
||||
import json
|
||||
import traceback
|
||||
import inspect
|
||||
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
from tornado import websocket
|
||||
|
||||
cwd = os.path.split(os.path.abspath(__file__))[0]
|
||||
|
||||
class ManagerHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, manager):
|
||||
self.manager = manager
|
||||
|
||||
class MainHandler(ManagerHandler):
|
||||
def get(self):
|
||||
return self.render("template.html", state=self.manager.state.__class__.__name__)
|
||||
|
||||
class ClientSocket(websocket.WebSocketHandler):
|
||||
def initialize(self, parent):
|
||||
self.parent = parent
|
||||
|
||||
def open(self):
|
||||
self.parent.sockets.append(self)
|
||||
self.parent.clients.append(self)
|
||||
|
||||
def on_close(self):
|
||||
self.parent.sockets.remove(self)
|
||||
|
||||
class DataRequest(tornado.web.RequestHandler):
|
||||
def initialize(self, manager):
|
||||
self.manager = manager
|
||||
self.parent.clients.remove(self)
|
||||
|
||||
class DataRequest(ManagerHandler):
|
||||
def get(self):
|
||||
data = list(self.manager.history)
|
||||
output = [dict(time=ts.time, temp=ts.temp) for ts in data]
|
||||
self.write(json.dumps(output))
|
||||
|
||||
class DoAction(tornado.web.RequestHandler):
|
||||
def initialize(self, manager):
|
||||
self.manager = manager
|
||||
class DoAction(ManagerHandler):
|
||||
def _run(self, name, argfunc):
|
||||
func = getattr(self.manager.state, name)
|
||||
#Introspect the function, get the arguments
|
||||
args, varargs, keywords, defaults = inspect.getargspec(func)
|
||||
|
||||
kwargs = dict()
|
||||
if defaults is not None:
|
||||
#keyword arguments
|
||||
for arg, d in zip(args[-len(defaults):], defaults):
|
||||
kwargs[arg] = argfunc(arg, default=d)
|
||||
end = len(defaults)
|
||||
else:
|
||||
end = len(args)
|
||||
|
||||
#required arguments
|
||||
for arg in args[1:end]:
|
||||
kwargs[arg] = argfunc(arg)
|
||||
|
||||
realfunc = getattr(self.manager, name)
|
||||
realfunc(**kwargs)
|
||||
|
||||
def get(self, action):
|
||||
try:
|
||||
func = getattr(self.manager, action)
|
||||
func()
|
||||
self._run(action, self.get_query_argument)
|
||||
self.write(json.dumps(dict(type="success")))
|
||||
except:
|
||||
self.write(json.dumps(dict(type="error", msg=traceback.format_exc())))
|
||||
except Exception as e:
|
||||
self.write(json.dumps(dict(type="error", error=repr(e), msg=traceback.format_exc())))
|
||||
|
||||
def post(self, action):
|
||||
try:
|
||||
func = getattr(self.manager, action)
|
||||
func()
|
||||
self._run(action, self.get_argument)
|
||||
self.write(json.dumps(dict(type="success")))
|
||||
except:
|
||||
self.write(json.dumps(dict(type="error", msg=traceback.format_exc())))
|
||||
except Exception as e:
|
||||
self.write(json.dumps(dict(type="error", error=repr(e), msg=traceback.format_exc())))
|
||||
|
||||
class WebApp(object):
|
||||
def __init__(self, manager, port=8888):
|
||||
self.handlers = [
|
||||
(r'/', MainHandler, dict(manager=manager)),
|
||||
(r"/ws/", ClientSocket, dict(parent=self)),
|
||||
(r"/temperature.json", DataRequest, dict(manager=manager)),
|
||||
(r"/do/(.*)", DoAction, dict(manager=manager)),
|
||||
(r"/(.*)", tornado.web.StaticFileHandler, dict(path=cwd)),
|
||||
]
|
||||
self.sockets = []
|
||||
self.clients = []
|
||||
self.port = port
|
||||
|
||||
def send(self, data):
|
||||
jsondat = json.dumps(data)
|
||||
for sock in self.sockets:
|
||||
for sock in self.clients:
|
||||
sock.write_message(jsondat)
|
||||
|
||||
def run(self):
|
||||
|
|
|
@ -7,31 +7,31 @@ import manager
|
|||
from collections import deque
|
||||
|
||||
class State(object):
|
||||
def __init__(self, machine):
|
||||
self.parent = machine
|
||||
def __init__(self, manager):
|
||||
self.parent = manager
|
||||
|
||||
def run(self):
|
||||
"""Action that must be continuously run while in this state"""
|
||||
self.parent.state_change.clear()
|
||||
self.parent.state_change.wait()
|
||||
|
||||
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))
|
||||
return dict(type="temperature", time=ts.time, temp=ts.temp, output=self.parent.regulator.output)
|
||||
|
||||
class Idle(State):
|
||||
def __init__(self, manager):
|
||||
super(Idle, self).__init__(manager)
|
||||
self.history = deque(maxlen=1024) #about 10 minutes worth
|
||||
|
||||
def ignite(self):
|
||||
_ignite(self.parent.regulator, self.parent.notify)
|
||||
return Lit, dict(history=self.history)
|
||||
|
||||
def start(self, **kwargs):
|
||||
def start(self, schedule, start_time=None, interval=5):
|
||||
_ignite(self.parent.regulator, self.parent.notify)
|
||||
kwargs['history'] = deque(self.history)
|
||||
kwargs = dict(history=self.history,
|
||||
schedule=json.loads(schedule),
|
||||
start_time=float(start_time),
|
||||
interval=float(interval)
|
||||
)
|
||||
return Running, kwargs
|
||||
|
||||
class Lit(State):
|
||||
|
@ -41,22 +41,21 @@ class Lit(State):
|
|||
|
||||
def set(self, value):
|
||||
try:
|
||||
self.parent.regulator.set(value)
|
||||
self.parent.notify(dict(type="success"))
|
||||
self.parent.regulator.set(float(value))
|
||||
return dict(type="success")
|
||||
except:
|
||||
self.parent.notify(dict(type="error", msg=traceback.format_exc()))
|
||||
return dict(type="error", msg=traceback.format_exc())
|
||||
|
||||
def run(self):
|
||||
ts = self.parent.therm.get()
|
||||
self.history.append(ts)
|
||||
|
||||
def start(self, **kwargs):
|
||||
kwargs['history'] = self.history
|
||||
def start(self, schedule, start_time=None, interval=5):
|
||||
kwargs = dict(history=self.history,
|
||||
schedule=json.loads(schedule),
|
||||
start_time=float(start_time),
|
||||
interval=float(interval)
|
||||
)
|
||||
return Running, kwargs
|
||||
|
||||
def stop(self):
|
||||
_shutoff(self.parent.regulator, self.parent.notify)
|
||||
return Idle
|
||||
return Cooling, dict(history=self.history)
|
||||
|
||||
class Cooling(State):
|
||||
def __init__(self, parent, history):
|
||||
|
@ -69,6 +68,20 @@ class Cooling(State):
|
|||
if ts.temp < 50:
|
||||
#TODO: save temperature log somewhere
|
||||
return Idle
|
||||
return dict(type="temperature", time=ts.time, temp=ts.temp)
|
||||
|
||||
def ignite(self):
|
||||
_ignite(self.parent.regulator, self.parent.notify)
|
||||
return Lit, dict(history=self.history)
|
||||
|
||||
def start(self, schedule, start_time=None, interval=5):
|
||||
_ignite(self.parent.regulator, self.parent.notify)
|
||||
kwargs = dict(history=self.history,
|
||||
schedule=json.loads(schedule),
|
||||
start_time=float(start_time),
|
||||
interval=float(interval)
|
||||
)
|
||||
return Running, kwargs
|
||||
|
||||
class Running(State):
|
||||
def __init__(self, parent, history, **kwargs):
|
||||
|
@ -92,7 +105,7 @@ class Running(State):
|
|||
_shutoff(self.parent.regulator, self.parent.notify)
|
||||
return Cooling, dict(history=self.history)
|
||||
|
||||
self.history.append(self.parent.therm.get())
|
||||
return super(Running, self).run()
|
||||
|
||||
def pause(self):
|
||||
self.profile.stop()
|
||||
|
@ -111,7 +124,7 @@ def _ignite(regulator, notify):
|
|||
except ValueError:
|
||||
msg = dict(type="error", msg="Cannot ignite: regulator not off")
|
||||
except Exception as e:
|
||||
msg = dict(type="error", msg=traceback.format_exc())
|
||||
msg = dict(type="error", error=repr(e), msg=traceback.format_exc())
|
||||
notify(msg)
|
||||
|
||||
def _shutoff(regulator, notify):
|
||||
|
@ -121,5 +134,5 @@ def _shutoff(regulator, notify):
|
|||
except ValueError:
|
||||
msg = dict(type="error", msg="Cannot shutoff: regulator not lit")
|
||||
except Exception as e:
|
||||
msg = dict(type="error", msg=traceback.format_exc())
|
||||
msg = dict(type="error", error=repr(e), msg=traceback.format_exc())
|
||||
notify(msg)
|
|
@ -18,12 +18,13 @@ var tempgraph = (function(module) {
|
|||
var temp = this.scalefunc(data.temp);
|
||||
|
||||
var hourstr = now.getHours() % 12;
|
||||
hourstr = hourstr == 0 ? 12 : houstr;
|
||||
hourstr = hourstr == 0 ? 12 : hourstr;
|
||||
var minstr = now.getMinutes();
|
||||
minstr = minstr.length < 2 ? "0"+minstr : minstr;
|
||||
var nowstr = hourstr + ":" + minstr + (now.getHours() >= 12 ? " pm" : " am");
|
||||
var tempstr = Math.round(temp*100) / 100;
|
||||
$("#current_time").text(nowstr);
|
||||
$("#current_temp").text(this.temp_prefix+temp+this.temp_suffix);
|
||||
$("#current_temp").text(this.temp_prefix+tempstr+this.temp_suffix);
|
||||
|
||||
//Adjust x and ylims
|
||||
if (now > this.last().time) {
|
||||
|
@ -49,8 +50,14 @@ var tempgraph = (function(module) {
|
|||
|
||||
//update the output slider and text, if necessary
|
||||
if (data.output !== undefined) {
|
||||
$("#current_output_text").text(data.output*100+"%");
|
||||
$("#current_output").val(data.output*1000);
|
||||
if (data.output == -1) {
|
||||
$("#current_output_text").text("Off");
|
||||
$("#current_output").val(0);
|
||||
} else {
|
||||
var outstr = Math.round(data.output*10000) / 100;
|
||||
$("#current_output_text").text(outstr+"%");
|
||||
$("#current_output").val(data.output*1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
module.Monitor.prototype.update_UI = function(packet) {
|
||||
|
@ -96,10 +103,56 @@ var tempgraph = (function(module) {
|
|||
return {x:new Date(d.time*1000), y:this.scalefunc(d.temp)};
|
||||
}
|
||||
|
||||
module.Monitor.prototype.setState = function(name) {
|
||||
|
||||
module.Monitor.prototype.set_state = function(name) {
|
||||
if (name == "Lit") {
|
||||
$("#ignite_button").addClass("disabled");
|
||||
$("#current_output").removeAttr("disabled");
|
||||
$("#stop_button").removeClass("disabled");
|
||||
$("#stop_button_navbar").removeClass("hidden disabled");
|
||||
} else if (name == "Idle" || name == "Cooling") {
|
||||
$("#ignite_button").removeClass("disabled");
|
||||
$("#current_output").attr("disabled", "disabled");
|
||||
$("#stop_button").addClass("disabled");
|
||||
$("#stop_button_navbar").addClass("hidden disabled");
|
||||
}
|
||||
}
|
||||
module.Monitor.prototype._bindUI = function() {
|
||||
$("#temp_scale_C").click(function() { this.setScale("C");}.bind(this));
|
||||
$("#temp_scale_F").click(function() { this.setScale("F");}.bind(this));
|
||||
//$("#temp_scale_C").click(function() { this.setScale("C");}.bind(this));
|
||||
|
||||
$("#ignite_button").click(function() {
|
||||
this._disable_all();
|
||||
$.getJSON("/do/ignite", function(data) {
|
||||
if (data.type == "error")
|
||||
alert(data.msg, data.error);
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
$("#stop_button, #stop_button_navbar").click(function() {
|
||||
this._disable_all();
|
||||
$.getJSON("/do/stop", function(data) {
|
||||
if (data.type == "error")
|
||||
alert(data.msg, data.error);
|
||||
else if (data.type == "success") {
|
||||
$("#current_output").val(0);
|
||||
}
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
$("#current_output").mouseup(function(e) {
|
||||
$.getJSON("/do/set?value="+(e.target.value / 1000), function(data) {
|
||||
if (data.type == "error")
|
||||
alert(data.msg, data.error);
|
||||
else if (data.type == "success")
|
||||
$("#current_output_text").text(e.target.value/10 + "%");
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
try {
|
||||
var sock = new WebSocket("ws://"+window.location.hostname+":"+window.location.port+"/ws/", "protocolOne");
|
||||
|
||||
|
@ -107,14 +160,17 @@ var tempgraph = (function(module) {
|
|||
var data = JSON.parse(event.data);
|
||||
if (data.type == "temperature")
|
||||
this.update_temp(data);
|
||||
else if (data.type == "state") {
|
||||
this.set_state(data.state);
|
||||
}
|
||||
}.bind(this);
|
||||
} catch (e) {}
|
||||
|
||||
$("#temp_scale_C").click(function() { this.setScale("C");}.bind(this));
|
||||
$("#temp_scale_F").click(function() { this.setScale("F");}.bind(this));
|
||||
//$("#temp_scale_C").click(function() { this.setScale("C");}.bind(this));
|
||||
}
|
||||
|
||||
module.Monitor.prototype._disable_all = function() {
|
||||
$("button").addClass("disabled");
|
||||
$("input").attr("disabled", "disabled");
|
||||
}
|
||||
|
||||
module.temp_to_C = function(temp) { return temp; }
|
||||
module.temp_to_F = function(temp) {
|
||||
|
@ -126,8 +182,3 @@ var tempgraph = (function(module) {
|
|||
|
||||
return module;
|
||||
}(tempgraph || {}));
|
||||
|
||||
d3.json("temperature.json", function(error, data) {
|
||||
monitor = new tempgraph.Monitor(data);
|
||||
|
||||
});
|
||||
|
|
|
@ -43,13 +43,7 @@
|
|||
height:500px;
|
||||
}
|
||||
|
||||
rect.pane {
|
||||
cursor: move;
|
||||
fill: none;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#stop-button {
|
||||
#stop_button_navbar {
|
||||
margin-left:10px;
|
||||
margin-right:10px;
|
||||
}
|
||||
|
@ -92,12 +86,12 @@
|
|||
<ul class="dropdown-menu" role="menu">
|
||||
<li class="active"><a href="#" id="temp_scale_F">°F</a></li>
|
||||
<li><a href="#" id="temp_scale_C">°C</a></li>
|
||||
<li><a href="#" id="temp_scale_cone">Δ</a></li>
|
||||
<!--<li><a href="#" id="temp_scale_cone">Δ</a></li>-->
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#" id="current_time">Time</a></li>
|
||||
<li><a href="#" id="current_output_text">0%</a></li>
|
||||
<li><button id="stop-button" class="btn btn-primary navbar-btn hidden" href="#">Stop</button></li>
|
||||
<li><button id="stop_button_navbar" class="btn btn-primary navbar-btn hidden" href="#">Stop</button></li>
|
||||
</ul>
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
|
@ -112,13 +106,13 @@
|
|||
|
||||
<div class="btn-group btn-group-justified row-space">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary disabled"><span class="glyphicon glyphicon-stop"></span> Off</button>
|
||||
<button id="stop_button" type="button" class="btn btn-primary disabled"><span class="glyphicon glyphicon-stop"></span> Off</button>
|
||||
</div>
|
||||
<div class="btn-group output-slider">
|
||||
<input id="current_output" type="range" min=0 max=1000 class="btn btn-default" />
|
||||
<input id="current_output" type="range" min=0 max=1000 value=0 class="btn btn-default" disabled="disabled" />
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-danger"><span class="glyphicon glyphicon-fire"></span> Ignite</button>
|
||||
<button id="ignite_button" type="button" class="btn btn-danger"><span class="glyphicon glyphicon-fire"></span> Ignite</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -159,5 +153,12 @@
|
|||
<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>
|
||||
<script type="text/javascript">
|
||||
var monitor;
|
||||
d3.json("temperature.json", function(error, data) {
|
||||
monitor = new tempgraph.Monitor(data);
|
||||
monitor.set_state("{{ state }}");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import re
|
||||
import time
|
||||
import random
|
||||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
|
@ -23,7 +24,7 @@ tempsample = namedtuple("tempsample", ['time', 'temp'])
|
|||
class MAX31850(object):
|
||||
def __init__(self, name="3b-000000182b57", smooth_window=4):
|
||||
self.device = "/sys/bus/w1/devices/%s/w1_slave"%name
|
||||
self.temps = deque(maxlen=smooth_window)
|
||||
self.history = deque(maxlen=smooth_window)
|
||||
self.last = None
|
||||
|
||||
def _read_temp(self):
|
||||
|
@ -41,9 +42,8 @@ class MAX31850(object):
|
|||
|
||||
def get(self):
|
||||
"""Blocking call to retrieve latest temperature sample"""
|
||||
temp = self._read_temp()
|
||||
self.history.append(self._read_temp())
|
||||
self.last = time.time()
|
||||
self.history.append(temp)
|
||||
return self.temperature
|
||||
|
||||
@property
|
||||
|
@ -54,16 +54,26 @@ class MAX31850(object):
|
|||
return tempsample(self.last, sum(self.history) / float(len(self.history)))
|
||||
|
||||
class Simulate(object):
|
||||
def __init__(self, regulator):
|
||||
def __init__(self, regulator, smooth_window=4):
|
||||
self.regulator = regulator
|
||||
self.history = deque(maxlen=smooth_window)
|
||||
self.last = None
|
||||
|
||||
def _read_temp(self):
|
||||
time.sleep(.8)
|
||||
return max([self.regulator.output, 0]) * 1000. + 15+random.gauss(0,.2)
|
||||
|
||||
def get(self):
|
||||
time.sleep(.8)
|
||||
self.history.append(self._read_temp())
|
||||
self.last = time.time()
|
||||
return self.temperature
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
return tempsample(time.time(), self.regulator.output * 1000. + 15)
|
||||
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 Monitor(threading.Thread):
|
||||
def __init__(self, cls=MAX31850, **kwargs):
|
||||
|
|
Ładowanie…
Reference in New Issue