State machine done: #3. Manual override works

master
James Gao 2014-10-22 21:46:35 -07:00
rodzic 7183181d44
commit 3ac85ba18b
6 zmienionych plików z 183 dodań i 88 usunięć

Wyświetl plik

@ -2,7 +2,6 @@ import stepper
import time import time
import random import random
import thermo import thermo
import warnings
import threading import threading
import traceback import traceback
import logging import logging
@ -36,10 +35,7 @@ class Manager(threading.Thread):
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) logger.info("No notifier set, ignoring message: %s"%data)
def __del__(self):
self.manager_stop()
def __getattr__(self, name): def __getattr__(self, name):
"""Mutates the manager to return State actions """Mutates the manager to return State actions
@ -58,12 +54,12 @@ class Manager(threading.Thread):
if isinstance(output, type) and issubclass(output, states.State) : if isinstance(output, type) and issubclass(output, states.State) :
self.state = output(self) self.state = output(self)
self.state_change.set() 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): elif isinstance(output, tuple) and issubclass(output[0], states.State):
newstate, kwargs = output newstate, kwargs = output
self.state = newstate(self, **kwargs) self.state = newstate(self, **kwargs)
self.notify(dict(type="change", state=newstate.__name__)) self.notify(dict(type="state", state=newstate.__name__))
elif isinstance(output, dict) and "type" in dict: elif isinstance(output, dict) and "type" in output:
self.notify(output) self.notify(output)
elif output is not None: elif output is not None:
logger.warn("Unknown state output: %r"%output) logger.warn("Unknown state output: %r"%output)

Wyświetl plik

@ -2,65 +2,89 @@ import time
import os import os
import json import json
import traceback import traceback
import inspect
import tornado.ioloop import tornado.ioloop
import tornado.web import tornado.web
from tornado import websocket from tornado import websocket
cwd = os.path.split(os.path.abspath(__file__))[0] 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): class ClientSocket(websocket.WebSocketHandler):
def initialize(self, parent): def initialize(self, parent):
self.parent = parent self.parent = parent
def open(self): def open(self):
self.parent.sockets.append(self) self.parent.clients.append(self)
def on_close(self): def on_close(self):
self.parent.sockets.remove(self) self.parent.clients.remove(self)
class DataRequest(tornado.web.RequestHandler):
def initialize(self, manager):
self.manager = manager
class DataRequest(ManagerHandler):
def get(self): def get(self):
data = list(self.manager.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))
class DoAction(tornado.web.RequestHandler): class DoAction(ManagerHandler):
def initialize(self, manager): def _run(self, name, argfunc):
self.manager = manager 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): def get(self, action):
try: try:
func = getattr(self.manager, action) self._run(action, self.get_query_argument)
func()
self.write(json.dumps(dict(type="success"))) self.write(json.dumps(dict(type="success")))
except: except Exception as e:
self.write(json.dumps(dict(type="error", msg=traceback.format_exc()))) self.write(json.dumps(dict(type="error", error=repr(e), msg=traceback.format_exc())))
def post(self, action): def post(self, action):
try: try:
func = getattr(self.manager, action) self._run(action, self.get_argument)
func()
self.write(json.dumps(dict(type="success"))) self.write(json.dumps(dict(type="success")))
except: except Exception as e:
self.write(json.dumps(dict(type="error", msg=traceback.format_exc()))) self.write(json.dumps(dict(type="error", error=repr(e), msg=traceback.format_exc())))
class WebApp(object): class WebApp(object):
def __init__(self, manager, port=8888): def __init__(self, manager, port=8888):
self.handlers = [ self.handlers = [
(r'/', MainHandler, dict(manager=manager)),
(r"/ws/", ClientSocket, dict(parent=self)), (r"/ws/", ClientSocket, dict(parent=self)),
(r"/temperature.json", DataRequest, dict(manager=manager)), (r"/temperature.json", DataRequest, dict(manager=manager)),
(r"/do/(.*)", DoAction, dict(manager=manager)), (r"/do/(.*)", DoAction, dict(manager=manager)),
(r"/(.*)", tornado.web.StaticFileHandler, dict(path=cwd)), (r"/(.*)", tornado.web.StaticFileHandler, dict(path=cwd)),
] ]
self.sockets = [] self.clients = []
self.port = port self.port = port
def send(self, data): def send(self, data):
jsondat = json.dumps(data) jsondat = json.dumps(data)
for sock in self.sockets: for sock in self.clients:
sock.write_message(jsondat) sock.write_message(jsondat)
def run(self): def run(self):

