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 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)

Wyświetl plik

@ -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):

Wyświetl plik

@ -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)

Wyświetl plik

@ -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);
});

Wyświetl plik

@ -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>

Wyświetl plik

@ -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):