From 3ac85ba18b4edad3e9673ff00ebab3b2abec93ee Mon Sep 17 00:00:00 2001 From: James Gao Date: Wed, 22 Oct 2014 21:46:35 -0700 Subject: [PATCH] State machine done: #3. Manual override works --- kiln/manager.py | 12 +++---- kiln/server.py | 62 ++++++++++++++++++++++----------- kiln/states.py | 69 ++++++++++++++++++++++--------------- kiln/temp_monitor.js | 81 ++++++++++++++++++++++++++++++++++++-------- kiln/template.html | 25 +++++++------- kiln/thermo.py | 22 ++++++++---- 6 files changed, 183 insertions(+), 88 deletions(-) diff --git a/kiln/manager.py b/kiln/manager.py index 5f05b0d..0a5a2a9 100644 --- a/kiln/manager.py +++ b/kiln/manager.py @@ -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) diff --git a/kiln/server.py b/kiln/server.py index 37c0efb..af627e2 100644 --- a/kiln/server.py +++ b/kiln/server.py @@ -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): diff --git a/kiln/states.py b/kiln/states.py index 252f8cf..ef5ed40 100644 --- a/kiln/states.py +++ b/kiln/states.py @@ -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) \ No newline at end of file diff --git a/kiln/temp_monitor.js b/kiln/temp_monitor.js index 86b7371..6b73f71 100644 --- a/kiln/temp_monitor.js +++ b/kiln/temp_monitor.js @@ -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); - -}); diff --git a/kiln/template.html b/kiln/template.html index 4f73eac..b668cba 100644 --- a/kiln/template.html +++ b/kiln/template.html @@ -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 @@
  • Time
  • 0%
  • -
  • +
  • @@ -112,13 +106,13 @@
    - +
    - +
    - +
    @@ -159,5 +153,12 @@ + diff --git a/kiln/thermo.py b/kiln/thermo.py index 6c9a0b8..b613529 100644 --- a/kiln/thermo.py +++ b/kiln/thermo.py @@ -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):