Wyświetl plik

@ -7,31 +7,31 @@ import manager
from collections import deque from collections import deque
class State(object): class State(object):
def __init__(self, machine): def __init__(self, manager):
self.parent = machine self.parent = manager
def run(self): def run(self):
"""Action that must be continuously run while in this state""" """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() ts = self.parent.therm.get()
self.history.append(ts) 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): def ignite(self):
_ignite(self.parent.regulator, self.parent.notify) _ignite(self.parent.regulator, self.parent.notify)
return Lit, dict(history=self.history) 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) _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 return Running, kwargs
class Lit(State): class Lit(State):
@ -41,22 +41,21 @@ class Lit(State):
def set(self, value): def set(self, value):
try: try:
self.parent.regulator.set(value) self.parent.regulator.set(float(value))
self.parent.notify(dict(type="success")) return dict(type="success")
except: except:
self.parent.notify(dict(type="error", msg=traceback.format_exc())) return dict(type="error", msg=traceback.format_exc())
def run(self): def start(self, schedule, start_time=None, interval=5):
ts = self.parent.therm.get() kwargs = dict(history=self.history,
self.history.append(ts) schedule=json.loads(schedule),
start_time=float(start_time),
def start(self, **kwargs): interval=float(interval)
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 Cooling, dict(history=self.history)
class Cooling(State): class Cooling(State):
def __init__(self, parent, history): def __init__(self, parent, history):
@ -69,6 +68,20 @@ class Cooling(State):
if ts.temp < 50: if ts.temp < 50:
#TODO: save temperature log somewhere #TODO: save temperature log somewhere
return Idle 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): class Running(State):
def __init__(self, parent, history, **kwargs): def __init__(self, parent, history, **kwargs):
@ -92,7 +105,7 @@ class Running(State):
_shutoff(self.parent.regulator, self.parent.notify) _shutoff(self.parent.regulator, self.parent.notify)
return Cooling, dict(history=self.history) return Cooling, dict(history=self.history)
self.history.append(self.parent.therm.get()) return super(Running, self).run()
def pause(self): def pause(self):
self.profile.stop() self.profile.stop()
@ -111,7 +124,7 @@ def _ignite(regulator, notify):
except ValueError: except ValueError:
msg = dict(type="error", msg="Cannot ignite: regulator not off") msg = dict(type="error", msg="Cannot ignite: regulator not off")
except Exception as e: 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) notify(msg)
def _shutoff(regulator, notify): def _shutoff(regulator, notify):
@ -121,5 +134,5 @@ def _shutoff(regulator, notify):
except ValueError: except ValueError:
msg = dict(type="error", msg="Cannot shutoff: regulator not lit") msg = dict(type="error", msg="Cannot shutoff: regulator not lit")
except Exception as e: 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) notify(msg)

Wyświetl plik

@ -18,12 +18,13 @@ var tempgraph = (function(module) {
var temp = this.scalefunc(data.temp); var temp = this.scalefunc(data.temp);
var hourstr = now.getHours() % 12; var hourstr = now.getHours() % 12;
hourstr = hourstr == 0 ? 12 : houstr; hourstr = hourstr == 0 ? 12 : hourstr;
var minstr = now.getMinutes(); var minstr = now.getMinutes();
minstr = minstr.length < 2 ? "0"+minstr : minstr; minstr = minstr.length < 2 ? "0"+minstr : minstr;
var nowstr = hourstr + ":" + minstr + (now.getHours() >= 12 ? " pm" : " am"); var nowstr = hourstr + ":" + minstr + (now.getHours() >= 12 ? " pm" : " am");
var tempstr = Math.round(temp*100) / 100;
$("#current_time").text(nowstr); $("#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 //Adjust x and ylims
if (now > this.last().time) { if (now > this.last().time) {
@ -49,8 +50,14 @@ var tempgraph = (function(module) {
//update the output slider and text, if necessary //update the output slider and text, if necessary
if (data.output !== undefined) { if (data.output !== undefined) {
$("#current_output_text").text(data.output*100+"%"); if (data.output == -1) {
$("#current_output").val(data.output*1000); $("#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) { 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)}; 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() { 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 { 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");
@ -107,14 +160,17 @@ var tempgraph = (function(module) {
var data = JSON.parse(event.data); var data = JSON.parse(event.data);
if (data.type == "temperature") if (data.type == "temperature")
this.update_temp(data); this.update_temp(data);
else if (data.type == "state") {
this.set_state(data.state);
}
}.bind(this); }.bind(this);
} catch (e) {} } 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_C = function(temp) { return temp; }
module.temp_to_F = function(temp) { module.temp_to_F = function(temp) {
@ -126,8 +182,3 @@ var tempgraph = (function(module) {
return module; return module;
}(tempgraph || {})); }(tempgraph || {}));
d3.json("temperature.json", function(error, data) {
monitor = new tempgraph.Monitor(data);
});

Wyświetl plik

@ -43,13 +43,7 @@
height:500px; height:500px;
} }
rect.pane { #stop_button_navbar {
cursor: move;
fill: none;
pointer-events: all;
}
#stop-button {
margin-left:10px; margin-left:10px;
margin-right:10px; margin-right:10px;
} }
@ -92,12 +86,12 @@
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
<li class="active"><a href="#" id="temp_scale_F">°F</a></li> <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_C">°C</a></li>
<li><a href="#" id="temp_scale_cone">Δ</a></li> <!--<li><a href="#" id="temp_scale_cone">Δ</a></li>-->
</ul> </ul>
</li> </li>
<li><a href="#" id="current_time">Time</a></li> <li><a href="#" id="current_time">Time</a></li>
<li><a href="#" id="current_output_text">0%</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> </ul>
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->
</div> </div>
@ -112,13 +106,13 @@
<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">
<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>
<div class="btn-group output-slider"> <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>
<div class="btn-group"> <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> </div>
</div> </div>
@ -159,5 +153,12 @@
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></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_graph.js"></script>
<script type="text/javascript" src="temp_monitor.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> </body>
</html> </html>

Wyświetl plik

@ -1,5 +1,6 @@
import re import re
import time import time
import random
import datetime import datetime
import logging import logging
import threading import threading
@ -23,7 +24,7 @@ tempsample = namedtuple("tempsample", ['time', 'temp'])
class MAX31850(object): class MAX31850(object):
def __init__(self, name="3b-000000182b57", smooth_window=4): def __init__(self, name="3b-000000182b57", smooth_window=4):
self.device = "/sys/bus/w1/devices/%s/w1_slave"%name 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 self.last = None
def _read_temp(self): def _read_temp(self):
@ -41,9 +42,8 @@ class MAX31850(object):
def get(self): def get(self):
"""Blocking call to retrieve latest temperature sample""" """Blocking call to retrieve latest temperature sample"""
temp = self._read_temp() self.history.append(self._read_temp())
self.last = time.time() self.last = time.time()
self.history.append(temp)
return self.temperature return self.temperature
@property @property
@ -54,16 +54,26 @@ class MAX31850(object):
return tempsample(self.last, sum(self.history) / float(len(self.history))) return tempsample(self.last, sum(self.history) / float(len(self.history)))
class Simulate(object): class Simulate(object):
def __init__(self, regulator): def __init__(self, regulator, smooth_window=4):
self.regulator = regulator 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): def get(self):
time.sleep(.8) self.history.append(self._read_temp())
self.last = time.time()
return self.temperature return self.temperature
@property @property
def temperature(self): 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): class Monitor(threading.Thread):
def __init__(self, cls=MAX31850, **kwargs): def __init__(self, cls=MAX31850, **kwargs):