Merge branch 'master' of github.com:jamesgao/kiln_controller

master
James Gao 2014-11-27 16:35:35 +00:00
commit 2663e294d5
30 zmienionych plików z 23661 dodań i 611 usunięć

19
BOM.md 100644
Wyświetl plik

@ -0,0 +1,19 @@
| Item | Where | Price |
|-----------|-----------|------:|
| Kiln | Craigslist| $100.00 |
|MR-750 Venturi Burner | [Link](http://www.axner.com/mr-750venturiburner.aspx) (locally purchased)| $43.50|
|Propane regulator|[Link](http://www.ebay.com/itm/331092530323?_trksid=p2060778.m2749.l2649&ssPageName=STRK%3AMEBIDX%3AIT)| $17.66|
|Various connectors| Hardware store | $10.00 |
|K-type kiln thermocouple with block|[Link](http://www.ebay.com/itm/291255615336?_trksid=p2060778.m2749.l2649&ssPageName=STRK%3AMEBIDX%3AIT)| $21.00|
|K-type thermocouple wire|[Link](http://www.amazon.com/gp/product/B00AKWP0BW/ref=oh_aui_detailpage_o03_s00?ie=UTF8&psc=1)| $9.11 |
|Raspberry Pi|[Link](http://www.newegg.com/Product/Product.aspx?Item=N82E16813142003&nm_mc=KNC-GoogleAdwords-PC&cm_mmc=KNC-GoogleAdwords-PC-_-pla-_-Embedded+Solutions-_-N82E16813142003&gclid=Cj0KEQjwtvihBRCd8fyrtfHRlJEBEiQAQcubtCFX3LJgefIMY9KhR8iZ2TvLeNQobi05eP_FuGFbpY8aApkE8P8HAQ)|$35.00|
|USB Wifi adapter|[Link](http://www.amazon.com/Edimax-EW-7811Un-Adapter-Raspberry-Supports/dp/B003MTTJOY)|$8.70|
|MAX31850K breakout|[Link](https://www.adafruit.com/products/1727)|$14.95|
|Alphanumeric LED with backpack|[Link](https://www.adafruit.com/products/1911)|$9.95|
|28BYJ-48 stepper|[Link](http://www.ebay.com/itm/2pcs-DC-5V-Stepper-Motor-ULN2003-Driver-Test-Module-Board-28BYJ-48-for-Arduino-/221347325924?pt=LH_DefaultDomain_0&hash=item33895427e4)|$5.00|
|Silicon Nitride igniter|[Link](https://www.sparkfun.com/products/11694)|$19.95|
|Flame Sensor|[Link](http://www.ebay.com/itm/Wavelength-760nm-1100nm-LM393-IR-Flame-Fire-Sensor-Module-Board-For-Arduino-/190938428869?pt=LH_DefaultDomain_0&hash=item2c74d135c5)|$1.50|
|12V 6A power supply|[Link](http://www.amazon.com/Adapter-Power-Supply-LCD-Monitor/dp/B003TUMDWG/ref=sr_1_2?ie=UTF8&qid=1413941132&sr=8-2&keywords=12V+power+supply)|$6.50|
*Total: $199.81*

BIN
BOM.ods

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 101 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 30 KiB

0
kiln/Adafruit_I2C.py 100755 → 100644
Wyświetl plik

94
kiln/PID.py 100644
Wyświetl plik

@ -0,0 +1,94 @@
import logging
logger = logging.getLogger("kiln.PID")
class PID(object):
"""
Discrete PID control
#The recipe gives simple implementation of a Discrete Proportional-Integral-Derivative (PID) controller. PID controller gives output value for error between desired reference input and measurement feedback to minimize error value.
#More information: http://en.wikipedia.org/wiki/PID_controller
#
#cnr437@gmail.com
#
####### Example #########
#
#p=PID(3.0,0.4,1.2)
#p.setPoint(5.0)
#while True:
# pid = p.update(measurement_value)
#
#
"""
def __init__(self, P=2.0, I=0.0, D=1.0, Derivator=0, Integrator=0, Integrator_max=500, Integrator_min=-500):
self.Kp=P
self.Ki=I
self.Kd=D
self.Derivator=Derivator
self.Integrator=Integrator
self.Integrator_max=Integrator_max
self.Integrator_min=Integrator_min
self.set_point=0.0
self.error=0.0
def update(self,current_value):
"""
Calculate PID output value for given reference input and feedback
"""
self.error = self.set_point - current_value
self.P_value = self.Kp * self.error
self.D_value = self.Kd * ( self.error - self.Derivator)
self.Derivator = self.error
self.Integrator = self.Integrator + self.error
if self.Integrator > self.Integrator_max:
self.Integrator = self.Integrator_max
elif self.Integrator < self.Integrator_min:
self.Integrator = self.Integrator_min
self.I_value = self.Integrator * self.Ki
PID = self.P_value + self.I_value + self.D_value
logger.info("P: %f, I: %f, D:%f, Output:%f"%(self.P_value, self.I_value, self.D_value, PID))
return PID
def setPoint(self,set_point):
"""
Initilize the setpoint of PID
"""
self.set_point = set_point
self.Integrator=0
self.Derivator=0
def setIntegrator(self, Integrator):
self.Integrator = Integrator
def setDerivator(self, Derivator):
self.Derivator = Derivator
def setKp(self,P):
self.Kp=P
def setKi(self,I):
self.Ki=I
def setKd(self,D):
self.Kd=D
def getPoint(self):
return self.set_point
def getError(self):
return self.error
def getIntegrator(self):
return self.Integrator
def getDerivator(self):
return self.Derivator

Wyświetl plik

@ -1,158 +1,127 @@
import stepper
import time
import random
import warnings
import thermo
import threading
import traceback
import logging
class StateMachine(object):
def __init__(self):
import stepper
import thermo
self.monitor = thermo.Monitor()
self.regulator = stepper.Regulator()
import states
import PID
class KilnController(object):
def __init__(self, schedule, monitor, interval=5, start_time=None, Kp=.025, Ki=.01, Kd=.001, simulate=True):
logger = logging.getLogger("kiln.manager")
class Manager(threading.Thread):
def __init__(self, start=states.Idle, simulate=False):
"""
Implement a state machine that cycles through States
"""
super(Manager, self).__init__()
self._send = 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_change = threading.Event()
self.running = True
self.start()
def notify(self, data):
if self._send is not None:
self._send(data)
else:
logger.info("No notifier set, ignoring message: %s"%data)
def __getattr__(self, name):
"""Mutates the manager to return State actions
If the requested attribute is a function, wrap the function
such that returned objects which are States indicate a state change
"""
attr = getattr(self.state, name)
if hasattr(attr, "__call__"):
def func(*args, **kwargs):
self._change_state(attr(*args, **kwargs))
return func
return attr
def _change_state(self, output):
if isinstance(output, type) and issubclass(output, states.State) :
self.state = output(self)
self.state_change.set()
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="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)
def run(self):
while self.running:
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.monitor = monitor
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.regulator = stepper.Regulator(simulate=simulate)
self.pid = PID(Kp, Ki, Kd)
self.simulate = simulate
if simulate:
self.schedule.insert(0, [0, 15])
else:
self.schedule.insert(0, [0, 15])
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
return time.time() - self.start_time
@property
def completed(self):
return self.elapsed > self.schedule[-1][0]
def stop(self):
self.running = False
def run(self):
try:
self.regulator.ignite()
print self.elapsed, self.schedule[-1][0]
while self.elapsed < self.schedule[-1][0]:
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)
print "In epoch %d, elapsed time %f, temperature %f"%(i, ts, setpoint)
if self.simulate:
temp = setpoint + random.gauss(-20, 15)
else:
temp = self.monitor.temperature
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)
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, block=True)
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)
time.sleep(self.interval - (time.time()-now))
except:
import traceback
traceback.print_exc()
if self.callback is not None:
self.callback(temp, setpoint, pid_out)
print "Started at %f"%self.start_time
self.regulator.off()
class PID(object):
"""
Discrete PID control
#The recipe gives simple implementation of a Discrete Proportional-Integral-Derivative (PID) controller. PID controller gives output value for error between desired reference input and measurement feedback to minimize error value.
#More information: http://en.wikipedia.org/wiki/PID_controller
#
#cnr437@gmail.com
#
####### Example #########
#
#p=PID(3.0,0.4,1.2)
#p.setPoint(5.0)
#while True:
# pid = p.update(measurement_value)
#
#
"""
def __init__(self, P=2.0, I=0.0, D=1.0, Derivator=0, Integrator=0, Integrator_max=500, Integrator_min=-500):
self.Kp=P
self.Ki=I
self.Kd=D
self.Derivator=Derivator
self.Integrator=Integrator
self.Integrator_max=Integrator_max
self.Integrator_min=Integrator_min
self.set_point=0.0
self.error=0.0
def update(self,current_value):
"""
Calculate PID output value for given reference input and feedback
"""
self.error = self.set_point - current_value
self.P_value = self.Kp * self.error
self.D_value = self.Kd * ( self.error - self.Derivator)
self.Derivator = self.error
self.Integrator = self.Integrator + self.error
if self.Integrator > self.Integrator_max:
self.Integrator = self.Integrator_max
elif self.Integrator < self.Integrator_min:
self.Integrator = self.Integrator_min
self.I_value = self.Integrator * self.Ki
PID = self.P_value + self.I_value + self.D_value
print "PID: %f, %f, %f: %f"%(self.P_value, self.I_value, self.D_value, PID)
return PID
def setPoint(self,set_point):
"""
Initilize the setpoint of PID
"""
self.set_point = set_point
self.Integrator=0
self.Derivator=0
def setIntegrator(self, Integrator):
self.Integrator = Integrator
def setDerivator(self, Derivator):
self.Derivator = Derivator
def setKp(self,P):
self.Kp=P
def setKi(self,I):
self.Ki=I
def setKd(self,D):
self.Kd=D
def getPoint(self):
return self.set_point
def getError(self):
return self.error
def getIntegrator(self):
return self.Integrator
def getDerivator(self):
return self.Derivator
time.sleep(self.interval - (time.time()-now))

14
kiln/paths.py 100644
Wyświetl plik

@ -0,0 +1,14 @@
import os
basepath = os.path.join(os.environ['HOME'], ".config", "pipid")
profile_path = os.path.join(basepath, "profiles")
log_path = os.path.join(basepath, "logs")
if not os.path.exists(profile_path):
os.makedirs(profile_path)
if not os.path.exists(log_path):
os.makedirs(log_path)
cwd = os.path.abspath(os.path.split(__file__)[0])
html_static = os.path.join(cwd, "static")
html_templates = os.path.join(cwd, "templates")

Wyświetl plik

@ -1,43 +1,123 @@
import time
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import re
import time
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]
import paths
cone_symbol = re.compile(r'\^([0-9]{1,3})')
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)
self.parent.clients.remove(self)
class DataRequest(tornado.web.RequestHandler):
def initialize(self, monitor):
self.monitor = monitor
class ManagerHandler(tornado.web.RequestHandler):
def initialize(self, manager):
self.manager = manager
class MainHandler(ManagerHandler):
def get(self):
output = [dict(time=ts[0], temp=ts[1]) for ts in self.monitor.history]
files = os.listdir(paths.profile_path)
fixname = lambda x: cone_symbol.sub(r'Δ\1', os.path.splitext(x)[0].replace("_", " "))
profiles = dict((fname, fixname(fname)) for fname in files)
return self.render(os.path.join(paths.html_templates, "main.html"),
state=self.manager.state.__class__.__name__,
profiles=profiles,
)
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 ProfileHandler(tornado.web.RequestHandler):
def get(self, name):
try:
with open(os.path.join(paths.profile_path, name)) as fp:
self.write(fp.read())
except IOError:
self.write_error(404)
def post(self, name):
try:
schedule = json.loads(self.get_argument("schedule"))
fname = os.path.join(paths.profile_path, name)
with open(fname, 'w') as fp:
json.dump(schedule, fp)
self.write(dict(type="success"))
except IOError:
self.write_error(404)
except Exception as e:
self.write(dict(type="error", error=repr(e), msg=traceback.format_exc()))
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:
self._run(action, self.get_query_argument)
self.write(json.dumps(dict(type="success")))
except Exception as e:
self.write(json.dumps(dict(type="error", error=repr(e), msg=traceback.format_exc())))
def post(self, action):
try:
self._run(action, self.get_argument)
self.write(json.dumps(dict(type="success")))
except Exception as e:
self.write(json.dumps(dict(type="error", error=repr(e), msg=traceback.format_exc())))
class WebApp(object):
def __init__(self, monitor, port=8888):
def __init__(self, manager, port=8888):
self.handlers = [
(r"/ws/", ClientSocket, dict(parent=self)),
(r"/data.json", DataRequest, dict(monitor=monitor)),
(r"/(.*)", tornado.web.StaticFileHandler, dict(path=cwd)),
(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"^/profile/?(.*)$", ProfileHandler),
(r"^/(.*)$", tornado.web.StaticFileHandler, dict(path=paths.html_static)),
]
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):
@ -47,14 +127,11 @@ class WebApp(object):
if __name__ == "__main__":
try:
import thermo
monitor = thermo.Monitor()
import manager
kiln = manager.Manager(simulate=True)
app = WebApp(kiln)
kiln._send = app.send
app = WebApp(monitor)
def send_temp(time, temp):
app.send(dict(time=time, temp=temp))
monitor.callback = send_temp
monitor.start()
app.run()
except KeyboardInterrupt:
monitor.stop()
kiln.manager_stop()

140
kiln/states.py 100644
Wyświetl plik

@ -0,0 +1,140 @@
"""Based on the pattern provided here:
http://python-3-patterns-idioms-test.readthedocs.org/en/latest/StateMachine.html
"""
import time
import traceback
import manager
from collections import deque
class State(object):
def __init__(self, manager):
self.parent = manager
def run(self):
"""Action that must be continuously run while in this state"""
ts = self.parent.therm.get()
self.history.append(ts)
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, 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 Lit(State):
def __init__(self, parent, history):
super(Lit, self).__init__(parent)
self.history = deque(history)
def set(self, value):
try:
self.parent.regulator.set(float(value))
return dict(type="success")
except:
return dict(type="error", msg=traceback.format_exc())
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 Cooling, dict(history=self.history)
class Cooling(State):
def __init__(self, parent, history):
super(Cooling, self).__init__(parent)
self.history = history
def run(self):
ts = self.parent.therm.get()
self.history.append(ts)
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, start_time=None, **kwargs):
super(Running, self).__init__(parent)
self.start_time = start_time
self.profile = manager.Profile(therm=self.parent.therm, regulator=self.parent.regulator,
callback=self._notify, start_time=start_time, **kwargs)
self.history = history
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"))
_shutoff(self.parent.regulator, self.parent.notify)
return Cooling, dict(history=self.history)
return super(Running, self).run()
def pause(self):
self.profile.stop()
return Lit, dict(history=self.history)
def stop(self):
self.profile.stop()
_shutoff(self.parent.regulator, self.parent.notify)
return Cooling, dict(history=self.history)
def _ignite(regulator, notify):
try:
regulator.ignite()
msg = dict(type="success")
except ValueError:
msg = dict(type="error", msg="Cannot ignite: regulator not off")
except Exception as e:
msg = dict(type="error", error=repr(e), msg=traceback.format_exc())
notify(msg)
def _shutoff(regulator, notify):
try:
regulator.off()
msg = dict(type="success")
except ValueError:
msg = dict(type="error", msg="Cannot shutoff: regulator not lit")
except Exception as e:
msg = dict(type="error", error=repr(e), msg=traceback.format_exc())
notify(msg)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -0,0 +1,73 @@
body {
padding-top: 70px;
}
.row-space {
margin-bottom:15px;
}
.output-slider {
width:7% !important;
}
.temperature {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
.axis .tick{
stroke:#DDD;
stroke-width:.5px;
}
.domain{
display:none;
}
#graph {
width:100%;
height:500px;
}
#stop_button_navbar {
margin-left:10px;
margin-right:10px;
}
#current_temp {
font-weight:bold;
font-size:200%;
color:black;
}
.profile-pane {
fill:#EEE;
}
.profile-pane-stroke {
fill:none;
stroke:#CCC;
stroke-width:3px;
cursor:ew-resize;
}
.profile-pane-stroke:hover {
stroke-width:15px;
stroke:#333;
}
.profile-line {
stroke:green;
stroke-width:1.5px;
fill:none;
}
.profile-line.dot {
fill:white;
stroke:black;
stroke-width:1px;
}
.profile-line.dot:hover {
stroke-width:3px;
}
#profile-node-info {
float:left;
position:absolute;
display:none;
width:200px;
}

Plik binarny nie jest wyświetlany.

Plik binarny nie jest wyświetlany.

File diff suppressed because one or more lines are too long

5
kiln/static/js/d3.v3.min.js vendored 100644

File diff suppressed because one or more lines are too long

4
kiln/static/js/jquery.min.js vendored 100644

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -0,0 +1,207 @@
/*
* juration - a natural language duration parser
* https://github.com/domchristie/juration
*
* Copyright 2011, Dom Christie
* Licenced under the MIT licence
*
*/
(function() {
var UNITS = {
seconds: {
patterns: ['second', 'sec', 's'],
value: 1,
formats: {
'chrono': '',
'micro': 's',
'short': 'sec',
'long': 'second'
}
},
minutes: {
patterns: ['minute', 'min', 'm(?!s)'],
value: 60,
formats: {
'chrono': ':',
'micro': 'm',
'short': 'min',
'long': 'minute'
}
},
hours: {
patterns: ['hour', 'hr', 'h'],
value: 3600,
formats: {
'chrono': ':',
'micro': 'h',
'short': 'hr',
'long': 'hour'
}
},
days: {
patterns: ['day', 'dy', 'd'],
value: 86400,
formats: {
'chrono': ':',
'micro': 'd',
'short': 'day',
'long': 'day'
}
},
weeks: {
patterns: ['week', 'wk', 'w'],
value: 604800,
formats: {
'chrono': ':',
'micro': 'w',
'short': 'wk',
'long': 'week'
}
},
months: {
patterns: ['month', 'mon', 'mo', 'mth'],
value: 2628000,
formats: {
'chrono': ':',
'micro': 'm',
'short': 'mth',
'long': 'month'
}
},
years: {
patterns: ['year', 'yr', 'y'],
value: 31536000,
formats: {
'chrono': ':',
'micro': 'y',
'short': 'yr',
'long': 'year'
}
}
};
var stringify = function(seconds, options) {
if(!_isNumeric(seconds)) {
throw "juration.stringify(): Unable to stringify a non-numeric value";
}
if((typeof options === 'object' && options.format !== undefined) && (options.format !== 'micro' && options.format !== 'short' && options.format !== 'long' && options.format !== 'chrono')) {
throw "juration.stringify(): format cannot be '" + options.format + "', and must be either 'micro', 'short', or 'long'";
}
var defaults = {
format: 'short'
};
var opts = _extend(defaults, options);
var units = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'], values = [];
var remaining = seconds;
for(var i = 0, len = units.length; i < len; i++) {
var unit = UNITS[units[i]];
values[i] = Math.floor(remaining / unit.value);
if(opts.format === 'micro' || opts.format === 'chrono') {
values[i] += unit.formats[opts.format];
}
else {
values[i] += ' ' + _pluralize(values[i], unit.formats[opts.format]);
}
remaining = remaining % unit.value;
}
var output = '';
for(i = 0, len = values.length; i < len; i++) {
if(values[i].charAt(0) !== "0" && opts.format != 'chrono') {
output += values[i] + ' ';
}
else if (opts.format == 'chrono') {
output += _padLeft(values[i]+'', '0', i==values.length-1 ? 2 : 3);
}
}
return output.replace(/\s+$/, '').replace(/^(00:)+/g, '').replace(/^0/, '');
};
var parse = function(string) {
// returns calculated values separated by spaces
for(var unit in UNITS) {
for(var i = 0, mLen = UNITS[unit].patterns.length; i < mLen; i++) {
var regex = new RegExp("((?:\\d+\\.\\d+)|\\d+)\\s?(" + UNITS[unit].patterns[i] + "s?(?=\\s|\\d|\\b))", 'gi');
string = string.replace(regex, function(str, p1, p2) {
return " " + (p1 * UNITS[unit].value).toString() + " ";
});
}
}
var sum = 0,
numbers = string
.replace(/(?!\.)\W+/g, ' ') // replaces non-word chars (excluding '.') with whitespace
.replace(/^\s+|\s+$|(?:and|plus|with)\s?/g, '') // trim L/R whitespace, replace known join words with ''
.split(' ');
for(var j = 0, nLen = numbers.length; j < nLen; j++) {
if(numbers[j] && isFinite(numbers[j])) {
sum += parseFloat(numbers[j]);
} else if(!numbers[j]) {
throw "juration.parse(): Unable to parse: a falsey value";
} else {
// throw an exception if it's not a valid word/unit
throw "juration.parse(): Unable to parse: " + numbers[j].replace(/^\d+/g, '');
}
}
return sum;
};
// _padLeft('5', '0', 2); // 05
var _padLeft = function(s, c, n) {
if (! s || ! c || s.length >= n) {
return s;
}
var max = (n - s.length)/c.length;
for (var i = 0; i < max; i++) {
s = c + s;
}
return s;
};
var _pluralize = function(count, singular) {
return count == 1 ? singular : singular + "s";
};
var _isNumeric = function(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
var _extend = function(obj, extObj) {
for (var i in extObj) {
if(extObj[i] !== undefined) {
obj[i] = extObj[i];
}
}
return obj;
};
var juration = {
parse: parse,
stringify: stringify,
humanize: stringify
};
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
//loaders that implement the Node module pattern (including browserify)
module.exports = juration;
} else {
// Otherwise expose juration
window.juration = juration;
// Register as a named AMD module
if ( typeof define === "function" && define.amd ) {
define("juration", [], function () { return juration; } );
}
}
})();

Wyświetl plik

@ -23,7 +23,7 @@ var tempgraph = (function(module) {
.attr("width", this.width)
.attr("height", this.height);
var xfm = this.svg.append("g")
this.pane = this.svg.append("g")
.attr("transform", "translate("+this.margin.left+","+this.margin.top+")")
/*xfm.append("rect")
@ -34,8 +34,8 @@ var tempgraph = (function(module) {
this.x = d3.time.scale().range([0, this.width]);
this.y = d3.scale.linear().range([this.height, 0]);
this.zoom = d3.behavior.zoom().on("zoom", this.draw.bind(this))
.on("zoomend", this.recenter.bind(this));
this.zoom = d3.behavior.zoom(this.obj).on("zoom", this.draw.bind(this))
.on("zoomend", this.recenter.bind(this, .2));
if (options.show_axes === undefined || options.show_axes) {
this.x_axis = d3.svg.axis().scale(this.x).orient("bottom")
@ -44,12 +44,12 @@ var tempgraph = (function(module) {
.tickSize(this.width).tickSubdivide(true);
//setup axies labels and ticks
xfm.append("g")
this.pane.append("g")
.attr("class", "x axis")
//.attr("transform", "translate(0," + this.height + ")")
.call(this.x_axis);
xfm.append("g")
this.pane.append("g")
.attr("class", "y axis")
.attr("transform", "translate("+this.width+", 0)")
.call(this.y_axis)
@ -63,7 +63,7 @@ var tempgraph = (function(module) {
}
this.axes = xfm.append("g")
this.axes = this.pane.append("g")
.attr("class", "axes")
.attr("style", "clip-path:url(#pane)");
window.onresize = this.resize.bind(this);
@ -71,7 +71,6 @@ var tempgraph = (function(module) {
};
module.Graph.prototype.plot = function(data, className, marker) {
this.x.domain(d3.extent(data, function(d) { return d.x; }));
this.y.domain(d3.extent(data, function(d) { return d.y; }));
this.zoom.x(this.x);
var line = d3.svg.line()
@ -84,11 +83,12 @@ var tempgraph = (function(module) {
.attr("d", line);
if (marker !== undefined && marker) {
var marker = this.axes.append("g")
.selectAll(".dot").data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5)
var selector = className.replace(/ /gi, ".");
var key = data.id === undefined ? undefined : function(d){ return d.id;};
var marker = this.axes.append("g").selectAll("."+selector+".dot")
.data(data, key).enter().append("circle")
.attr("class", className+" dot")
.attr("r", 10)
.attr("cx", function(d) { return this.x(d.x); }.bind(this))
.attr("cy", function(d) { return this.y(d.y); }.bind(this));
}
@ -96,7 +96,8 @@ var tempgraph = (function(module) {
this.lines[className] = {line:line, data:data, marker:marker};
this.svg.call(this.zoom);
this.draw();
return line;
this.recenter(.2);
return this.lines[className];
}
module.Graph.prototype.draw = function() {
this.svg.select("g.x.axis").call(this.x_axis);
@ -107,7 +108,7 @@ var tempgraph = (function(module) {
data = this.lines[name].data;
marker = this.lines[name].marker;
if (marker !== undefined) {
this.svg.selectAll(".dot").data(data)
this.axes.selectAll(".dot").data(data)
.attr("cx", function(d) { return this.x(d.x)}.bind(this))
.attr("cy", function(d) { return this.y(d.y)}.bind(this));
}
@ -129,22 +130,45 @@ var tempgraph = (function(module) {
this.height = height;
this.draw();
}
module.Graph.prototype.recenter = function() {
module.Graph.prototype.recenter = function(margin) {
//Argument margin gives the fraction of (max - min) to add to the margin
//Defaults to 0
var extent = [], data, valid,
low = this.x.domain()[0], high=this.x.domain()[1];
for (var name in this.lines) {
data = this.lines[name].data;
valid = data.filter(function(d) { return low <= d.x && d.x <= high; })
extent = extent.concat(valid);
}
this.y.domain(d3.extent(extent, function(d) {return d.y;}));
extent = d3.extent(extent, function(d){return d.y});
if (margin > 0) {
var range = extent[1]-extent[0];
extent[0] -= margin*range;
extent[1] += margin*range;
}
this.y.domain(extent);
this.draw();
}
module.Graph.prototype.update = function(className, data) {
this.lines[className].data = data;
this.axes.select("path."+className).datum(data)
.attr("d", this.lines[className].line);
var join, selector;
if (this.lines[className].marker) {
selector = className.replace(/ /gi, ".");
join = this.axes.selectAll("."+selector+".dot")
.data(data, function(d){ return d.id;});
join.enter().append("circle")
.attr("class", className+" dot")
.attr("r", 10);
join.exit().remove();
join.attr("cx", function(d) { return this.x(d.x); }.bind(this))
.attr("cy", function(d) { return this.y(d.y); }.bind(this));
}
this.draw();
return join;
}
module.Graph.prototype.xlim = function(min, max) {
if (min === undefined)
@ -164,6 +188,15 @@ var tempgraph = (function(module) {
this.svg.select(".ylabel").text(text);
}
module.Graph.prototype.remove = function(className) {
var selector = className.replace(/ /gi, ".");
this.axes.selectAll("path."+selector).remove();
if (this.lines[className].marker) {
this.axes.selectAll("."+selector+".dot").remove();
}
delete this.lines[className];
}
return module;
}(tempgraph || {}));

Wyświetl plik

@ -0,0 +1,223 @@
var tempgraph = (function(module) {
module.Monitor = function(initial) {
this.temperature = initial;
this.profile = null;
//default to F
this.scalefunc = new module.TempScale("F");
this.graph = new module.Graph();
this._mapped = this.temperature.map(this._map_temp.bind(this));
this.graph.plot(this._mapped, "temperature", false);
this.updateTemp(this.last());
this._bindUI();
}
module.Monitor.prototype.updateTemp = function(data) {
var now = new Date(data.time*1000.);
var temp = this.scalefunc.scale(data.temp);
var nowstr = module.format_time(now);
$("#current_time").text(nowstr);
$("#current_temp").text(this.scalefunc.print(Math.round(temp*100) / 100));
//Adjust x and ylims
if (now > this.last().time) {
this.temperature.push(data);
this._mapped.push({x:now, y:temp});
var lims = this.graph.x.domain();
//incoming sample needs to shift xlim
if (now > lims[1]) {
var start = new Date(now.getTime() - lims[1].getTime() + lims[0].getTime());
this.graph.x.domain([start, now]);
}
//If incoming sample is higher or lower than the ylims, expand that as well
var ylims = this.graph.y.domain();
if (temp >= ylims[1] || temp <= ylims[0]) {
this.graph.recenter(.2);
}
this.graph.update("temperature", this._mapped);
}
//update the output slider and text, if necessary
if (data.output !== undefined) {
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);
}
}
//update the profile
if (this.profile)
this.profile.update();
}
module.Monitor.prototype.setProfile = function(schedule, start_time) {
this.profile = new module.Profile(this.graph, this.scalefunc, schedule, start_time);
var start = this.profile.time_start === undefined ?
"Not started" : module.format_time(start_time);
$("#profile_time_total").text(this.profile.time_total);
$("#profile_time_start").text(start);
//$("#profile_time_finish") = this.profile.time_finish();
$("#profile_info, #profile_actions").hide().removeClass("hidden").slideDown();
return this.profile;
}
module.Monitor.prototype.last = function() {
return this.temperature[this.temperature.length-1];
}
module.Monitor.prototype.setScale = function(scale) {
$("a#temp_scale_C").parent().removeClass("active");
$("a#temp_scale_F").parent().removeClass("active");
$("a#temp_scale_cone").parent().removeClass("active");
if (scale == "C") {
$("li a#temp_scale_C").parent().addClass("active");
this.scalefunc = new module.TempScale("C");
this.graph.ylabel("Temperature (°C)")
} else if (scale == "F") {
$("li a#temp_scale_F").parent().addClass("active");
this.scalefunc = new module.TempScale("F");
this.graph.ylabel("Temperature (°F)")
} else if (scale == "cone") {
$("li a#temp_scale_cone").parent().addClass("active");
this.scalefunc = new module.TempScale("cone");
this.graph.ylabel("Temperature (Δ)");
}
this._mapped = this.temperature.map(this._map_temp.bind(this));
this.graph.y.domain(d3.extent(this._mapped, function(d) { return d.y; }));
this.updateTemp(this.last());
this.graph.update("temperature", this._mapped);
if (this.profile)
this.profile.setScale(this.scalefunc);
}
module.Monitor.prototype._map_temp = function(d) {
return {x:new Date(d.time*1000), y:this.scalefunc.scale(d.temp)};
}
module.Monitor.prototype.setState = function(name) {
if (name == "Lit") {
$("#ignite_button").addClass("disabled");
$("#current_output").removeAttr("disabled");
$("#stop_button").removeClass("disabled");
$("#stop_button_navbar").removeClass("hidden disabled");
$("#profile_select").removeClass("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");
$("#profile_select").removeClass("disabled");
} else if (name == "Profile") {
}
}
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));
$("#profile_name").val("");
$("#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 + "%");
})
});
$("#profile_list a").click(function(e) {
$("#profile_list li").removeClass("active");
$(e.target).parent().addClass("active");
$("#profile_name").val($(e.target).text());
var fname = $(e.target).attr("data-fname");
if (this.profile)
this.profile.cleanup();
$.getJSON("/profile/"+fname, function(data) {
this.setProfile(data);
}.bind(this));
}.bind(this));
try {
var sock = new WebSocket("ws://"+window.location.hostname+":"+window.location.port+"/ws/");
sock.onmessage = function(event) {
var data = JSON.parse(event.data);
if (data.type == "temperature")
this.updateTemp(data);
else if (data.type == "state") {
this.setState(data.state);
}
}.bind(this);
} catch (e) {}
}
module.Monitor.prototype._disable_all = function() {
$("button").addClass("disabled");
$("input").attr("disabled", "disabled");
}
module.TempScale = function(name) {
if (name == "C") {
this.scale = function(t) { return t;};
this.inverse = function(t) { return t;};
this.print = function(t) { return t+"°C"}
} else if (name == "F") {
this.scale = function(temp) { return temp * 9 / 5 + 32; }
this.inverse = function(temp) { return (temp - 32) * 5 / 9;}
this.print = function(t) { return t+"°F"}
} else if (name == "cone") {
}
}
module.TempScale.C_to_cone = function(temp) {
var cones = [600,614,635,683,717,747,792,804,838,852,884,894,900,923,955,984,999,1046,1060,1101,1120,1137,1154,1162,1168,1186,1196,1222,1240,1263,1280,1305,1315,1326,1346]
var names = [];
for (var i = -22; i < 0; i++) {
names.push("0"+(""+i).slice(1));
}
for (var i = 1; i < 14; i++) {
names.push(""+i);
}
return cones, names
}
module.format_time = function(now) {
if (!(now instanceof Date))
now = new Date(now);
var hourstr = now.getHours() % 12;
hourstr = hourstr == 0 ? 12 : hourstr;
var minstr = now.getMinutes();
minstr = minstr < 10 ? "0"+minstr : minstr;
return hourstr + ":" + minstr + (now.getHours() >= 12 ? " pm" : " am");
}
return module;
}(tempgraph || {}));

Wyświetl plik

@ -0,0 +1,338 @@
var tempgraph = (function(module) {
module.Profile = function(graph, scale, schedule, start_time, running) {
var end = schedule[schedule.length-1][0];
this.length = end;
this.time_total = juration.stringify(end);
this.time_start = start_time;
this.schedule = schedule;
this.running = running === undefined ? false : running;
this.graph = graph;
this.scalefunc = scale;
this.drag_start = d3.behavior.drag()
.on("dragstart", function() {
d3.event.sourceEvent.stopPropagation();
}).on("drag.profile", this.dragStart.bind(this));
//Generate the highlight pane to indicate where the profile is running
this.pane_stroke = this.graph.pane.insert("line", ":first-child")
.attr("class", "profile-pane-stroke")
.attr("y1", 0).attr("y2", this.graph.height)
.call(this.drag_start);
this.pane = this.graph.pane.insert("rect", ":first-child")
.attr("class", "profile-pane")
.attr("height", this.graph.height)
.attr("clip-path", "url(#pane)");
this.line = this.graph.plot(this._schedule(), "profile-line", true);
//immediately view range from 10 min before to end time of profile
var now = new Date();
var rstart = new Date(now.getTime() - 10*60*100);
var rend = this.time_finish(now);
this.graph.xlim(rstart, rend);
this.drag = d3.behavior.drag().origin(function(d) {
return {x:this.graph.x(d.x), y:this.graph.y(d.y)};
}.bind(this)).on("dragstart", function(d) {
d3.event.sourceEvent.stopPropagation();
this._node = this._findNode(d);
}.bind(this));
this.update();
//events
this._bindUI();
}
module.Profile.prototype.time_finish = function() {
var now = this.time_start instanceof Date ? this.time_start : new Date();
return new Date(now.getTime() + this.length*1000);
}
module.Profile.prototype.setScale = function(scale) {
this.scalefunc = scale;
this.update();
}
module.Profile.prototype.update = function() {
var start_time = this.time_start instanceof Date ? this.time_start : new Date();
var end_time = new Date(start_time.getTime()+this.length*1000);
var width = this.graph.x(end_time) - this.graph.x(start_time);
var x = this.graph.x(start_time);
this.pane.attr("width", width)
.attr("transform","translate("+this.graph.x(start_time)+",0)");
this.pane_stroke.attr("x1", x).attr("x2", x);
var join = this.graph.update("profile-line", this._schedule());
join.on("mouseover.profile", this.hoverNode.bind(this))
.on("mouseout.profile", this._hideInfo.bind(this))
.on("dblclick.profile", this.delNode.bind(this));
join.call(this.drag);
//update the profile info box
var start = this.time_start instanceof Date ? module.format_time(this.time_start) : "Not started";
var finish = this.time_finish();
var remain = (finish - (new Date())) / 1000;
$("#profile_time_finish").text(module.format_time(finish));
$("#profile_time_start").text(start);
$("#profile_time_remain").text(juration.stringify(remain));
}
module.Profile.prototype._bindUI = function() {
$("#profile_name").attr("disabled", "disabled");
$("#profile_actions .btn-success").click(this.save.bind(this));
$("#profile_actions .btn-primary").click(this.start.bind(this));
$("#profile_actions .btn-default").click(this.pause.bind(this));
// Info pane events
var updateNode = function() {
clearTimeout(this.timeout_infoedit);
var time = juration.parse($("#profile-node-info input.time").val());
var temp = parseFloat($("#profile-node-info input.temp").val());
this._updateNode(this._node, time, temp);
}.bind(this)
$("#profile-node-info").on("mouseout.profile", this._hideInfo.bind(this));
$("#profile-node-info").on("mouseover.profile", function() {
clearTimeout(this.timeout_infohide);
}.bind(this));
$("#profile-node-info input").on("keypress", function(e) {
clearTimeout(this.timeout_infoedit);
if (e.keyCode == 13) {
updateNode();
} else {
this.timeout_infoedit = setTimeout(updateNode, 2000);
}
}.bind(this));
$("#profile-node-info input").on("blur", function() {
this._focused = false;
updateNode();
this._hideInfo();
}.bind(this));
$("#profile-node-info input").on("focus", function() {
this._focused = true;
}.bind(this));
//Graph events
this.graph.zoom.on("zoom.profile", this.update.bind(this));
this.setState();
}
module.Profile.prototype._schedule = function() {
var start_time = this.time_start instanceof Date ? this.time_start : new Date();
var schedule = [];
for (var i = 0; i < this.schedule.length; i++) {
var time = new Date(start_time.getTime() + this.schedule[i][0]*1000);
var temp = this.scalefunc.scale(this.schedule[i][1]);
schedule.push({id:i, x:time, y:temp});
}
return schedule;
}
module.Profile.prototype._hideInfo = function() {
this.timeout_infohide = setTimeout(function() {
if (!this._focused)
$("#profile-node-info").fadeOut(100);
}.bind(this), 250);
}
module.Profile.prototype._findNode = function(d) {
return d.id;
}
module.Profile.prototype._updateNode = function(node, time, temp) {
var start_time = this.time_start instanceof Date ? this.time_start : new Date();
if (!(time instanceof Date)) {
//This is probably just a direct offset, no need to compute time
this.schedule[node][0] = time;
} else if (node == 0) {
this.schedule[node][0] = 0;
} else {
var newtime = (time - start_time.getTime()) / 1000;
this.schedule[node][0] = newtime;
}
//update length only if we're editing the final node
if (node == this.schedule.length-1) {
this.length = this.schedule[node][0];
}
this.schedule[node][1] = this.scalefunc.inverse(temp);
//if we're dragging this node "behind" the previous, push it back as well
//except if the previous one is the first node, in which case just set it to zero
if (node > 0 && this.schedule[node-1][0] >= newtime) {
if (node-1 == 0)
this.schedule[node][0] = 0;
else
this.schedule[node-1][0] = newtime;
} else if (node < this.schedule.length-1 && this.schedule[node+1][0] < newtime){
this.schedule[node+1][0] = newtime;
if (node+1 == this.schedule.length-1)
this.length = this.schedule[node+1][0];
}
this._showInfo(node);
this.update();
//Unlock the save buttons and names
$("#profile_name").removeAttr("disabled");
$("#profile_actions .btn-success").removeClass("disabled");
$("#profile_actions .btn-primary").addClass("disabled");
}
module.Profile.prototype._showInfo = function(node) {
this._node = node;
var start_time = this.time_start instanceof Date ? this.time_start : new Date();
var time = new Date(this.schedule[node][0]*1000 + start_time.getTime());
var temp = this.scalefunc.scale(this.schedule[node][1]);
$("#profile-node-info")
.css('left', this.graph.x(time)+80)
.css('top', this.graph.y(temp)+50)
.fadeIn(100);
$("#profile-node-info div.name").text("Set point "+(node+1));
$("#profile-node-info input.temp").val(this.scalefunc.print(Math.round(temp*100)/100));
var timestr;
try {
timestr = juration.stringify(this.schedule[node][0]);
} catch (e) {
timestr = 0;
}
$("#profile-node-info input.time").val(timestr);
}
module.Profile.prototype.addNode = function() {
d3.event.stopPropagation();
var mouse = d3.mouse(this.graph.pane[0][0]);
var start_time = this.time_start instanceof Date ? this.time_start : new Date();
var secs = (this.graph.x.invert(mouse[0]) - start_time) / 1000;
var start, end;
for (var i = 0; i < this.schedule.length-1; i++) {
start = this.schedule[i][0];
end = this.schedule[i+1][0];
if (start < secs && secs < end) {
var t2 = this.schedule[i+1][1], t1 = this.schedule[i][1];
var frac = (secs - start) / (end - start);
var temp = frac * (t2 - t1) + t1;
this.schedule.splice(i+1, 0, [secs, temp]);
}
}
this.update();
$("#profile_name").removeAttr("disabled");
$("#profile_actions .btn-success").removeClass("disabled");
$("#profile_actions .btn-primary").addClass("disabled");
}
module.Profile.prototype.delNode = function(d) {
d3.event.stopPropagation();
var node = this._findNode(d);
//ignore attempts to delete the starting and ending nodes
if (node != 0 && this.schedule.length > 2) {
this.schedule.splice(node, 1);
if (node == this.schedule.length) {
this.length = this.schedule[node-1][0];
}
}
this.update();
$("#profile_name").removeAttr("disabled");
$("#profile_actions .btn-success").removeClass("disabled");
$("#profile_actions .btn-primary").addClass("disabled");
}
module.Profile.prototype.dragNode = function(d) {
var time = this.graph.x.invert(d3.event.x);
var temp = this.graph.y.invert(d3.event.y);
this._updateNode(d.id, time, temp);
}
module.Profile.prototype.hoverNode = function(d) {
clearTimeout(this.timeout_infohide);
this._showInfo(d.id);
}
module.Profile.prototype.dragStart = function() {
this.time_start = this.graph.x.invert(d3.event.x);
if (this.time_start > new Date())
this.time_start = null;
this.update();
}
module.Profile.prototype.save = function() {
//convert name into filename
var rawname = $("#profile_name").val();
var name = rawname.replace(/ /gi, "_");
name = name.replace(/Δ/gi, "^");
name += ".json";
var post = {schedule:JSON.stringify(this.schedule)};
$.post("/profile/"+name, post).done(function(result) {
if (result.type == "success") {
$("#profile_name").attr("disabled", "disabled");
$("#profile_actions .btn-success").addClass("disabled");
//Check if the name exists in the menu, otherwise add new entry
var notnew = false;
$("#profile_list a").each(function() {
console.log($(this).data("fname"), $(this).data("fname") == name);
notnew = $(this).data("fname") == name || notnew;
});
if (!notnew) {
//Add a new entry into the profile list dropdown
$("#profile_list li").removeClass("active");
var html = "<li><a href='#' data-fname='"+name+"' class='active'>"+rawname+"</a></li>";
$("#profile_list").append(html).addClass("active").select("a")
.click(function(e) {
$("#profile_list a").removeClass("active");
$(e.target).addClass("active");
$("#profile_name").val($(e.target).text());
var fname = $(e.target).attr("data-fname");
this.cleanup();
$.getJSON("/profile/"+fname, function(data) {
monitor.setProfile(data);
});
}.bind(this));
}
this.setState(false);
} else if (result.type == "error") {
alert(result.msg);
}
}.bind(this));
}
module.Profile.prototype.setState = function(running) {
this.running = running === undefined ? this.running : running;
console.log("Set State: ", this.running);
if (this.running) {
this.line.marker.on("dblclick.profile", null);
this.graph.pane.on("dblclick.profile", null);
$("#profile-node-info input").attr("disabled", "disabled");
this.drag.on("drag.profile", null);
this.drag_start.on("drag.profile", null);
$("#profile_actions .btn-success").addClass("disabled");
$("#profile_actions .btn-primary").addClass("disabled");
$("#profile_actions .btn-default").removeClass("disabled");
} else {
this.line.marker.on("dblclick.profile", this.delNode.bind(this));
this.graph.pane.on("dblclick.profile", this.addNode.bind(this));
$("#profile-node-info input").removeAttr("disabled");
this.drag.on("drag.profile", this.dragNode.bind(this));
this.drag_start.on("drag.profile", this.dragStart.bind(this));
$("#profile_actions .btn-success").addClass("disabled");
$("#profile_actions .btn-primary").removeClass("disabled");
$("#profile_actions .btn-default").addClass("disabled");
}
}
module.Profile.prototype.cleanup = function() {
this.graph.remove("profile-line");
this.pane.remove();
this.pane_stroke.remove();
}
module.Profile.prototype.pause = function() {
$("#profile_actions .btn-default").addClass("disabled");
//TODO: ajax query
this.setState(false)
}
module.Profile.prototype.start = function() {
$("#profile_actions .btn-primary").addClass("disabled");
//TODO: ajax query
//This should be done by the query
this.setState(true);
if (!(this.time_start instanceof Date))
this.time_start = new Date();
}
return module;
}(tempgraph || {}));

Wyświetl plik

@ -3,6 +3,9 @@ import atexit
import threading
import warnings
import Queue
import logging
logger = logging.getLogger("kiln.Stepper")
try:
from RPi import GPIO
@ -16,7 +19,10 @@ class Stepper(threading.Thread):
[0,0,1,1],
[1,0,0,1]]
def __init__(self, pin1=5, pin2=6, pin3=13, pin4=19, timeout=1):
def __init__(self, pin1=5, pin2=6, pin3=13, pin4=19, timeout=1, home_pin=None):
super(Stepper, self).__init__()
self.daemon = True
self.queue = Queue.Queue()
self.finished = threading.Event()
@ -26,11 +32,12 @@ class Stepper(threading.Thread):
GPIO.setup(pin2, GPIO.OUT)
GPIO.setup(pin3, GPIO.OUT)
GPIO.setup(pin4, GPIO.OUT)
self.home_pin = home_pin
GPIO.setup(home_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
self.phase = 0
self.timeout = timeout
super(Stepper, self).__init__()
self.daemon = True
self.home()
self.start()
def stop(self):
self.queue.put((None, None, None))
@ -51,6 +58,18 @@ class Stepper(threading.Thread):
self.queue.put((num, speed, block))
self.finished.wait()
def home(self):
if self.home_pin is None:
raise ValueError("No homing switch defined")
while GPIO.input(self.home_pin):
for i in range(len(self.pattern)):
for pin, out in zip(self.pins, self.pattern[i]):
GPIO.output(pin, out)
time.sleep(1. / 150.)
self.phase = 0
def run(self):
try:
step, speed, block = self.queue.get()
@ -135,12 +154,15 @@ class StepperSim(object):
def stop(self):
print "stopping"
class Regulator(object):
def __init__(self, maxsteps=4500, minsteps=2480, speed=150, ignite_pin=None, simulate=False):
class Regulator(threading.Thread):
def __init__(self, maxsteps=4500, minsteps=2480, speed=150, ignite_pin=None, flame_pin=None, simulate=False):
"""Set up a stepper-controlled regulator. Implement some safety measures
to make sure everything gets shut off at the end
TODO: integrate flame sensor by converting this into a thread, and checking
flame state regularly. If flame sensor off, immediately increase gas and attempt
reignition, or shut off after 5 seconds of failure.
Parameters
----------
maxsteps : int
@ -152,11 +174,13 @@ class Regulator(object):
ignite_pin : int or None
If not None, turn on this pin during the ignite sequence
"""
if simulate:
self.stepper = StepperSim()
else:
self.stepper = Stepper()
self.stepper.start()
self.current = 0
self.max = maxsteps
self.min = minsteps
@ -165,6 +189,9 @@ class Regulator(object):
self.ignite_pin = ignite_pin
if ignite_pin is not None:
GPIO.setup(ignite_pin, OUT)
self.flame_pin = flame_pin
if flame_pin is not None:
GPIO.setup(flame_pin, IN)
def exit():
if self.current != 0:
@ -173,7 +200,10 @@ class Regulator(object):
atexit.register(exit)
def ignite(self, start=2800, delay=1):
print "Ignition..."
if self.current != 0:
raise ValueError("Must be off to ignite")
logger.info("Ignition start")
self.stepper.step(start, self.speed, block=True)
if self.ignite_pin is not None:
GPIO.output(self.ignite_pin, True)
@ -182,13 +212,13 @@ class Regulator(object):
GPIO.output(self.ignite_pin, False)
self.stepper.step(self.min - start, self.speed, block=True)
self.current = self.min
print "Done!"
logger.info("Ignition complete")
def off(self, block=True):
print "Turning off..."
self.stepper.step(-self.current, self.speed, block=block)
logger.info("Shutting off gas")
#self.stepper.step(-self.current, self.speed, block=block)
self.stepper.home()
self.current = 0
print "Done!"
def set(self, value, block=False):
if self.current == 0:
@ -200,3 +230,15 @@ class Regulator(object):
print "Currently at %d, target %d, stepping %d"%(self.current, target, nsteps)
self.current = target
self.stepper.step(nsteps, self.speed, block=block)
@property
def output(self):
out = (self.current - self.min) / float(self.max - self.min)
if out < 0:
return -1
return out
def run(self):
"""Check the status of the flame sensor"""
#since the flame sensor does not yet exist, we'll save this for later
pass

Wyświetl plik

@ -1,126 +0,0 @@
var tempgraph = (function(module) {
module.Monitor = function(initial) {
this.temperature = initial;
//default to F
this.scalefunc = module.temp_to_F;
this.temp_suffix = "°F"
this.temp_prefix = ""
this.graph = new tempgraph.Graph();
this._mapped = this.temperature.map(this._map_temp.bind(this));
this.graph.plot(this._mapped, "temperature", false);
this.update_temp(this.last());
this._bindUI();
}
module.Monitor.prototype.update_temp = function(data) {
var now = new Date(data.time*1000.);
var temp = this.scalefunc(data.temp);
var minstr = now.getMinutes();
minstr = minstr.length < 2 ? "0"+minstr : minstr;
var nowstr = now.getHours() % 12 + ":" + minstr + (now.getHours() > 12 ? " pm" : " am");
$("#current_time").text(nowstr);
$("#current_temp").text(this.temp_prefix+temp+this.temp_suffix);
//Adjust x and ylims
if (now > this.last().time) {
this.temperature.push(data);
this._mapped.push({x:now, y:temp});
var lims = this.graph.x.domain();
//incoming sample needs to shift xlim
if (now > lims[1]) {
var start = new Date(now.getTime() - lims[1].getTime() + lims[0].getTime());
this.graph.x.domain([start, now]);
//If incoming sample is higher or lower than the ylims, expand that as well
var ylims = this.graph.y.domain(), range = 2*(ylims[1] - ylims[0]);
if (temp >= ylims[1]) {
this.graph.y.domain([ylims[0], ylims[0]+range]);
} else if (temp <= ylims[0]) {
this.graph.y.domain([ylims[1]-range, ylims[1]]);
}
}
this.graph.update("temperature", this._mapped);
}
//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);
}
}
module.Monitor.prototype.update_UI = function(packet) {
}
module.Monitor.prototype.setProfile = function(profile) {
}
module.Monitor.prototype.last = function() {
return this.temperature[this.temperature.length-1];
}
module.Monitor.prototype.setScale = function(scale) {
$("a#temp_scale_C").parent().removeClass("active");
$("a#temp_scale_F").parent().removeClass("active");
$("a#temp_scale_cone").parent().removeClass("active");
if (scale == "C") {
$("li a#temp_scale_C").parent().addClass("active");
this.scalefunc = module.temp_to_C;
this.graph.ylabel("Temperature (°C)")
this.temp_suffix = "°C";
this.temp_prefix = "";
} else if (scale == "F") {
$("li a#temp_scale_F").parent().addClass("active");
this.scalefunc = module.temp_to_F;
this.graph.ylabel("Temperature (°F)")
this.temp_suffix = "°F";
this.temp_prefix = "";
} else if (scale == "cone") {
$("li a#temp_scale_cone").parent().addClass("active");
this.scalefunc = module.temp_to_cone;
this.graph.ylabel("Temperature (Δ)");
this.temp_prefix = "Δ";
this.temp_suffix = "";
}
this._mapped = this.temperature.map(this._map_temp.bind(this));
this.graph.y.domain(d3.extent(this._mapped, function(d) { return d.y; }));
this.update_temp(this.last());
this.graph.update("temperature", this._mapped);
}
module.Monitor.prototype._map_temp = function(d) {
return {x:new Date(d.time*1000), y:this.scalefunc(d.temp)};
}
module.Monitor.prototype._bindUI = function() {
try {
var sock = new WebSocket("ws://"+window.location.hostname+":"+window.location.port+"/ws/", "protocolOne");
sock.onmessage = function(event) {
var data = JSON.parse(event.data);
this.update_temp(data);
}.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.temp_to_C = function(temp) { return temp; }
module.temp_to_F = function(temp) {
return temp * 9 / 5 + 32;
}
module.temp_to_cone = function(temp) {
return "Not implemented"
}
return module;
}(tempgraph || {}));
d3.json("data.json", function(error, data) {
monitor = new tempgraph.Monitor(data);
});

Wyświetl plik

@ -8,59 +8,12 @@
<!-- Bootstrap -->
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="css/bootstrap.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="css/bootstrap-theme.min.css">
<style type="text/css">
body {
padding-top: 70px;
}
.row-space {
margin-bottom:15px;
}
.output-slider {
width:7% !important;
}
.temperature {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
.axis .tick{
stroke:#DDD;
stroke-width:.5px;
}
.domain{
display:none;
}
#graph {
width:100%;
height:500px;
}
rect.pane {
cursor: move;
fill: none;
pointer-events: all;
}
#stop-button {
margin-left:10px;
margin-right:10px;
}
#current_temp {
font-weight:bold;
font-size:200%;
color:black;
}
</style>
<link rel="stylesheet" href="css/temp_monitor.css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
@ -70,7 +23,6 @@
<![endif]-->
</head>
<body>
<div class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
@ -92,12 +44,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>
@ -108,30 +60,30 @@
<div class="row">
<div class="col-sm-8 col-md-8 row-space">
<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>
<svg id="graph" class="row-space"></svg>
<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>
<div class="col-sm-4 col-md-4">
<div class="input-group input-group-lg row-space">
<input type="text" disabled="disabled" class="form-control" placeholder="Load profile">
<input id="profile_name" type="text" disabled="disabled" class="form-control" placeholder="Load profile">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a href="#">...</a></li>
<ul id="profile_list" class="dropdown-menu dropdown-menu-right" role="menu">
{% for fname, name in profiles.items() %}
<li><a href="#" data-fname="{{fname}}">{{ name }}</a></li>
{% end %}
</ul>
</div><!-- /btn-group -->
</div><!-- /input-group -->
@ -139,9 +91,13 @@
<div class='panel-body'>
<dl class='dl-horizontal'>
<dt>Total time</dt>
<dd></dd>
<dd id="profile_time_total"></dd>
<dt>Start time</dt>
<dd id="profile_time_start"></dd>
<dt>Finish at</dt>
<dd></dd>
<dd id="profile_time_finish"></dd>
<dt>Remaining</dt>
<dd id="profile_time_remain"></dd>
</dl>
</div>
</div>
@ -155,9 +111,38 @@
</div>
</div>
<div id='profile-node-info' class='panel panel-info'>
<div class='panel-heading name'>
Set point
</div>
<div class='panel-body'>
<div class="input-group input-group-sm row-space">
<span class="input-group-addon">Time</span>
<input type="text" class="form-control time">
</div>
<div class="input-group input-group-sm">
<span class="input-group-addon">Temp</span>
<input type="text" class="form-control temp">
</div>
</div>
</div>
</div> <!-- /container -->
<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="js/jquery.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="js/juration.js"></script>
<script type="text/javascript" src="js/temp_graph.js"></script>
<script type="text/javascript" src="js/temp_profile.js"></script>
<script type="text/javascript" src="js/temp_monitor.js"></script>
<script type="text/javascript">
var monitor;
d3.json("temperature.json", function(error, data) {
monitor = new tempgraph.Monitor(data);
monitor.setState("{{ state }}");
});
</script>
</body>
</html>

Wyświetl plik

@ -1,9 +1,10 @@
import re
import time
import random
import datetime
import logging
import threading
from collections import deque
from collections import deque, namedtuple
logger = logging.getLogger("thermo")
@ -18,23 +19,13 @@ def temp_to_cone(temp):
return names[i]+'.%d'%int(frac*10)
return "13+"
class Monitor(threading.Thread):
def __init__(self, name="3b-000000182b57", callback=None):
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.history = deque(maxlen=1048576)
self.callback = callback
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
super(Monitor, self).__init__()
self.running = True
self.history = deque(maxlen=smooth_window)
self.last = None
def _read_temp(self):
with open(self.device, 'r') as f:
@ -49,39 +40,71 @@ class Monitor(threading.Thread):
if match is not None:
return float(match.group(1)) / 1000.0
def stop(self):
self.running = False
if self.display is not None:
self.display.stop()
def get(self):
"""Blocking call to retrieve latest temperature sample"""
self.history.append(self._read_temp())
self.last = time.time()
return self.temperature
@property
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, 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):
self.history.append(self._read_temp())
self.last = time.time()
return self.temperature
@property
def temperature(self):
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):
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):
# with open("/home/pi/data.txt", "w") as f:
# f.write("time\ttemp\n")
while self.running:
temp = self._read_temp()
now = time.time()
self.history.append((now, temp))
if self.callback is not None:
self.callback(now, temp)
with open("/home/pi/data.txt", "a") as f:
f.write("%f\t%f\n"%(now, temp))
_, temp = self.therm.get()
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()
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__":
mon = Monitor()
mon.start()
monitor = Monitor()
monitor.start()

Wyświetl plik

@ -40,11 +40,42 @@ module zipties() {
}
module holder() {
difference() {
intersection() {
translate([0,0,-3.3-thick]) scale([1,1.4]) cylinder(r=36+thick, h=16, $fn=128);
translate([36+thick-20,-zip_pos-3-thick, -10]) cube([20, 2*zip_pos+6+2*thick,20]);
width = 2*zip_pos+6+2*thick;
module slot(length) {
hull() {
cylinder(r=1, h=6);
translate([length,0]) cylinder(r=1, h=6);
}
}
//translate([18,-width/2, -3.3-thick+16+32-6]) cube([12, 20, 6]);
difference() {
union() {
//main body
intersection() {
translate([0,0,-3.3-thick]) scale([1,1.4]) cylinder(r=36+thick, h=16, $fn=128);
translate([36+thick-20,-width/2, -10]) cube([20, width,20]);
}
//switch tab
intersection() {
translate([0,0,-3.3-thick]) scale([1, 1.4]) difference() {
cylinder(r=36+thick, h=16+34+3, $fn=128);
translate([0,0,-1]) cylinder(r=36, h=16+32+3-2,$fn=128);
}
translate([36+thick-20, -width/2, -3.3-thick]) cube([50, 20, 16+32+4]);
}
}
//Screw slots for switch
translate([20,-width/2+10-9.5/2, -3.3-thick+16+32-1]) {
translate([3,0]) slot(5);
translate([3,9.5]) slot(5);
}
//Slot for wire to ensure no gear tangling
translate([20, -width/2+8.5,-3.3-thick+16+32-3]) cube([50, 3, 3]);
regulator();
zipties();
}

Plik diff jest za duży Load Diff