kopia lustrzana https://github.com/jamesgao/kiln_controller
Merge branch 'master' of github.com:jamesgao/kiln_controller
commit
2663e294d5
|
@ -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
BIN
BOM.ods
Plik binarny nie jest wyświetlany.
Plik diff jest za duży
Load Diff
Plik diff jest za duży
Load Diff
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,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
|
247
kiln/manager.py
247
kiln/manager.py
|
@ -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))
|
||||
|
|
|
@ -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")
|
121
kiln/server.py
121
kiln/server.py
|
@ -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()
|
||||
|
|
|
@ -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
|
@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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; } );
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -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 || {}));
|
||||
|
|
@ -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 || {}));
|
|
@ -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 || {}));
|
|
@ -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
|
|
@ -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);
|
||||
|
||||
});
|
|
@ -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>
|
111
kiln/thermo.py
111
kiln/thermo.py
|
@ -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()
|
|
@ -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
Ładowanie…
Reference in New Issue