├── BOM.ods ├── circuits ├── pcb_top.png └── pcb_eagle.png ├── kiln ├── static │ ├── fonts │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── css │ │ ├── temp_monitor.css │ │ └── bootstrap-theme.min.css │ └── js │ │ ├── juration.js │ │ ├── temp_monitor.js │ │ ├── temp_graph.js │ │ ├── temp_profile.js │ │ └── bootstrap.min.js ├── test_stepper.py ├── paths.py ├── bisque_firing_06.py ├── breakout.py ├── PID.py ├── Adafruit_LEDBackpack.py ├── thermo.py ├── server.py ├── states.py ├── Adafruit_I2C.py ├── manager.py ├── templates │ └── main.html ├── Adafruit_alphanumeric.py └── stepper.py ├── firmware ├── README.md └── controller │ ├── protocol.h │ ├── pushbutton.h │ ├── protocol.cpp │ └── controller.ino ├── .gitignore ├── models ├── bracket.scad ├── gears.scad ├── regulator.scad ├── utils.scad └── involute_gears.scad ├── BOM.md └── LICENSE /BOM.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesgao/kiln_controller/HEAD/BOM.ods -------------------------------------------------------------------------------- /circuits/pcb_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesgao/kiln_controller/HEAD/circuits/pcb_top.png -------------------------------------------------------------------------------- /circuits/pcb_eagle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesgao/kiln_controller/HEAD/circuits/pcb_eagle.png -------------------------------------------------------------------------------- /kiln/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesgao/kiln_controller/HEAD/kiln/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /kiln/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesgao/kiln_controller/HEAD/kiln/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /kiln/test_stepper.py: -------------------------------------------------------------------------------- 1 | import time 2 | import stepper 3 | 4 | def test_noblock(): 5 | reg = stepper.Regulator(ignite_pin=None) 6 | 7 | reg.ignite() 8 | reg.set(.5) 9 | time.sleep(.5) 10 | reg.set(.1) 11 | time.sleep(.5) 12 | reg.set(.5, block=True) 13 | return reg 14 | -------------------------------------------------------------------------------- /kiln/paths.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | basepath = os.path.join(os.environ['HOME'], ".config", "pipid") 4 | profile_path = os.path.join(basepath, "profiles") 5 | log_path = os.path.join(basepath, "logs") 6 | 7 | if not os.path.exists(profile_path): 8 | os.makedirs(profile_path) 9 | if not os.path.exists(log_path): 10 | os.makedirs(log_path) 11 | 12 | cwd = os.path.abspath(os.path.split(__file__)[0]) 13 | html_static = os.path.join(cwd, "static") 14 | html_templates = os.path.join(cwd, "templates") -------------------------------------------------------------------------------- /firmware/README.md: -------------------------------------------------------------------------------- 1 | Firmware 2 | ======== 3 | The kiln controller circuit communicates with the raspberry pi using an I2C bus. This allows multiple connectors to be stacked to enable more controllers and feedback circuits. 4 | 5 | Communication protocol 6 | ---------------------- 7 | 8 | | Register | Input | Meaning | 9 | ------------------------------------------------------------- 10 | | ord('I') | True/False | Toggle ignition | 11 | | ord('M') | integer | Move motor | 12 | | ord('F') | None | Show flame status | -------------------------------------------------------------------------------- /kiln/bisque_firing_06.py: -------------------------------------------------------------------------------- 1 | import manager 2 | import thermo 3 | import sys 4 | import time 5 | 6 | if __name__ == "__main__": 7 | start_time = None 8 | if len(sys.argv) > 1: 9 | start_time = float(sys.argv[1]) 10 | schedule = [[2*60*60, 176], [4*60*60, 620], [6*60*60, 1013], [6*60*60+20*60, 1013]] 11 | mon = thermo.Monitor() 12 | mon.start() 13 | time.sleep(1) 14 | #schedule = [[20, 176], [40, 620], [60, 1013]] 15 | kiln = manager.KilnController(schedule, mon, start_time=start_time, simulate=False) 16 | kiln.run() 17 | mon.stop() 18 | -------------------------------------------------------------------------------- /firmware/controller/protocol.h: -------------------------------------------------------------------------------- 1 | #ifndef PROTOCOL_H 2 | #define PROTOCOL_H 3 | 4 | #include "Wire.h" 5 | 6 | #define MAX_ACTIONS 16 7 | 8 | class Comm { 9 | private: 10 | static char buffer[BUFFER_LENGTH+1]; 11 | static int _nacts; 12 | static char _commands[MAX_ACTIONS]; 13 | static char* (*_actions[MAX_ACTIONS])(int, char*); 14 | static char _current_cmd; 15 | static int _current_len; 16 | 17 | static void _handle_request(int); 18 | static void _handle_response(void); 19 | 20 | public: 21 | Comm(int addr); 22 | int action(char, char* (*)(int, char*)); 23 | }; 24 | 25 | #endif //PROTOCOL_H 26 | -------------------------------------------------------------------------------- /firmware/controller/pushbutton.h: -------------------------------------------------------------------------------- 1 | class pushbutton : public CallBackInterface 2 | { 3 | public: 4 | int n_clicks; 5 | uint8_t pin; 6 | unsigned int interval; 7 | unsigned long last; 8 | 9 | pushbutton (uint8_t _pin, unsigned int _interval): pin(_pin) , interval(_interval) { 10 | dir = 0; 11 | n_clicks = 0; 12 | last = 0; 13 | init(); 14 | }; 15 | void cbmethod() { 16 | last = millis(); 17 | }; 18 | 19 | void update() { 20 | if (last != 0 && (millis() - last) > interval) { 21 | n_clicks += dir; 22 | last = 0; 23 | } 24 | } 25 | 26 | void setDir(int d) { 27 | dir = d; 28 | } 29 | 30 | private: 31 | int dir; 32 | 33 | void init () { 34 | pinMode(pin, INPUT); 35 | digitalWrite(pin, HIGH); 36 | PCintPort::attachInterrupt(pin, this, CHANGE); 37 | }; 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /firmware/controller/protocol.cpp: -------------------------------------------------------------------------------- 1 | #include "protocol.h" 2 | 3 | char Comm::buffer[BUFFER_LENGTH+1]; 4 | int Comm::_nacts; 5 | char Comm::_commands[MAX_ACTIONS]; 6 | char* (*Comm::_actions[MAX_ACTIONS])(int, char*); 7 | char Comm::_current_cmd; 8 | int Comm::_current_len; 9 | 10 | Comm::Comm(int addr) { 11 | Wire.begin(addr); 12 | Wire.onReceive(_handle_request); 13 | Wire.onRequest(_handle_response); 14 | } 15 | 16 | int Comm::action(char cmd, char* (*func)(int, char*)) { 17 | if (_nacts >= MAX_ACTIONS) 18 | return 1; 19 | 20 | _actions[_nacts] = func; 21 | return 0; 22 | } 23 | 24 | void Comm::_handle_request(int nbytes) { 25 | _current_cmd = Wire.read(); 26 | _current_len = nbytes-1; 27 | for (int i = 0; i < nbytes-1; i++) { 28 | buffer[i] = Wire.read(); 29 | } 30 | } 31 | 32 | void Comm::_handle_response() { 33 | for (int i = 0; i < MAX_ACTIONS; i++) { 34 | if (_commands[i] == _current_cmd) { 35 | _actions[i](_current_len, buffer); 36 | } 37 | } 38 | Wire.write(buffer); 39 | } 40 | -------------------------------------------------------------------------------- /models/bracket.scad: -------------------------------------------------------------------------------- 1 | tube = .775*25.4; 2 | wing = 10; 3 | thick = 5; 4 | 5 | delta = .2; 6 | 7 | $fn=128; 8 | 9 | length = tube+2*wing+2*thick; 10 | 11 | module profile() { 12 | module solids() { 13 | translate([-length/2,0]) square([length, thick]); 14 | translate([-tube/2-thick,0]) square([tube+2*thick, tube/2-delta]); 15 | intersection() { 16 | translate([0,tube/2-delta]) circle(r=tube/2+thick); 17 | translate([-tube/2-thick,0]) square([tube+2*thick, tube+thick-delta]); 18 | } 19 | } 20 | difference() { 21 | solids(); 22 | translate([0,tube/2-delta]) circle(r=tube/2); 23 | translate([-tube/2,-thick-delta]) square([tube, thick+tube/2]); 24 | } 25 | } 26 | 27 | module screw() { 28 | rotate([-90,0]) translate([0,0,-1]) { 29 | cylinder(r=1.8, h=thick+2); 30 | } 31 | } 32 | module bracket() { 33 | screw_pos = tube/2+thick+wing/2; 34 | difference() { 35 | linear_extrude(height=length) profile(); 36 | translate([-screw_pos,0,wing/2]) screw(); 37 | translate([-screw_pos,0,length-wing/2]) screw(); 38 | translate([screw_pos,0,wing/2]) screw(); 39 | translate([screw_pos,0,length-wing/2]) screw(); 40 | } 41 | } 42 | bracket(); 43 | //translate([0,-5]) rotate([90,0]) bracket(); -------------------------------------------------------------------------------- /kiln/static/css/temp_monitor.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 70px; 3 | } 4 | .row-space { 5 | margin-bottom:15px; 6 | } 7 | .output-slider { 8 | width:7% !important; 9 | } 10 | 11 | .temperature { 12 | fill: none; 13 | stroke: steelblue; 14 | stroke-width: 1.5px; 15 | } 16 | 17 | .axis .tick{ 18 | stroke:#DDD; 19 | stroke-width:.5px; 20 | } 21 | .domain{ 22 | display:none; 23 | } 24 | 25 | #graph { 26 | width:100%; 27 | height:500px; 28 | } 29 | 30 | #stop_button_navbar { 31 | margin-left:10px; 32 | margin-right:10px; 33 | } 34 | 35 | #current_temp { 36 | font-weight:bold; 37 | font-size:200%; 38 | color:black; 39 | } 40 | 41 | .profile-pane { 42 | fill:#EEE; 43 | } 44 | .profile-pane-stroke { 45 | fill:none; 46 | stroke:#CCC; 47 | stroke-width:3px; 48 | cursor:ew-resize; 49 | } 50 | .profile-pane-stroke:hover { 51 | stroke-width:15px; 52 | stroke:#333; 53 | } 54 | .profile-line { 55 | stroke:green; 56 | stroke-width:1.5px; 57 | fill:none; 58 | } 59 | .profile-line.dot { 60 | fill:white; 61 | stroke:black; 62 | stroke-width:1px; 63 | } 64 | .profile-line.dot:hover { 65 | stroke-width:3px; 66 | } 67 | 68 | #profile-node-info { 69 | float:left; 70 | position:absolute; 71 | display:none; 72 | width:200px; 73 | } -------------------------------------------------------------------------------- /BOM.md: -------------------------------------------------------------------------------- 1 | | Item | Where | Price | 2 | |-----------|-----------|------:| 3 | | Kiln | Craigslist| $100.00 | 4 | |MR-750 Venturi Burner | [Link](http://www.axner.com/mr-750venturiburner.aspx) (locally purchased)| $43.50| 5 | |Propane regulator|[Link](http://www.ebay.com/itm/331092530323?_trksid=p2060778.m2749.l2649&ssPageName=STRK%3AMEBIDX%3AIT)| $17.66| 6 | |Various connectors| Hardware store | $10.00 | 7 | |K-type kiln thermocouple with block|[Link](http://www.ebay.com/itm/291255615336?_trksid=p2060778.m2749.l2649&ssPageName=STRK%3AMEBIDX%3AIT)| $21.00| 8 | |K-type thermocouple wire|[Link](http://www.amazon.com/gp/product/B00AKWP0BW/ref=oh_aui_detailpage_o03_s00?ie=UTF8&psc=1)| $9.11 | 9 | |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| 10 | |USB Wifi adapter|[Link](http://www.amazon.com/Edimax-EW-7811Un-Adapter-Raspberry-Supports/dp/B003MTTJOY)|$8.70| 11 | |MAX31850K breakout|[Link](https://www.adafruit.com/products/1727)|$14.95| 12 | |Alphanumeric LED with backpack|[Link](https://www.adafruit.com/products/1911)|$9.95| 13 | |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| 14 | |Silicon Nitride igniter|[Link](https://www.sparkfun.com/products/11694)|$19.95| 15 | |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| 16 | |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| 17 | 18 | *Total: $199.81* 19 | 20 | -------------------------------------------------------------------------------- /kiln/breakout.py: -------------------------------------------------------------------------------- 1 | import smbus 2 | import struct 3 | from collections import namedtuple 4 | 5 | Status = namedtuple('Status', 'ignite flame motor main_temp ambient weight aux_temp0 aux_temp1') 6 | 7 | class Breakout(object): 8 | fmt = struct.Struct('; 2 | use ; 3 | use ; 4 | 5 | gsmall = 11; 6 | glarge = 30; 7 | sep = 35+6.1; 8 | 9 | 10 | pitch = (sep * 2 * 180) / (glarge + gsmall); 11 | 12 | module herringbone(teeth, height, pitch=pitch, twist=120, pressure_angle=20) { 13 | translate([0,0,height/2]) { 14 | gear (number_of_teeth=teeth, 15 | circular_pitch=pitch, 16 | pressure_angle=pressure_angle, 17 | clearance = 0.2, 18 | gear_thickness = height/2, 19 | rim_thickness = height/2, 20 | rim_width = 5, 21 | hub_thickness=0, 22 | bore_diameter=0, 23 | twist=twist/teeth); 24 | mirror([0,0,1]) 25 | gear (number_of_teeth=teeth, 26 | circular_pitch=pitch, 27 | pressure_angle=pressure_angle, 28 | clearance = 0.2, 29 | gear_thickness = height/2, 30 | rim_thickness = height/2, 31 | rim_width = 5, 32 | hub_thickness = 0, 33 | bore_diameter=0, 34 | circles=circles, 35 | twist=twist/teeth); 36 | } 37 | } 38 | module bevel_herringbone(teeth, height, pitch=pitch, bevel=0) { 39 | diam = teeth * pitch / 180; 40 | intersection() { 41 | herringbone(teeth, height, pitch=pitch); 42 | cylinder(r2=diam/2-bevel, r1=diam/2+height-bevel, h=height); 43 | cylinder(r1=diam/2-bevel, r2=diam/2+height-bevel, h=height); 44 | } 45 | } 46 | 47 | module gear_small() { 48 | module setscrew() { 49 | translate([0,0,-1]) polyhole(20, 5); 50 | translate([0,0,8+4]) rotate([0,90]) polyhole(10, 4); 51 | translate([2.5,-7.25/2,8]) cube([3.5, 7.25, 10]); 52 | } 53 | difference() { 54 | union() { 55 | mirror([0,0]) bevel_herringbone(gsmall, 8); 56 | translate([0,0,8]) cylinder(r=20/2, h=8); 57 | } 58 | setscrew(); 59 | rotate([0,0,180]) setscrew(); 60 | //translate([10-1, -5,8]) cube([3, 10, 10]); 61 | } 62 | } 63 | 64 | module gear_large() { 65 | diam = glarge * pitch / 180; 66 | 67 | difference() { 68 | bevel_herringbone(glarge, 8); 69 | translate([0,0,-1]) knob(); 70 | } 71 | } 72 | 73 | translate([-gsmall * pitch / 180 / 2,0, 0]) rotate([0,0,360/gsmall*0]) gear_small(); 74 | translate([glarge * pitch / 180 / 2+10,0]) gear_large(); 75 | 76 | -------------------------------------------------------------------------------- /kiln/PID.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger("kiln.PID") 4 | 5 | class PID(object): 6 | """ 7 | Discrete PID control 8 | #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. 9 | #More information: http://en.wikipedia.org/wiki/PID_controller 10 | # 11 | #cnr437@gmail.com 12 | # 13 | ####### Example ######### 14 | # 15 | #p=PID(3.0,0.4,1.2) 16 | #p.setPoint(5.0) 17 | #while True: 18 | # pid = p.update(measurement_value) 19 | # 20 | # 21 | """ 22 | 23 | def __init__(self, P=2.0, I=0.0, D=1.0, Derivator=0, Integrator=0, Integrator_max=2000, Integrator_min=-2000): 24 | 25 | self.Kp=P 26 | self.Ki=I 27 | self.Kd=D 28 | self.Derivator=Derivator 29 | self.Integrator=Integrator 30 | self.Integrator_max=Integrator_max 31 | self.Integrator_min=Integrator_min 32 | 33 | self.set_point=0.0 34 | self.error=0.0 35 | 36 | def update(self,current_value): 37 | """ 38 | Calculate PID output value for given reference input and feedback 39 | """ 40 | 41 | self.error = self.set_point - current_value 42 | 43 | self.P_value = self.Kp * self.error 44 | self.D_value = self.Kd * ( self.error - self.Derivator) 45 | self.Derivator = self.error 46 | 47 | self.Integrator = self.Integrator + self.error 48 | 49 | if self.Integrator > self.Integrator_max: 50 | self.Integrator = self.Integrator_max 51 | elif self.Integrator < self.Integrator_min: 52 | self.Integrator = self.Integrator_min 53 | 54 | self.I_value = self.Integrator * self.Ki 55 | PID = self.P_value + self.I_value + self.D_value 56 | 57 | logger.info("P: %f, I: %f, D:%f, Output:%f"%(self.P_value, self.I_value, self.D_value, PID)) 58 | 59 | return PID 60 | 61 | def setPoint(self,set_point): 62 | """ 63 | Initilize the setpoint of PID 64 | """ 65 | self.set_point = set_point 66 | self.Integrator=0 67 | self.Derivator=0 68 | 69 | def setIntegrator(self, Integrator): 70 | self.Integrator = Integrator 71 | 72 | def setDerivator(self, Derivator): 73 | self.Derivator = Derivator 74 | 75 | def setKp(self,P): 76 | self.Kp=P 77 | 78 | def setKi(self,I): 79 | self.Ki=I 80 | 81 | def setKd(self,D): 82 | self.Kd=D 83 | 84 | def getPoint(self): 85 | return self.set_point 86 | 87 | def getError(self): 88 | return self.error 89 | 90 | def getIntegrator(self): 91 | return self.Integrator 92 | 93 | def getDerivator(self): 94 | return self.Derivator 95 | -------------------------------------------------------------------------------- /kiln/Adafruit_LEDBackpack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import time 4 | from copy import copy 5 | from Adafruit_I2C import Adafruit_I2C 6 | 7 | # ============================================================================ 8 | # LEDBackpack Class 9 | # ============================================================================ 10 | 11 | class LEDBackpack: 12 | i2c = None 13 | 14 | # Registers 15 | __HT16K33_REGISTER_DISPLAY_SETUP = 0x80 16 | __HT16K33_REGISTER_SYSTEM_SETUP = 0x20 17 | __HT16K33_REGISTER_DIMMING = 0xE0 18 | 19 | # Blink rate 20 | __HT16K33_BLINKRATE_OFF = 0x00 21 | __HT16K33_BLINKRATE_2HZ = 0x01 22 | __HT16K33_BLINKRATE_1HZ = 0x02 23 | __HT16K33_BLINKRATE_HALFHZ = 0x03 24 | 25 | # Display buffer (8x16-bits) 26 | __buffer = [0x0000, 0x0000, 0x0000, 0x0000, \ 27 | 0x0000, 0x0000, 0x0000, 0x0000 ] 28 | 29 | # Constructor 30 | def __init__(self, address=0x70, debug=False): 31 | self.i2c = Adafruit_I2C(address) 32 | self.address = address 33 | self.debug = debug 34 | 35 | # Turn the oscillator on 36 | self.i2c.write8(self.__HT16K33_REGISTER_SYSTEM_SETUP | 0x01, 0x00) 37 | 38 | # Turn blink off 39 | self.setBlinkRate(self.__HT16K33_BLINKRATE_OFF) 40 | 41 | # Set maximum brightness 42 | self.setBrightness(15) 43 | 44 | # Clear the screen 45 | self.clear() 46 | 47 | def setBrightness(self, brightness): 48 | "Sets the brightness level from 0..15" 49 | if (brightness > 15): 50 | brightness = 15 51 | self.i2c.write8(self.__HT16K33_REGISTER_DIMMING | brightness, 0x00) 52 | 53 | def setBlinkRate(self, blinkRate): 54 | "Sets the blink rate" 55 | if (blinkRate > self.__HT16K33_BLINKRATE_HALFHZ): 56 | blinkRate = self.__HT16K33_BLINKRATE_OFF 57 | self.i2c.write8(self.__HT16K33_REGISTER_DISPLAY_SETUP | 0x01 | (blinkRate << 1), 0x00) 58 | 59 | def setBufferRow(self, row, value, update=True): 60 | "Updates a single 16-bit entry in the 8*16-bit buffer" 61 | if (row > 7): 62 | return # Prevent buffer overflow 63 | self.__buffer[row] = value # value # & 0xFFFF 64 | if (update): 65 | self.writeDisplay() # Update the display 66 | 67 | def getBufferRow(self, row): 68 | "Returns a single 16-bit entry in the 8*16-bit buffer" 69 | if (row > 7): 70 | return 71 | return self.__buffer[row] 72 | 73 | def getBuffer(self): 74 | "Returns a copy of the raw buffer contents" 75 | bufferCopy = copy(self.__buffer) 76 | return bufferCopy 77 | 78 | def writeDisplay(self): 79 | "Updates the display memory" 80 | bytes = [] 81 | for item in self.__buffer: 82 | bytes.append(item & 0xFF) 83 | bytes.append((item >> 8) & 0xFF) 84 | self.i2c.writeList(0x00, bytes) 85 | 86 | def clear(self, update=True): 87 | "Clears the display memory" 88 | self.__buffer = [ 0, 0, 0, 0, 0, 0, 0, 0 ] 89 | if (update): 90 | self.writeDisplay() 91 | 92 | led = LEDBackpack(0x70) 93 | 94 | -------------------------------------------------------------------------------- /models/regulator.scad: -------------------------------------------------------------------------------- 1 | use ; 2 | use <../lib/28byj.scad>; 3 | 4 | thick = 4; 5 | zip_pos = (35 - 12.5)/3 + 12.5; 6 | 7 | module knob() { 8 | cylinder(r=47.6/2, h=13.33); 9 | for (i=[0:10]) { 10 | rotate([0,0,36*i]) translate([-3.5/2, 20]) 11 | cube([3.5, 1.2+7.6/2, 13.33]); 12 | } 13 | } 14 | 15 | module regulator(knob=false) { 16 | $fn=128; 17 | color("gray") { 18 | translate([0,0,-3.3-1.8]) cylinder(r=67/2, h=2); 19 | translate([0,0,-3.3]) cylinder(r=36, h=6.6); 20 | hull() { 21 | translate([0,0,3.0]) cylinder(r=32.5, h=2.5); 22 | translate([0,0,3.3+2.4]) cylinder(r1=32, r2=12.5, h=8.4); 23 | } 24 | translate([0,0,3.3+2.4+8.4]) cylinder(r=12.5, h=2.4); 25 | //tube 26 | translate([-15.4/2,-35,-10]) cube([15.4, 70, 10]); 27 | } 28 | color("red") { 29 | //knob 30 | if (knob) translate([0,0,3.1+2.4*2+8.4]) knob(); 31 | 32 | } 33 | } 34 | 35 | module zipties() { 36 | translate([0,-zip_pos+3]) rotate([90,0]) rotate([0,0,90]) 37 | ziptie2(40, 65); 38 | translate([0,zip_pos+3]) rotate([90,0]) rotate([0,0,90]) 39 | ziptie2(40, 65); 40 | } 41 | 42 | module holder() { 43 | width = 2*zip_pos+6+2*thick; 44 | 45 | module slot(length) { 46 | hull() { 47 | cylinder(r=1.5, h=6, $fn=16); 48 | translate([length,0]) cylinder(r=1.5, h=6, $fn=16); 49 | } 50 | } 51 | 52 | //translate([18,-width/2, -3.3-thick+16+32-6]) cube([12, 20, 6]); 53 | difference() { 54 | union() { 55 | //main body 56 | intersection() { 57 | translate([0,0,-3.3-thick]) scale([1,1.4]) cylinder(r=36+thick, h=16, $fn=128); 58 | translate([36+thick-20,-width/2, -10]) cube([20, width,20]); 59 | } 60 | 61 | //switch tab 62 | intersection() { 63 | translate([0,0,-3.3-thick]) scale([1, 1.4]) difference() { 64 | cylinder(r=36+10+thick, h=16+34+3, $fn=128); 65 | translate([0,0,16]) cylinder(r=36+10, h=32+3-2,$fn=128); 66 | } 67 | translate([36+thick-15, -width/2, -3.3-thick]) cube([50, 20, 16+32+4]); 68 | } 69 | } 70 | //Screw slots for switch 71 | translate([25,-width/2+10-9.5/2, -3.3-thick+16+32-1]) { 72 | translate([3,0]) slot(10); 73 | translate([3,9.5]) slot(10); 74 | } 75 | 76 | //Slot for wire to ensure no gear tangling 77 | translate([20, -width/2+8.5,-3.3-thick+16+32-3]) cube([50, 3, 3]); 78 | 79 | regulator(); 80 | zipties(); 81 | } 82 | } 83 | module motor_holder() { 84 | difference() { 85 | union() { 86 | //top motor plate 87 | intersection() { 88 | translate([0,0,-3.3-2*thick+16]) scale([1.45,1.2]) 89 | cylinder(r=36+thick, h=thick, $fn=128); 90 | translate([-36-thick-20,-zip_pos-3-thick, -10]) 91 | cube([40, 2*zip_pos+6+2*thick,20]); 92 | } 93 | 94 | intersection() { 95 | translate([0,0,-3.3-thick]) scale([1,1.4]) cylinder(r=36+thick, h=16, $fn=128); 96 | translate([-36-thick-20,-zip_pos-3-thick, -10]) cube([40, 2*zip_pos+6+2*thick,20]); 97 | } 98 | } 99 | regulator(); 100 | zipties(); 101 | 102 | //motor cutout 103 | translate([-35-14.1,0,-10]) { 104 | cylinder(r=14.5, h=20, $fn=64); 105 | translate([-50,-14.5]) cube([50,29,20]); 106 | translate([0,-35/2]) cylinder(r=1.8, h=50); 107 | translate([0, 35/2]) cylinder(r=1.8, h=50); 108 | } 109 | } 110 | } 111 | 112 | //horizontal 113 | //translate([0,0,3.1+thick]) rotate([180,0]) 114 | //vertical 115 | translate([0,0,zip_pos+3+thick]) rotate([90,0]) 116 | { 117 | //translate([20,0,-20]) rotate([0,-90]) 118 | holder(); 119 | //translate([-20,0,-20]) rotate([0,90]) 120 | motor_holder(); 121 | } 122 | 123 | //regulator(true); 124 | //translate([-35-14,0,-8]) rotate([0,0,-90]) stepper28BYJ(); 125 | /* 126 | use ; 127 | %translate([0,0,20]) gear_large(); 128 | %translate([-35-6,0,28]) rotate([180,0]) gear_small(); 129 | //zipties();*/ -------------------------------------------------------------------------------- /models/utils.scad: -------------------------------------------------------------------------------- 1 | use ; 2 | 3 | module nut(flats, h=1) { 4 | cylinder(r=flats/sqrt(3), h=h, $fn=6); 5 | } 6 | 7 | module polyhole(h, d) { 8 | n = max(round(2 * d),3); 9 | rotate([0,0,180]) 10 | cylinder(h = h, r = (d / 2) / cos (180 / n), $fn = n); 11 | } 12 | 13 | module ccube(size=[1,1,1]) { 14 | translate([-size[0]/2,-size[1]/2,0]) cube(size); 15 | } 16 | 17 | module rrect(size=[10,10,1], rad=1, center=false, $fn=36) { 18 | hull() { 19 | translate([rad, rad]) cylinder(r=rad, h=size[2], $fn=$fn); 20 | translate([size[0]-rad,rad]) cylinder(r=rad, h=size[2], $fn=$fn); 21 | translate([rad, size[1]-rad]) cylinder(r=rad, h=size[2], $fn=$fn); 22 | translate([size[0]-rad, size[1]-rad]) cylinder(r=rad, h=size[2], $fn=$fn); 23 | } 24 | } 25 | 26 | module tube(r=10, h=10, thick=2) { 27 | difference() { 28 | cylinder(r=r+thick, h); 29 | translate([0,0,-1]) cylinder(r=r, h=h+2); 30 | } 31 | } 32 | 33 | module ziptie(tube=25.4, zthick=2, zwidth=6) { 34 | difference() { 35 | cylinder(r=tube/2+thick+zthick, h=zwidth); 36 | translate([0,0,-1]) cylinder(r=tube/2+thick, h=zwidth+2); 37 | } 38 | } 39 | module ziptie2(diam, outer, width=6, thick=2) { 40 | scale([1,outer/diam]) difference() { 41 | cylinder(r=diam/2+thick, h=width); 42 | translate([0,0,-1]) cylinder(r=diam/2, h=width+2); 43 | } 44 | } 45 | 46 | module spiral(r1=20, r2=25, thick=2, $fn=36, wedge=[0, 360]) { 47 | inc = 360 / $fn; 48 | for (t=[wedge[0]:inc:wedge[1]-inc]) { 49 | assign( ra = (1 - t/wedge[1]) * r1 + t/wedge[1] * r2, 50 | rb=(1-(t+inc)/wedge[1])*r1 + (t+inc)/wedge[1] * r2) { 51 | polygon([ 52 | [ra*cos(t), ra*sin(t)], 53 | [(ra+thick)*cos(t),(ra+thick)*sin(t)], 54 | [(rb+thick)*cos(t+inc), (rb+thick)*sin(t+inc)], 55 | [rb*cos(t+inc), rb*sin(t+inc)] 56 | ]); 57 | } 58 | } 59 | } 60 | 61 | module frustum(a=[1,1,1,1], h=10) { 62 | polyhedron( 63 | points=[ 64 | [0,0,0],[a[0],0,0],[a[0],a[1],0],[0,a[1],0], 65 | [a[0]/2-a[2]/2,a[1]/2-a[3]/2,h], 66 | [a[0]/2+a[2]/2,a[1]/2-a[3]/2,h], 67 | [a[0]/2+a[2]/2,a[1]/2+a[3]/2,h], 68 | [a[0]/2-a[2]/2,a[1]/2+a[3]/2,h]], 69 | triangles = [ 70 | [0,2,3],[0,1,2],[0,4,5],[0,5,1], 71 | [1,5,6],[1,6,2],[2,6,7],[2,7,3], 72 | [3,7,4],[3,4,0],[4,6,5],[4,7,6] 73 | ] 74 | ); 75 | } 76 | 77 | module eyelet(d=10, h=10, hole=3, thick=2, nut=0, bolt=false) { 78 | difference() { 79 | union() { 80 | translate([-d/2,0]) cube([d, h, thick]); 81 | translate([0, h, 0]) cylinder(r=d/2, h=thick, $fn=72); 82 | } 83 | if (nut > 0) translate([0,h,-.01]) nutHole(nut); 84 | if (bolt) translate([0,h,thick-2]) mirror([0,0,1]) boltHole(hole, length=thick); 85 | else translate([0, h, -1]) polyhole(thick+2, hole); 86 | } 87 | } 88 | 89 | module double_eyelet(d=20, h=20, hole=3, thick=2, rad=5, nut=0, bolt=false) { 90 | translate([0, h-rad,0]) difference() { 91 | hull() { 92 | translate([rad-d/2, 0,0]) cylinder(r=rad, h=thick); 93 | translate([d/2-rad, 0,0]) cylinder(r=rad, h=thick); 94 | translate([-d/2,rad-h,0]) cube([d,1, thick]); 95 | } 96 | if (nut > 0) { 97 | translate([rad-d/2, 0, 0]) nutHole(nut); 98 | translate([d/2-rad, 0, 0]) nutHole(nut); 99 | } 100 | if (bolt) { 101 | translate([rad-d/2, 0, thick-2]) mirror([0,0,1]) boltHole(hole, length=thick); 102 | translate([d/2-rad, 0, thick-2]) mirror([0,0,1]) boltHole(hole, length=thick); 103 | } else { 104 | translate([rad-d/2, 0, -1]) polyhole(thick+2, hole); 105 | translate([d/2-rad, 0, -1]) polyhole(thick+2, hole); 106 | } 107 | } 108 | } 109 | 110 | module tri_equi(leg=10) { 111 | translate([0,offset]) difference() { 112 | translate([-leg/2,0]) square([leg, leg]); 113 | translate([leg/2,0]) rotate([0, 0, 30]) square([2*leg, 2*leg]); 114 | translate([-leg/2,0]) rotate([0, 0, -30]) mirror([1,0]) square([2*leg, 2*leg]); 115 | } 116 | } 117 | 118 | /* 119 | module screw(rad, length, tpmm=2, $fn=16) { 120 | i = 0; 121 | //translate([0,0,i/$fn*tpmm]) rotate([0,0,i*360/$fn]) 122 | linear_extrude(height) 123 | translate([rad,0,0]) child(); 124 | } 125 | 126 | */ 127 | -------------------------------------------------------------------------------- /kiln/thermo.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import random 4 | import datetime 5 | import logging 6 | import threading 7 | from collections import deque, namedtuple 8 | from math import isnan 9 | 10 | logger = logging.getLogger("thermo") 11 | 12 | def temp_to_cone(temp): 13 | """Convert the current temperature to cone value using linear interpolation""" 14 | 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] 15 | names = [str(i).replace('-', '0') for i in range(-22,0)] + [str(i) for i in range(1, 14)] 16 | for i in range(len(cones)-1): 17 | low, high = cones[i], cones[i+1] 18 | if low <= temp < high: 19 | frac = (temp - low) / float(high - low) 20 | return names[i]+'.%d'%int(frac*10) 21 | return "13+" 22 | 23 | tempsample = namedtuple("tempsample", ['time', 'temp']) 24 | 25 | class MAX31850(object): 26 | def __init__(self, name="3b-000000182b57", smooth_window=4): 27 | self.device = "/sys/bus/w1/devices/%s/w1_slave"%name 28 | self.history = deque(maxlen=smooth_window) 29 | self.last = None 30 | 31 | def _read_temp(self): 32 | with open(self.device, 'r') as f: 33 | lines = f.readlines() 34 | 35 | while lines[0].strip()[-3:] != 'YES': 36 | time.sleep(0.2) 37 | with open(self.device, 'r') as f: 38 | lines = f.readlines() 39 | 40 | match = re.match(r'^[0-9a-f\s]{27}t=(\d+)$', lines[1]) 41 | if match is not None: 42 | return float(match.group(1)) / 1000.0 43 | 44 | def get(self): 45 | """Blocking call to retrieve latest temperature sample""" 46 | self.history.append(self._read_temp()) 47 | self.last = time.time() 48 | return self.temperature 49 | 50 | @property 51 | def temperature(self): 52 | if self.last is None or time.time() - self.last > 5: 53 | return self.get() 54 | 55 | return tempsample(self.last, sum(self.history) / float(len(self.history))) 56 | 57 | class Simulate(object): 58 | def __init__(self, regulator, smooth_window=120): 59 | self.regulator = regulator 60 | self.history = deque(maxlen=smooth_window) 61 | self.last = None 62 | 63 | def _read_temp(self): 64 | time.sleep(.25) 65 | return max([self.regulator.output, 0]) * 1000. + 15+random.gauss(0,.2) 66 | 67 | def get(self): 68 | self.history.append(self._read_temp()) 69 | self.last = time.time() 70 | return self.temperature 71 | 72 | @property 73 | def temperature(self): 74 | if self.last is None or time.time() - self.last > 5: 75 | return self.get() 76 | 77 | return tempsample(self.last, sum(self.history) / float(len(self.history))) 78 | 79 | class Breakout(object): 80 | def __init__(self, addr, smooth_window=16): 81 | import breakout 82 | self.device = breakout.Breakout(addr) 83 | self.history = deque(maxlen=smooth_window) 84 | self.last = None 85 | 86 | def get(self): 87 | time.sleep(.25) 88 | temp = self.device.temperature 89 | if not isnan(temp) and len(self.history) < 1 or abs(temp - self.history[-1]) < 15 or (time.time() - self.last) > 5: 90 | self.last = time.time() 91 | self.history.append(temp) 92 | return self.temperature 93 | 94 | @property 95 | def temperature(self): 96 | if self.last is None or time.time() - self.last > 5: 97 | return self.get() 98 | 99 | return tempsample(self.last, sum(self.history) / float(len(self.history))) 100 | 101 | class Monitor(threading.Thread): 102 | def __init__(self, cls=MAX31850, **kwargs): 103 | self.therm = cls(**kwargs) 104 | self.running = True 105 | 106 | from Adafruit_alphanumeric import AlphaScroller 107 | self.display = AlphaScroller(interval=.4) 108 | self.display.start() 109 | self.display.hide() 110 | 111 | def run(self): 112 | while self.running: 113 | _, temp = self.therm.get() 114 | 115 | if temp > 50: 116 | if not self.display.shown: 117 | self.display.show() 118 | fahr = temp * 9. / 5. + 32. 119 | text = list('%0.0f'%temp) + ['degree'] + list('C %0.0f'%fahr)+['degree'] + list("F") 120 | if 600 <= temp: 121 | text += [' ', ' ', 'cone']+list(temp_to_cone(temp)) 122 | self.display.set_text(text, reset=False) 123 | elif self.display.shown: 124 | self.display.hide() 125 | 126 | def stop(self): 127 | self.running = False 128 | self.display.stop() 129 | 130 | 131 | if __name__ == "__main__": 132 | monitor = Monitor() 133 | monitor.start() 134 | -------------------------------------------------------------------------------- /kiln/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import re 6 | import time 7 | import json 8 | import traceback 9 | import inspect 10 | 11 | import tornado.ioloop 12 | import tornado.web 13 | from tornado import websocket 14 | 15 | import paths 16 | 17 | cone_symbol = re.compile(r'\^([0-9]{1,3})') 18 | 19 | class ClientSocket(websocket.WebSocketHandler): 20 | def initialize(self, parent): 21 | self.parent = parent 22 | 23 | def open(self): 24 | self.parent.clients.append(self) 25 | 26 | def on_close(self): 27 | self.parent.clients.remove(self) 28 | 29 | class ManagerHandler(tornado.web.RequestHandler): 30 | def initialize(self, manager): 31 | self.manager = manager 32 | 33 | class MainHandler(ManagerHandler): 34 | def get(self): 35 | files = os.listdir(paths.profile_path) 36 | fixname = lambda x: cone_symbol.sub(r'Δ\1', os.path.splitext(x)[0].replace("_", " ")) 37 | profiles = dict((fname, fixname(fname)) for fname in files) 38 | 39 | return self.render(os.path.join(paths.html_templates, "main.html"), 40 | state=self.manager.state.__class__.__name__, 41 | state_data=json.dumps(self.manager.state.status), 42 | profiles=profiles, 43 | ) 44 | 45 | class DataRequest(ManagerHandler): 46 | def get(self): 47 | self.set_header("Content-Type", "application/json") 48 | data = list(self.manager.history) 49 | output = [dict(time=ts.time, temp=ts.temp) for ts in data] 50 | self.write(json.dumps(output)) 51 | 52 | class ProfileHandler(tornado.web.RequestHandler): 53 | def get(self, name): 54 | try: 55 | with open(os.path.join(paths.profile_path, name)) as fp: 56 | self.write(fp.read()) 57 | except IOError: 58 | self.write_error(404) 59 | 60 | def post(self, name): 61 | try: 62 | schedule = json.loads(self.get_argument("schedule")) 63 | fname = os.path.join(paths.profile_path, name) 64 | with open(fname, 'w') as fp: 65 | json.dump(schedule, fp) 66 | self.write(dict(type="success")) 67 | except IOError: 68 | self.write_error(404) 69 | except Exception as e: 70 | self.write(dict(type="error", error=repr(e), msg=traceback.format_exc())) 71 | 72 | class DoAction(ManagerHandler): 73 | def _run(self, name, argfunc): 74 | func = getattr(self.manager.state, name) 75 | #Introspect the function, get the arguments 76 | args, varargs, keywords, defaults = inspect.getargspec(func) 77 | 78 | kwargs = dict() 79 | if defaults is not None: 80 | #keyword arguments 81 | for arg, d in zip(args[-len(defaults):], defaults): 82 | kwargs[arg] = argfunc(arg, default=d) 83 | end = len(defaults) 84 | else: 85 | end = len(args) 86 | 87 | #required arguments 88 | for arg in args[1:end]: 89 | kwargs[arg] = argfunc(arg) 90 | 91 | realfunc = getattr(self.manager, name) 92 | realfunc(**kwargs) 93 | 94 | def get(self, action): 95 | try: 96 | self._run(action, self.get_query_argument) 97 | self.write(json.dumps(dict(type="success"))) 98 | except Exception as e: 99 | self.write(json.dumps(dict(type="error", error=repr(e), msg=traceback.format_exc()))) 100 | 101 | def post(self, action): 102 | try: 103 | self._run(action, self.get_argument) 104 | self.write(json.dumps(dict(type="success"))) 105 | except Exception as e: 106 | self.write(json.dumps(dict(type="error", error=repr(e), msg=traceback.format_exc()))) 107 | 108 | class WebApp(object): 109 | def __init__(self, manager, port=8888): 110 | self.handlers = [ 111 | (r"^/$", MainHandler, dict(manager=manager)), 112 | (r"^/ws/?$", ClientSocket, dict(parent=self)), 113 | (r"^/temperature.json$", DataRequest, dict(manager=manager)), 114 | (r"^/do/(.*)/?$", DoAction, dict(manager=manager)), 115 | (r"^/profile/?(.*)$", ProfileHandler), 116 | (r"^/(.*)$", tornado.web.StaticFileHandler, dict(path=paths.html_static)), 117 | ] 118 | self.clients = [] 119 | self.port = port 120 | 121 | def send(self, data): 122 | jsondat = json.dumps(data) 123 | for sock in self.clients: 124 | sock.write_message(jsondat) 125 | 126 | def run(self): 127 | self.app = tornado.web.Application(self.handlers, gzip=True) 128 | self.app.listen(8888) 129 | tornado.ioloop.IOLoop.instance().start() 130 | 131 | if __name__ == "__main__": 132 | try: 133 | import manager 134 | kiln = manager.Manager(simulate=False) 135 | app = WebApp(kiln) 136 | kiln._send = app.send 137 | 138 | app.run() 139 | except KeyboardInterrupt: 140 | kiln.manager_stop() 141 | -------------------------------------------------------------------------------- /kiln/states.py: -------------------------------------------------------------------------------- 1 | """Based on the pattern provided here: 2 | http://python-3-patterns-idioms-test.readthedocs.org/en/latest/StateMachine.html 3 | """ 4 | import json 5 | import time 6 | import traceback 7 | import manager 8 | from collections import deque 9 | 10 | class State(object): 11 | def __init__(self, manager): 12 | self.parent = manager 13 | 14 | @property 15 | def status(self): 16 | return dict() 17 | 18 | def run(self): 19 | """Action that must be continuously run while in this state""" 20 | ts = self.parent.therm.get() 21 | self.history.append(ts) 22 | return dict(type="temperature", time=ts.time, temp=ts.temp, output=self.parent.regulator.output) 23 | 24 | class Idle(State): 25 | def __init__(self, manager): 26 | super(Idle, self).__init__(manager) 27 | self.history = deque(maxlen=2400) #about 10 minutes worth, 4 samples / sec * 60 sec / min * 10 min 28 | 29 | def ignite(self): 30 | _ignite(self.parent.regulator, self.parent.notify) 31 | return Lit, dict(history=self.history) 32 | 33 | def start_profile(self, schedule, start_time=None, interval=5): 34 | _ignite(self.parent.regulator, self.parent.notify) 35 | kwargs = dict(history=self.history, 36 | schedule=json.loads(schedule), 37 | start_time=float(start_time), 38 | interval=float(interval) 39 | ) 40 | return Running, kwargs 41 | 42 | class Lit(State): 43 | def __init__(self, parent, history): 44 | super(Lit, self).__init__(parent) 45 | self.history = manager.TempLog(history) 46 | 47 | def set(self, value): 48 | try: 49 | self.parent.regulator.set(float(value)) 50 | return dict(type="success") 51 | except: 52 | return dict(type="error", msg=traceback.format_exc()) 53 | 54 | def start_profile(self, schedule, start_time=None, interval=5): 55 | kwargs = dict(history=self.history, 56 | schedule=json.loads(schedule), 57 | start_time=float(start_time), 58 | interval=float(interval) 59 | ) 60 | return Running, kwargs 61 | 62 | def shutoff(self): 63 | _shutoff(self.parent.regulator, self.parent.notify) 64 | return Cooling, dict(history=self.history) 65 | 66 | class Cooling(State): 67 | def __init__(self, parent, history): 68 | super(Cooling, self).__init__(parent) 69 | self.history = history 70 | 71 | def run(self): 72 | ts = self.parent.therm.get() 73 | self.history.append(ts) 74 | if ts.temp < 50: 75 | # Direction logged by TempLog 76 | # fname = time.strftime('%Y-%m-%d_%I:%M%P.log') 77 | # with open(os.path.join(paths.log_path, fname), 'w') as fp: 78 | # for time, temp in self.history: 79 | # fp.write("%s\t%s\n"%time, temp) 80 | return Idle 81 | return dict(type="temperature", time=ts.time, temp=ts.temp) 82 | 83 | def ignite(self): 84 | _ignite(self.parent.regulator, self.parent.notify) 85 | return Lit, dict(history=self.history) 86 | 87 | def start_profile(self, schedule, start_time=None, interval=5): 88 | _ignite(self.parent.regulator, self.parent.notify) 89 | kwargs = dict(history=self.history, 90 | schedule=json.loads(schedule), 91 | start_time=float(start_time), 92 | interval=float(interval) 93 | ) 94 | return Running, kwargs 95 | 96 | class Running(State): 97 | def __init__(self, parent, history, start_time=None, **kwargs): 98 | super(Running, self).__init__(parent) 99 | self.profile = manager.Profile(therm=self.parent.therm, regulator=self.parent.regulator, 100 | callback=self._notify, start_time=start_time, **kwargs) 101 | self.start_time = self.profile.start_time 102 | self.history = history 103 | 104 | @property 105 | def status(self): 106 | return dict(start_time=self.start_time, schedule=self.profile.schedule) 107 | 108 | def _notify(self, therm, setpoint, out): 109 | self.parent.notify(dict( 110 | type="profile", 111 | temp=therm, 112 | setpoint=setpoint, 113 | output=out, 114 | ts=self.profile.elapsed, 115 | )) 116 | 117 | def run(self): 118 | if self.profile.completed: 119 | #self.parent.notify(dict(type="profile",status="complete")) 120 | print "Profile complete!" 121 | _shutoff(self.parent.regulator, self.parent.notify) 122 | return Cooling, dict(history=self.history) 123 | 124 | return super(Running, self).run() 125 | 126 | def pause(self): 127 | self.profile.stop() 128 | return Lit, dict(history=self.history) 129 | 130 | def stop_profile(self): 131 | self.profile.stop() 132 | _shutoff(self.parent.regulator, self.parent.notify) 133 | return Cooling, dict(history=self.history) 134 | 135 | def shutoff(self): 136 | return self.stop_profile() 137 | 138 | def _ignite(regulator, notify): 139 | try: 140 | regulator.ignite() 141 | msg = dict(type="success") 142 | except ValueError: 143 | msg = dict(type="error", msg="Cannot ignite: regulator not off") 144 | except Exception as e: 145 | msg = dict(type="error", error=repr(e), msg=traceback.format_exc()) 146 | notify(msg) 147 | 148 | def _shutoff(regulator, notify): 149 | try: 150 | regulator.off() 151 | msg = dict(type="success") 152 | except ValueError: 153 | msg = dict(type="error", msg="Cannot shutoff: regulator not lit") 154 | except Exception as e: 155 | msg = dict(type="error", error=repr(e), msg=traceback.format_exc()) 156 | notify(msg) -------------------------------------------------------------------------------- /kiln/Adafruit_I2C.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import smbus 4 | 5 | # =========================================================================== 6 | # Adafruit_I2C Class 7 | # =========================================================================== 8 | 9 | class Adafruit_I2C(object): 10 | 11 | @staticmethod 12 | def getPiRevision(): 13 | "Gets the version number of the Raspberry Pi board" 14 | # Courtesy quick2wire-python-api 15 | # https://github.com/quick2wire/quick2wire-python-api 16 | # Updated revision info from: http://elinux.org/RPi_HardwareHistory#Board_Revision_History 17 | try: 18 | with open('/proc/cpuinfo','r') as f: 19 | for line in f: 20 | if line.startswith('Revision'): 21 | return 1 if line.rstrip()[-1] in ['2','3'] else 2 22 | except: 23 | return 0 24 | 25 | @staticmethod 26 | def getPiI2CBusNumber(): 27 | # Gets the I2C bus number /dev/i2c# 28 | return 1 if Adafruit_I2C.getPiRevision() > 1 else 0 29 | 30 | def __init__(self, address, busnum=-1, debug=False): 31 | self.address = address 32 | # By default, the correct I2C bus is auto-detected using /proc/cpuinfo 33 | # Alternatively, you can hard-code the bus version below: 34 | # self.bus = smbus.SMBus(0); # Force I2C0 (early 256MB Pi's) 35 | # self.bus = smbus.SMBus(1); # Force I2C1 (512MB Pi's) 36 | self.bus = smbus.SMBus(busnum if busnum >= 0 else Adafruit_I2C.getPiI2CBusNumber()) 37 | self.debug = debug 38 | 39 | def reverseByteOrder(self, data): 40 | "Reverses the byte order of an int (16-bit) or long (32-bit) value" 41 | # Courtesy Vishal Sapre 42 | byteCount = len(hex(data)[2:].replace('L','')[::2]) 43 | val = 0 44 | for i in range(byteCount): 45 | val = (val << 8) | (data & 0xff) 46 | data >>= 8 47 | return val 48 | 49 | def errMsg(self): 50 | print "Error accessing 0x%02X: Check your I2C address" % self.address 51 | return -1 52 | 53 | def write8(self, reg, value): 54 | "Writes an 8-bit value to the specified register/address" 55 | try: 56 | self.bus.write_byte_data(self.address, reg, value) 57 | if self.debug: 58 | print "I2C: Wrote 0x%02X to register 0x%02X" % (value, reg) 59 | except IOError, err: 60 | return self.errMsg() 61 | 62 | def write16(self, reg, value): 63 | "Writes a 16-bit value to the specified register/address pair" 64 | try: 65 | self.bus.write_word_data(self.address, reg, value) 66 | if self.debug: 67 | print ("I2C: Wrote 0x%02X to register pair 0x%02X,0x%02X" % 68 | (value, reg, reg+1)) 69 | except IOError, err: 70 | return self.errMsg() 71 | 72 | def writeRaw8(self, value): 73 | "Writes an 8-bit value on the bus" 74 | try: 75 | self.bus.write_byte(self.address, value) 76 | if self.debug: 77 | print "I2C: Wrote 0x%02X" % value 78 | except IOError, err: 79 | return self.errMsg() 80 | 81 | def writeList(self, reg, list): 82 | "Writes an array of bytes using I2C format" 83 | try: 84 | if self.debug: 85 | print "I2C: Writing list to register 0x%02X:" % reg 86 | print list 87 | self.bus.write_i2c_block_data(self.address, reg, list) 88 | except IOError, err: 89 | return self.errMsg() 90 | 91 | def readList(self, reg, length): 92 | "Read a list of bytes from the I2C device" 93 | try: 94 | results = self.bus.read_i2c_block_data(self.address, reg, length) 95 | if self.debug: 96 | print ("I2C: Device 0x%02X returned the following from reg 0x%02X" % 97 | (self.address, reg)) 98 | print results 99 | return results 100 | except IOError, err: 101 | return self.errMsg() 102 | 103 | def readU8(self, reg): 104 | "Read an unsigned byte from the I2C device" 105 | try: 106 | result = self.bus.read_byte_data(self.address, reg) 107 | if self.debug: 108 | print ("I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % 109 | (self.address, result & 0xFF, reg)) 110 | return result 111 | except IOError, err: 112 | return self.errMsg() 113 | 114 | def readS8(self, reg): 115 | "Reads a signed byte from the I2C device" 116 | try: 117 | result = self.bus.read_byte_data(self.address, reg) 118 | if result > 127: result -= 256 119 | if self.debug: 120 | print ("I2C: Device 0x%02X returned 0x%02X from reg 0x%02X" % 121 | (self.address, result & 0xFF, reg)) 122 | return result 123 | except IOError, err: 124 | return self.errMsg() 125 | 126 | def readU16(self, reg, little_endian=True): 127 | "Reads an unsigned 16-bit value from the I2C device" 128 | try: 129 | result = self.bus.read_word_data(self.address,reg) 130 | # Swap bytes if using big endian because read_word_data assumes little 131 | # endian on ARM (little endian) systems. 132 | if not little_endian: 133 | result = ((result << 8) & 0xFF00) + (result >> 8) 134 | if (self.debug): 135 | print "I2C: Device 0x%02X returned 0x%04X from reg 0x%02X" % (self.address, result & 0xFFFF, reg) 136 | return result 137 | except IOError, err: 138 | return self.errMsg() 139 | 140 | def readS16(self, reg, little_endian=True): 141 | "Reads a signed 16-bit value from the I2C device" 142 | try: 143 | result = self.readU16(reg,little_endian) 144 | if result > 32767: result -= 65536 145 | return result 146 | except IOError, err: 147 | return self.errMsg() 148 | 149 | if __name__ == '__main__': 150 | try: 151 | bus = Adafruit_I2C(address=0) 152 | print "Default I2C bus is accessible" 153 | except: 154 | print "Error accessing default I2C bus" 155 | -------------------------------------------------------------------------------- /kiln/manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stepper 3 | import time 4 | import random 5 | import thermo 6 | import threading 7 | import traceback 8 | import logging 9 | 10 | import states 11 | import PID 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.setLevel(logging.INFO) 15 | 16 | class TempLog(object): 17 | def __init__(self, history, interval=60, suffix=""): #save data every 60 seconds 18 | import paths 19 | self.history = history 20 | fname = time.strftime('%Y-%m-%d_%I:%M%P') 21 | if len(suffix) > 0: 22 | suffix = "_"+suffix 23 | self.fname = os.path.join(paths.log_path, fname+suffix+".log") 24 | with open(self.fname, 'w') as fp: 25 | fp.write("time\ttemp\n") 26 | for t, temp in history: 27 | fp.write("%f\t%f\n"%(t, temp)) 28 | self.next = time.time() + interval 29 | self.interval = interval 30 | self._buffer = [] 31 | 32 | def __iter__(self): 33 | return iter(self.history) 34 | 35 | def append(self, data): 36 | self.history.append(data) 37 | self._buffer.append(data) 38 | if time.time() > self.next: 39 | with open(self.fname, 'a') as fp: 40 | for t, temp in self._buffer: 41 | fp.write("%f\t%f\n"%(t, temp)) 42 | self._buffer = [] 43 | self.next = time.time() + self.interval 44 | 45 | class Manager(threading.Thread): 46 | def __init__(self, start=states.Idle, simulate=False): 47 | """ 48 | Implement a state machine that cycles through States 49 | """ 50 | super(Manager, self).__init__() 51 | self._send = None 52 | 53 | if simulate: 54 | self.regulator = stepper.Regulator(simulate=simulate) 55 | self.therm = thermo.Simulate(regulator=self.regulator) 56 | else: 57 | self.regulator = stepper.Breakout(0x08) 58 | self.therm = thermo.Breakout(0x08) 59 | 60 | self.state = start(self) 61 | self.state_change = threading.Event() 62 | 63 | self.running = True 64 | self.start() 65 | 66 | def notify(self, data): 67 | if self._send is not None: 68 | try: 69 | self._send(data) 70 | except: 71 | pass 72 | else: 73 | logger.info("No notifier set, ignoring message: %s"%data) 74 | 75 | def __getattr__(self, name): 76 | """Mutates the manager to return State actions 77 | If the requested attribute is a function, wrap the function 78 | such that returned obejcts which are States indicate a state change 79 | """ 80 | attr = getattr(self.state, name) 81 | if hasattr(attr, "__call__"): 82 | def func(*args, **kwargs): 83 | self._change_state(attr(*args, **kwargs)) 84 | return func 85 | 86 | return attr 87 | 88 | def _change_state(self, output): 89 | if isinstance(output, type) and issubclass(output, states.State) : 90 | self.state = output(self) 91 | self.state_change.set() 92 | self.notify(dict(type="state", state=output.__name__)) 93 | logger.info("Switching to state '%s'"%output.__name__) 94 | elif isinstance(output, tuple) and issubclass(output[0], states.State): 95 | newstate, kwargs = output 96 | self.state = newstate(self, **kwargs) 97 | self.notify(dict(type="state", state=newstate.__name__)) 98 | logger.info("Switching to state '%s'"%newstate.__name__) 99 | elif isinstance(output, dict) and "type" in output: 100 | self.notify(output) 101 | elif output is not None: 102 | logger.warn("Unknown state output: %r"%output) 103 | 104 | def run(self): 105 | while self.running: 106 | self._change_state(self.state.run()) 107 | 108 | def manager_stop(self): 109 | self.running = False 110 | self.state_change.set() 111 | 112 | class Profile(threading.Thread): 113 | """Performs the PID loop required for feedback control""" 114 | def __init__(self, schedule, therm, regulator, interval=1, start_time=None, callback=None, 115 | Kp=.03, Ki=.015, Kd=.001): 116 | super(Profile, self).__init__() 117 | self.daemon = True 118 | 119 | self.schedule = schedule 120 | self.therm = therm 121 | self.regulator = regulator 122 | self.interval = interval 123 | self.start_time = start_time 124 | if start_time is None: 125 | self.start_time = time.time() 126 | 127 | self.pid = PID.PID(Kp, Ki, Kd) 128 | self.callback = callback 129 | self.running = True 130 | self.duty_cycle = False 131 | self.start() 132 | 133 | @property 134 | def elapsed(self): 135 | ''' Returns the elapsed time from start in seconds''' 136 | return time.time() - self.start_time 137 | 138 | @property 139 | def completed(self): 140 | return self.elapsed > self.schedule[-1][0] 141 | 142 | def stop(self): 143 | self.running = False 144 | 145 | def run(self): 146 | _next = time.time()+self.interval 147 | while not self.completed and self.running: 148 | ts = self.elapsed 149 | #find epoch 150 | for i in range(len(self.schedule)-1): 151 | if self.schedule[i][0] < ts < self.schedule[i+1][0]: 152 | time0, temp0 = self.schedule[i] 153 | time1, temp1 = self.schedule[i+1] 154 | frac = (ts - time0) / (time1 - time0) 155 | setpoint = frac * (temp1 - temp0) + temp0 156 | self.pid.setPoint(setpoint) 157 | 158 | temp = self.therm.temperature.temp 159 | if temp == -1: 160 | continue #skip invalid temperature readings 161 | elif temp - setpoint > 10: 162 | self.regulator.off() 163 | self.duty_cycle = True 164 | pid_out = -1 165 | elif self.duty_cycle: 166 | if temp - setpoint < -5: 167 | self.regulator.ignite() 168 | self.duty_cycle = False 169 | pid_out = -1 170 | else: 171 | pid_out = self.pid.update(temp) 172 | if pid_out < 0: pid_out = 0 173 | if pid_out > 1: pid_out = 1 174 | self.regulator.set(pid_out) 175 | 176 | if self.callback is not None: 177 | self.callback(temp, setpoint, pid_out) 178 | 179 | sleep = _next - time.time() 180 | if sleep > 0: 181 | time.sleep(sleep) 182 | _next += self.interval 183 | -------------------------------------------------------------------------------- /firmware/controller/controller.ino: -------------------------------------------------------------------------------- 1 | #define PIN_IGNITE 10 2 | #define PIN_STEP1 9 3 | #define PIN_STEP2 8 4 | #define PIN_STEP3 7 5 | #define PIN_STEP4 6 6 | #define PIN_AUXTEMP 2 7 | #define PIN_TEMP_CS 4 8 | #define PIN_LOADCELL A3 9 | #define PIN_FLAME_A A2 10 | #define PIN_FLAME_D 1 11 | #define PIN_REGLIMIT 5 12 | 13 | #define STEP_SPEED 275//in steps per second 14 | #define TEMP_UPDATE 250 //milliseconds 15 | #define AUX_UPDATE 1000 //milliseconds 16 | #define MOTOR_TIMEOUT 60000 //milliseconds 17 | 18 | #define NUM_AUXTEMP 2 19 | 20 | #define NO_PORTB_PINCHANGES 21 | #define NO_PORTC_PINCHANGES 22 | #define DISABLE_PCINT_MULTI_SERVICE 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include "pushbutton.h" 32 | 33 | struct Status { 34 | unsigned char ignite; 35 | unsigned char flame; 36 | unsigned int motor; 37 | float main_temp; 38 | float ambient; 39 | float weight; 40 | float aux_temp[NUM_AUXTEMP]; 41 | } status; 42 | uint8_t* status_data = (uint8_t*) &status; 43 | 44 | const float step_interval = 1. / STEP_SPEED * 1000.; //milliseconds 45 | 46 | //intermediate variables 47 | Adafruit_MAX31855 thermo(PIN_TEMP_CS); 48 | Stepper stepper(2048, PIN_STEP4, PIN_STEP2, PIN_STEP3, PIN_STEP1); 49 | pushbutton reglimit = pushbutton(PIN_REGLIMIT, 5); 50 | OneWire oneWire(PIN_AUXTEMP); 51 | DallasTemperature sensors(&oneWire); 52 | DeviceAddress aux_addr[NUM_AUXTEMP]; 53 | 54 | char i2c_command; 55 | float next_step; 56 | unsigned long next_temp; 57 | unsigned long next_aux = 0; 58 | unsigned char motor_active = false; 59 | unsigned long stepper_target = 0; 60 | unsigned long num_aux = 0; 61 | int n_clicks = 0; //Number of full rotations 62 | boolean limit_state = false; 63 | unsigned long limit_last; 64 | 65 | void setup() { 66 | //setup ignition mosfet 67 | pinMode(PIN_IGNITE, OUTPUT); 68 | digitalWrite(PIN_IGNITE, LOW); 69 | status.ignite = 0.; 70 | 71 | status.flame = false; 72 | status.weight = 0.; 73 | status.aux_temp[0] = -1.; 74 | status.aux_temp[1] = -1.; 75 | sensors.begin(); 76 | 77 | //Setup I2C 78 | Wire.begin(0x08); 79 | Wire.onRequest(i2c_update); 80 | Wire.onReceive(i2c_action); 81 | 82 | //Set up regulator stepper 83 | status.motor = 0; 84 | 85 | //set initial thermocouple temperature 86 | delay(500); 87 | update_temp(); 88 | next_temp = millis() + TEMP_UPDATE; 89 | 90 | //Setup auxtemp ds18b20 sensors 91 | num_aux = sensors.getDeviceCount(); 92 | num_aux = NUM_AUXTEMP < num_aux ? NUM_AUXTEMP : num_aux; 93 | for (int i = 0; i < num_aux; i++) { 94 | sensors.getAddress(aux_addr[i], i); 95 | sensors.setResolution(aux_addr[i], 12); 96 | status.aux_temp[i] = 0.; 97 | } 98 | if (num_aux > 0) { 99 | sensors.setWaitForConversion(false); 100 | sensors.requestTemperatures(); 101 | next_aux = millis() + AUX_UPDATE; 102 | } 103 | } 104 | 105 | int dir; 106 | unsigned long now; 107 | void loop() { 108 | now = millis(); 109 | reglimit.update(); 110 | status.aux_temp[1] = reglimit.n_clicks; 111 | 112 | if (stepper_target != status.motor && now > next_step) { 113 | dir = status.motor < stepper_target ? 1 : -1; 114 | stepper.step(dir); 115 | 116 | //Limit switch tripped 117 | if (stepper_target == 0) { 118 | if (reglimit.n_clicks == 0) 119 | status.motor = 0; 120 | } else { 121 | status.motor += dir; 122 | } 123 | 124 | next_step += step_interval; 125 | } 126 | 127 | //put motor to sleep after timeout 128 | if (motor_active && (now - next_step) > MOTOR_TIMEOUT) { 129 | digitalWrite(PIN_STEP1, LOW); 130 | digitalWrite(PIN_STEP2, LOW); 131 | digitalWrite(PIN_STEP3, LOW); 132 | digitalWrite(PIN_STEP4, LOW); 133 | motor_active = false; 134 | } 135 | 136 | //update temperature 137 | if (now > next_temp) { 138 | update_temp(); 139 | next_temp += TEMP_UPDATE; 140 | } 141 | 142 | //update auxtemp 143 | if (num_aux > 0 && now > next_aux) { 144 | update_aux(); 145 | next_aux += AUX_UPDATE; 146 | } 147 | 148 | //check flame status 149 | } 150 | 151 | void set_regulator(unsigned long pos) { 152 | motor_active = true; 153 | reglimit.setDir(status.motor < pos ? 1 : -1); 154 | if (stepper_target == status.motor) 155 | next_step = millis(); //Start stepping immediately 156 | stepper_target = pos; 157 | } 158 | 159 | void update_temp() { 160 | thermo.readAll(status.main_temp, status.ambient); 161 | } 162 | 163 | void update_aux() { 164 | for (int i = 0; i < num_aux; i++) { 165 | status.aux_temp[i] = sensors.getTempC(aux_addr[i]); 166 | } 167 | sensors.requestTemperatures(); 168 | } 169 | 170 | void i2c_update() { 171 | if (i2c_command == 'M') { 172 | Wire.write((byte*) &(status.motor), 4); 173 | } else if (i2c_command == 'I') { 174 | Wire.write((byte*) &(status.ignite), 1); 175 | } else if (i2c_command == 'T') { 176 | Wire.write((byte*) &(status.main_temp), 4); 177 | } else if (i2c_command == 'F') { 178 | Wire.write((byte*) &(status.flame), 1); 179 | } else { 180 | Wire.write(status_data, sizeof(struct Status)); 181 | } 182 | 183 | i2c_command = 0; 184 | } 185 | 186 | byte buffer[32]; 187 | void i2c_action(int nbytes) { 188 | i2c_command = Wire.read(); 189 | 190 | int i = 0; 191 | while (Wire.available()) { 192 | buffer[i++] = Wire.read(); 193 | } 194 | 195 | if (nbytes == 1) { 196 | return; //Command already stored, no arguments 197 | } 198 | 199 | switch (i2c_command) { 200 | case 'M': 201 | set_regulator(*((unsigned int*) buffer)); 202 | break; 203 | case 'I': 204 | analogWrite(PIN_IGNITE, buffer[0]); 205 | status.ignite = buffer[0]; 206 | break; 207 | } 208 | 209 | i2c_command = 0; 210 | } 211 | -------------------------------------------------------------------------------- /kiln/static/js/juration.js: -------------------------------------------------------------------------------- 1 | /* 2 | * juration - a natural language duration parser 3 | * https://github.com/domchristie/juration 4 | * 5 | * Copyright 2011, Dom Christie 6 | * Licenced under the MIT licence 7 | * 8 | */ 9 | 10 | (function() { 11 | 12 | var UNITS = { 13 | seconds: { 14 | patterns: ['second', 'sec', 's'], 15 | value: 1, 16 | formats: { 17 | 'chrono': '', 18 | 'micro': 's', 19 | 'short': 'sec', 20 | 'long': 'second' 21 | } 22 | }, 23 | minutes: { 24 | patterns: ['minute', 'min', 'm(?!s)'], 25 | value: 60, 26 | formats: { 27 | 'chrono': ':', 28 | 'micro': 'm', 29 | 'short': 'min', 30 | 'long': 'minute' 31 | } 32 | }, 33 | hours: { 34 | patterns: ['hour', 'hr', 'h'], 35 | value: 3600, 36 | formats: { 37 | 'chrono': ':', 38 | 'micro': 'h', 39 | 'short': 'hr', 40 | 'long': 'hour' 41 | } 42 | }, 43 | days: { 44 | patterns: ['day', 'dy', 'd'], 45 | value: 86400, 46 | formats: { 47 | 'chrono': ':', 48 | 'micro': 'd', 49 | 'short': 'day', 50 | 'long': 'day' 51 | } 52 | }, 53 | weeks: { 54 | patterns: ['week', 'wk', 'w'], 55 | value: 604800, 56 | formats: { 57 | 'chrono': ':', 58 | 'micro': 'w', 59 | 'short': 'wk', 60 | 'long': 'week' 61 | } 62 | }, 63 | months: { 64 | patterns: ['month', 'mon', 'mo', 'mth'], 65 | value: 2628000, 66 | formats: { 67 | 'chrono': ':', 68 | 'micro': 'm', 69 | 'short': 'mth', 70 | 'long': 'month' 71 | } 72 | }, 73 | years: { 74 | patterns: ['year', 'yr', 'y'], 75 | value: 31536000, 76 | formats: { 77 | 'chrono': ':', 78 | 'micro': 'y', 79 | 'short': 'yr', 80 | 'long': 'year' 81 | } 82 | } 83 | }; 84 | 85 | var stringify = function(seconds, options) { 86 | 87 | if(!_isNumeric(seconds)) { 88 | throw "juration.stringify(): Unable to stringify a non-numeric value"; 89 | } 90 | 91 | if((typeof options === 'object' && options.format !== undefined) && (options.format !== 'micro' && options.format !== 'short' && options.format !== 'long' && options.format !== 'chrono')) { 92 | throw "juration.stringify(): format cannot be '" + options.format + "', and must be either 'micro', 'short', or 'long'"; 93 | } 94 | 95 | var defaults = { 96 | format: 'short' 97 | }; 98 | 99 | var opts = _extend(defaults, options); 100 | 101 | var units = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'], values = []; 102 | var remaining = seconds; 103 | for(var i = 0, len = units.length; i < len; i++) { 104 | var unit = UNITS[units[i]]; 105 | values[i] = Math.floor(remaining / unit.value); 106 | 107 | if(opts.format === 'micro' || opts.format === 'chrono') { 108 | values[i] += unit.formats[opts.format]; 109 | } 110 | else { 111 | values[i] += ' ' + _pluralize(values[i], unit.formats[opts.format]); 112 | } 113 | remaining = remaining % unit.value; 114 | } 115 | var output = ''; 116 | for(i = 0, len = values.length; i < len; i++) { 117 | if(values[i].charAt(0) !== "0" && opts.format != 'chrono') { 118 | output += values[i] + ' '; 119 | } 120 | else if (opts.format == 'chrono') { 121 | output += _padLeft(values[i]+'', '0', i==values.length-1 ? 2 : 3); 122 | } 123 | } 124 | return output.replace(/\s+$/, '').replace(/^(00:)+/g, '').replace(/^0/, ''); 125 | }; 126 | 127 | var parse = function(string) { 128 | 129 | // returns calculated values separated by spaces 130 | for(var unit in UNITS) { 131 | for(var i = 0, mLen = UNITS[unit].patterns.length; i < mLen; i++) { 132 | var regex = new RegExp("((?:\\d+\\.\\d+)|\\d+)\\s?(" + UNITS[unit].patterns[i] + "s?(?=\\s|\\d|\\b))", 'gi'); 133 | string = string.replace(regex, function(str, p1, p2) { 134 | return " " + (p1 * UNITS[unit].value).toString() + " "; 135 | }); 136 | } 137 | } 138 | 139 | var sum = 0, 140 | numbers = string 141 | .replace(/(?!\.)\W+/g, ' ') // replaces non-word chars (excluding '.') with whitespace 142 | .replace(/^\s+|\s+$|(?:and|plus|with)\s?/g, '') // trim L/R whitespace, replace known join words with '' 143 | .split(' '); 144 | 145 | for(var j = 0, nLen = numbers.length; j < nLen; j++) { 146 | if(numbers[j] && isFinite(numbers[j])) { 147 | sum += parseFloat(numbers[j]); 148 | } else if(!numbers[j]) { 149 | throw "juration.parse(): Unable to parse: a falsey value"; 150 | } else { 151 | // throw an exception if it's not a valid word/unit 152 | throw "juration.parse(): Unable to parse: " + numbers[j].replace(/^\d+/g, ''); 153 | } 154 | } 155 | return sum; 156 | }; 157 | 158 | // _padLeft('5', '0', 2); // 05 159 | var _padLeft = function(s, c, n) { 160 | if (! s || ! c || s.length >= n) { 161 | return s; 162 | } 163 | 164 | var max = (n - s.length)/c.length; 165 | for (var i = 0; i < max; i++) { 166 | s = c + s; 167 | } 168 | 169 | return s; 170 | }; 171 | 172 | var _pluralize = function(count, singular) { 173 | return count == 1 ? singular : singular + "s"; 174 | }; 175 | 176 | var _isNumeric = function(n) { 177 | return !isNaN(parseFloat(n)) && isFinite(n); 178 | }; 179 | 180 | var _extend = function(obj, extObj) { 181 | for (var i in extObj) { 182 | if(extObj[i] !== undefined) { 183 | obj[i] = extObj[i]; 184 | } 185 | } 186 | return obj; 187 | }; 188 | 189 | var juration = { 190 | parse: parse, 191 | stringify: stringify, 192 | humanize: stringify 193 | }; 194 | 195 | if ( typeof module === "object" && module && typeof module.exports === "object" ) { 196 | //loaders that implement the Node module pattern (including browserify) 197 | module.exports = juration; 198 | } else { 199 | // Otherwise expose juration 200 | window.juration = juration; 201 | 202 | // Register as a named AMD module 203 | if ( typeof define === "function" && define.amd ) { 204 | define("juration", [], function () { return juration; } ); 205 | } 206 | } 207 | })(); -------------------------------------------------------------------------------- /kiln/templates/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Kiln Controller 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 57 | 58 | 59 |
60 | 61 |
62 |
63 | 64 | 65 | 66 |
67 |
68 | 69 |
70 |
71 | 72 |
73 |
74 | 75 |
76 |
77 |
78 |
79 |
80 | 81 |
82 | 83 | 88 |
89 |
90 | 104 | 109 | webcam 110 |
111 |
112 |
113 | 114 |
115 |
116 | Set point 117 |
118 |
119 |
120 | Time 121 | 122 |
123 |
124 | Temp 125 | 126 |
127 |
128 |
129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /kiln/Adafruit_alphanumeric.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import threading 3 | import time 4 | import datetime 5 | from Adafruit_LEDBackpack import LEDBackpack 6 | 7 | # =========================================================================== 8 | # Alphanumeric Display 9 | # =========================================================================== 10 | 11 | # This class is meant to be used with the 4-character alphanumeric 12 | # displays available from Adafruit 13 | 14 | class Alphanumeric(object): 15 | disp = None 16 | 17 | # Hexadecimal character lookup table 18 | lut = [ 19 | 0b0000000000000001, 20 | 0b0000000000000010, 21 | 0b0000000000000100, 22 | 0b0000000000001000, 23 | 0b0000000000010000, 24 | 0b0000000000100000, 25 | 0b0000000001000000, 26 | 0b0000000010000000, 27 | 0b0000000100000000, 28 | 0b0000001000000000, 29 | 0b0000010000000000, 30 | 0b0000100000000000, 31 | 0b0001000000000000, 32 | 0b0010000000000000, 33 | 0b0100000000000000, 34 | 0b1000000000000000, 35 | 0b0000000000000000, 36 | 0b0000000000000000, 37 | 0b0000000000000000, 38 | 0b0000000000000000, 39 | 0b0000000000000000, 40 | 0b0000000000000000, 41 | 0b0000000000000000, 42 | 0b0000000000000000, 43 | 0b0001001011001001, 44 | 0b0001010111000000, 45 | 0b0001001011111001, 46 | 0b0000000011100011, 47 | 0b0000010100110000, 48 | 0b0001001011001000, 49 | 0b0011101000000000, 50 | 0b0001011100000000, 51 | 0b0000000000000000, # 52 | 0b0000000000000110, # ! 53 | 0b0000001000100000, # " 54 | 0b0001001011001110, # # 55 | 0b0001001011101101, # $ 56 | 0b0000110000100100, # % 57 | 0b0010001101011101, # & 58 | 0b0000010000000000, # ' 59 | 0b0010010000000000, # ( 60 | 0b0000100100000000, # ) 61 | 0b0011111111000000, # * 62 | 0b0001001011000000, # + 63 | 0b0000100000000000, # , 64 | 0b0000000011000000, # - 65 | 0b0000000000000000, # . 66 | 0b0000110000000000, # / 67 | 0b0000110000111111, # 0 68 | 0b0000000000000110, # 1 69 | 0b0000000011011011, # 2 70 | 0b0000000010001111, # 3 71 | 0b0000000011100110, # 4 72 | 0b0010000001101001, # 5 73 | 0b0000000011111101, # 6 74 | 0b0000000000000111, # 7 75 | 0b0000000011111111, # 8 76 | 0b0000000011101111, # 9 77 | 0b0001001000000000, # : 78 | 0b0000101000000000, # ; 79 | 0b0010010000000000, # < 80 | 0b0000000011001000, # = 81 | 0b0000100100000000, # > 82 | 0b0001000010000011, # ? 83 | 0b0000001010111011, # @ 84 | 0b0000000011110111, # A 85 | 0b0001001010001111, # B 86 | 0b0000000000111001, # C 87 | 0b0001001000001111, # D 88 | 0b0000000011111001, # E 89 | 0b0000000001110001, # F 90 | 0b0000000010111101, # G 91 | 0b0000000011110110, # H 92 | 0b0001001000000000, # I 93 | 0b0000000000011110, # J 94 | 0b0010010001110000, # K 95 | 0b0000000000111000, # L 96 | 0b0000010100110110, # M 97 | 0b0010000100110110, # N 98 | 0b0000000000111111, # O 99 | 0b0000000011110011, # P 100 | 0b0010000000111111, # Q 101 | 0b0010000011110011, # R 102 | 0b0000000011101101, # S 103 | 0b0001001000000001, # T 104 | 0b0000000000111110, # U 105 | 0b0000110000110000, # V 106 | 0b0010100000110110, # W 107 | 0b0010110100000000, # X 108 | 0b0001010100000000, # Y 109 | 0b0000110000001001, # Z 110 | 0b0000000000111001, # [ 111 | 0b0010000100000000, # 112 | 0b0000000000001111, # ] 113 | 0b0000110000000011, # ^ 114 | 0b0000000000001000, # _ 115 | 0b0000000100000000, # ` 116 | 0b0001000001011000, # a 117 | 0b0010000001111000, # b 118 | 0b0000000011011000, # c 119 | 0b0000100010001110, # d 120 | 0b0000100001011000, # e 121 | 0b0000000001110001, # f 122 | 0b0000010010001110, # g 123 | 0b0001000001110000, # h 124 | 0b0001000000000000, # i 125 | 0b0000000000001110, # j 126 | 0b0011011000000000, # k 127 | 0b0000000000110000, # l 128 | 0b0001000011010100, # m 129 | 0b0001000001010000, # n 130 | 0b0000000011011100, # o 131 | 0b0000000101110000, # p 132 | 0b0000010010000110, # q 133 | 0b0000000001010000, # r 134 | 0b0010000010001000, # s 135 | 0b0000000001111000, # t 136 | 0b0000000000011100, # u 137 | 0b0010000000000100, # v 138 | 0b0010100000010100, # w 139 | 0b0010100011000000, # x 140 | 0b0010000000001100, # y 141 | 0b0000100001001000, # z 142 | 0b0000100101001001, # { 143 | 0b0001001000000000, # | 144 | 0b0010010010001001, # } 145 | 0b0000010100100000, # ~ 146 | 0b0011111111111111] 147 | 148 | special = dict(degree= 0b0000000011100011, cone=0b0010100000001000) 149 | 150 | def __init__(self, address=0x70, debug=False): 151 | self.disp = LEDBackpack(address=address, debug=debug) 152 | 153 | def writeCharRaw(self, charNumber, value): 154 | "Sets a digit using the raw 16-bit value" 155 | if (charNumber > 4): 156 | return 157 | # Set the appropriate digit 158 | self.disp.setBufferRow(charNumber, value) 159 | 160 | def writeChar(self, charNumber, value, dot=False): 161 | "Sets a single decimal or hexademical value (0..9 and A..F)" 162 | if (charNumber > 4): 163 | return 164 | if value in self.special: 165 | value = self.special[value] 166 | else: 167 | value = self.lut[ord(value)] 168 | # Set the appropriate digit 169 | self.disp.setBufferRow(charNumber, value | (dot << 14)) 170 | 171 | class AlphaScroller(threading.Thread): 172 | def __init__(self, address=0x70, interval=.25): 173 | self.interval = interval 174 | self.disp = Alphanumeric(address) 175 | self.text = '' 176 | self.counter = 0 177 | self.pause = threading.Event() 178 | self.pause.set() 179 | self.shown = True 180 | 181 | super(AlphaScroller, self).__init__() 182 | self.running = True 183 | 184 | def stop(self): 185 | self.pause.set() 186 | self.running = False 187 | 188 | def set_text(self, text, pad=True, reset=True): 189 | i = 0 190 | self.text = [] 191 | while i < len(text): 192 | if i+1 < len(text) and text[i+1] == ".": 193 | self.text.append((text[i], True)) 194 | i+= 1 195 | else: 196 | self.text.append((text[i],)) 197 | i+= 1 198 | 199 | if pad: 200 | self.text += [(' ',)]*4 201 | 202 | if reset: 203 | self.counter = 0 204 | 205 | def set_speed(self, interval): 206 | self.interval = interval 207 | 208 | def show(self): 209 | self.pause.set() 210 | self.shown = True 211 | 212 | def hide(self): 213 | self.pause.clear() 214 | self.clear() 215 | self.shown = False 216 | 217 | def clear(self): 218 | for i in range(4): 219 | self.disp.writeChar(i, ' ') 220 | 221 | def run(self): 222 | while self.running: 223 | for i in range(4): 224 | if len(self.text) < 4: 225 | char = self.text[i] if i < len(self.text) else (' ',) 226 | else: 227 | char = self.text[(self.counter+i) % len(self.text)] 228 | if len(char[0]) == 0: 229 | print self.text 230 | self.disp.writeChar(i, *char) 231 | time.sleep(self.interval) 232 | self.counter += 1 233 | self.pause.wait() 234 | self.clear() 235 | 236 | if __name__ == "__main__": 237 | scroller = AlphaScroller(interval=.4) 238 | scroller.set_text(list("Hello. World.")+["cone","degree"]) 239 | scroller.start() 240 | try: 241 | while scroller.isAlive(): 242 | scroller.join(1) 243 | except KeyboardInterrupt: 244 | scroller.stop() 245 | -------------------------------------------------------------------------------- /kiln/static/js/temp_monitor.js: -------------------------------------------------------------------------------- 1 | var tempgraph = (function(module) { 2 | module.Monitor = function(initial) { 3 | this.temperature = initial; 4 | this.profile = null; 5 | //default to F 6 | this.scalefunc = new module.TempScale("F"); 7 | 8 | this.graph = new module.Graph(); 9 | this._mapped = this.temperature.map(this._map_temp.bind(this)); 10 | this.graph.plot(this._mapped, "temperature", false); 11 | 12 | this.updateTemp(this.last()); 13 | this._bindUI(); 14 | } 15 | module.Monitor.prototype.updateTemp = function(data) { 16 | var now = new Date(data.time*1000.); 17 | var temp = this.scalefunc.scale(data.temp); 18 | 19 | var nowstr = module.format_time(now); 20 | $("#current_time").text(nowstr); 21 | $("#current_temp").text(this.scalefunc.print(Math.round(temp*100) / 100)); 22 | 23 | //Adjust x and ylims 24 | if (now > this.last().time) { 25 | this.temperature.push(data); 26 | this._mapped.push({x:now, y:temp}); 27 | 28 | var lims = this.graph.x.domain(); 29 | //incoming sample needs to shift xlim 30 | if (now > lims[1]) { 31 | var start = new Date(now.getTime() - lims[1].getTime() + lims[0].getTime()); 32 | this.graph.x.domain([start, now]); 33 | } 34 | 35 | //If incoming sample is higher or lower than the ylims, expand that as well 36 | var ylims = this.graph.y.domain(); 37 | if (temp >= ylims[1] || temp <= ylims[0]) { 38 | this.graph.recenter(.2); 39 | } 40 | this.graph.update("temperature", this._mapped); 41 | } 42 | 43 | //update the output slider and text, if necessary 44 | if (data.output !== undefined) { 45 | if (data.output == -1) { 46 | $("#current_output_text").text("Off"); 47 | $("#current_output").val(0); 48 | } else { 49 | var outstr = Math.round(data.output*10000) / 100; 50 | $("#current_output_text").text(outstr+"%"); 51 | $("#current_output").val(data.output*1000); 52 | } 53 | } 54 | 55 | //update the profile 56 | if (this.profile) 57 | this.profile.update(); 58 | } 59 | module.Monitor.prototype.setProfile = function(schedule, start_time) { 60 | this.profile = new module.Profile(this.graph, this.scalefunc, schedule, start_time); 61 | var start = this.profile.time_start === undefined ? 62 | "Not started" : module.format_time(start_time); 63 | $("#profile_time_total").text(this.profile.time_total); 64 | $("#profile_time_start").text(start); 65 | //$("#profile_time_finish") = this.profile.time_finish(); 66 | $("#profile_info, #profile_actions").hide().removeClass("hidden").slideDown(); 67 | return this.profile; 68 | } 69 | module.Monitor.prototype.last = function() { 70 | return this.temperature[this.temperature.length-1]; 71 | } 72 | 73 | module.Monitor.prototype.setScale = function(scale) { 74 | $("a#temp_scale_C").parent().removeClass("active"); 75 | $("a#temp_scale_F").parent().removeClass("active"); 76 | $("a#temp_scale_cone").parent().removeClass("active"); 77 | if (scale == "C") { 78 | $("li a#temp_scale_C").parent().addClass("active"); 79 | this.scalefunc = new module.TempScale("C"); 80 | this.graph.ylabel("Temperature (°C)") 81 | } else if (scale == "F") { 82 | $("li a#temp_scale_F").parent().addClass("active"); 83 | this.scalefunc = new module.TempScale("F"); 84 | this.graph.ylabel("Temperature (°F)") 85 | } else if (scale == "cone") { 86 | $("li a#temp_scale_cone").parent().addClass("active"); 87 | this.scalefunc = new module.TempScale("cone"); 88 | this.graph.ylabel("Temperature (Δ)"); 89 | } 90 | this._mapped = this.temperature.map(this._map_temp.bind(this)); 91 | this.graph.y.domain(d3.extent(this._mapped, function(d) { return d.y; })); 92 | 93 | this.updateTemp(this.last()); 94 | this.graph.update("temperature", this._mapped); 95 | if (this.profile) 96 | this.profile.setScale(this.scalefunc); 97 | } 98 | 99 | module.Monitor.prototype._map_temp = function(d) { 100 | return {x:new Date(d.time*1000), y:this.scalefunc.scale(d.temp)}; 101 | } 102 | 103 | module.Monitor.prototype.setState = function(name, data) { 104 | if (name == "Lit" || name =="Running") { 105 | $("#ignite_button").addClass("disabled"); 106 | $("#current_output").removeAttr("disabled"); 107 | $("#stop_button").removeClass("disabled"); 108 | $("#stop_button_navbar").removeClass("hidden disabled"); 109 | $("#profile_select").removeClass("disabled"); 110 | } else if (name == "Idle" || name == "Cooling") { 111 | $("#ignite_button").removeClass("disabled"); 112 | $("#current_output").attr("disabled", "disabled"); 113 | $("#stop_button").addClass("disabled"); 114 | $("#stop_button_navbar").addClass("hidden disabled"); 115 | $("#profile_select").removeClass("disabled"); 116 | } 117 | if (name == "Running") { 118 | if (data !== undefined) { 119 | this.setProfile(data.schedule, new Date(data.start_time*1000)); 120 | } 121 | this.profile.setState(true); 122 | } else if (this.profile === undefined) { 123 | this.profile.setState(false); 124 | } 125 | } 126 | module.Monitor.prototype._bindUI = function() { 127 | $("#temp_scale_C").click(function() { this.setScale("C");}.bind(this)); 128 | $("#temp_scale_F").click(function() { this.setScale("F");}.bind(this)); 129 | //$("#temp_scale_C").click(function() { this.setScale("C");}.bind(this)); 130 | 131 | $("#profile_name").val(""); 132 | 133 | $("#ignite_button").click(function() { 134 | this._disable_all(); 135 | $.getJSON("/do/ignite", function(data) { 136 | if (data.type == "error") 137 | alert(data.msg, data.error); 138 | }); 139 | }.bind(this)); 140 | 141 | $("#stop_button, #stop_button_navbar").click(function() { 142 | this._disable_all(); 143 | $.getJSON("/do/shutoff", function(data) { 144 | if (data.type == "error") 145 | alert(data.msg, data.error); 146 | else if (data.type == "success") { 147 | $("#current_output").val(0); 148 | } 149 | }); 150 | }.bind(this)); 151 | 152 | $("#current_output").mouseup(function(e) { 153 | $.getJSON("/do/set?value="+(e.target.value / 1000), function(data) { 154 | if (data.type == "error") 155 | alert(data.msg, data.error); 156 | else if (data.type == "success") 157 | $("#current_output_text").text(e.target.value/10 + "%"); 158 | }) 159 | }); 160 | 161 | $("#profile_list a").click(function(e) { 162 | $("#profile_list li").removeClass("active"); 163 | $(e.target).parent().addClass("active"); 164 | $("#profile_name").val($(e.target).text()); 165 | var fname = $(e.target).attr("data-fname"); 166 | if (this.profile) 167 | this.profile.cleanup(); 168 | $.getJSON("/profile/"+fname, function(data) { 169 | this.setProfile(data); 170 | }.bind(this)); 171 | }.bind(this)); 172 | 173 | try { 174 | var sock = new WebSocket("ws://"+window.location.hostname+":"+window.location.port+"/ws/"); 175 | 176 | sock.onmessage = function(event) { 177 | var data = JSON.parse(event.data); 178 | if (data.type == "temperature") 179 | this.updateTemp(data); 180 | else if (data.type == "state") { 181 | this.setState(data.state); 182 | } 183 | }.bind(this); 184 | } catch (e) {} 185 | } 186 | 187 | module.Monitor.prototype._disable_all = function() { 188 | $("button").addClass("disabled"); 189 | $("input").attr("disabled", "disabled"); 190 | } 191 | 192 | 193 | module.TempScale = function(name) { 194 | if (name == "C") { 195 | this.scale = function(t) { return t;}; 196 | this.inverse = function(t) { return t;}; 197 | this.print = function(t) { return t+"°C"} 198 | } else if (name == "F") { 199 | this.scale = function(temp) { return temp * 9 / 5 + 32; } 200 | this.inverse = function(temp) { return (temp - 32) * 5 / 9;} 201 | this.print = function(t) { return t+"°F"} 202 | } else if (name == "cone") { 203 | 204 | } 205 | } 206 | module.TempScale.C_to_cone = function(temp) { 207 | 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] 208 | var names = []; 209 | for (var i = -22; i < 0; i++) { 210 | names.push("0"+(""+i).slice(1)); 211 | } 212 | for (var i = 1; i < 14; i++) { 213 | names.push(""+i); 214 | } 215 | return cones, names 216 | } 217 | 218 | module.format_time = function(now) { 219 | if (!(now instanceof Date)) 220 | now = new Date(now); 221 | var hourstr = now.getHours() % 12; 222 | hourstr = hourstr == 0 ? 12 : hourstr; 223 | var minstr = now.getMinutes(); 224 | minstr = minstr < 10 ? "0"+minstr : minstr; 225 | return hourstr + ":" + minstr + (now.getHours() >= 12 ? " pm" : " am"); 226 | } 227 | 228 | return module; 229 | }(tempgraph || {})); 230 | -------------------------------------------------------------------------------- /kiln/static/js/temp_graph.js: -------------------------------------------------------------------------------- 1 | var tempgraph = (function(module) { 2 | module.graph_defaults = { 3 | margin: {top: 20, right: 20, bottom: 30, left: 50}, 4 | object: "#graph", 5 | show_axes: true, 6 | } 7 | module.Graph = function(options) { 8 | if (options === undefined) 9 | options = module.graph_defaults; 10 | 11 | if (options.object !== undefined) 12 | this.obj = options.object; 13 | else 14 | this.obj = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 15 | 16 | this.margin = options.margin; 17 | this.width = options.width ? options.width : $(this.obj).width() - this.margin.left - this.margin.right; 18 | this.height = options.height ? options.height : $(this.obj).height() - this.margin.top - options.margin.bottom; 19 | 20 | this.svg = d3.select(this.obj); 21 | this.svg.append("defs").append("clipPath").attr("id", "pane") 22 | .append("rect") 23 | .attr("width", this.width) 24 | .attr("height", this.height); 25 | 26 | this.pane = this.svg.append("g") 27 | .attr("transform", "translate("+this.margin.left+","+this.margin.top+")") 28 | 29 | /*xfm.append("rect") 30 | .attr("style", "fill:#DDD") 31 | .attr("width", this.width) 32 | .attr("height", this.height);*/ 33 | 34 | this.x = d3.time.scale().range([0, this.width]); 35 | this.y = d3.scale.linear().range([this.height, 0]); 36 | 37 | this.zoom = d3.behavior.zoom(this.obj).on("zoom", this.draw.bind(this)) 38 | .on("zoomend", this.recenter.bind(this, .2)); 39 | 40 | if (options.show_axes === undefined || options.show_axes) { 41 | this.x_axis = d3.svg.axis().scale(this.x).orient("bottom") 42 | .tickSize(this.height).tickSubdivide(true); 43 | this.y_axis = d3.svg.axis().scale(this.y).orient("left") 44 | .tickSize(this.width).tickSubdivide(true); 45 | 46 | //setup axies labels and ticks 47 | this.pane.append("g") 48 | .attr("class", "x axis") 49 | //.attr("transform", "translate(0," + this.height + ")") 50 | .call(this.x_axis); 51 | 52 | this.pane.append("g") 53 | .attr("class", "y axis") 54 | .attr("transform", "translate("+this.width+", 0)") 55 | .call(this.y_axis) 56 | .append("text") 57 | .attr("class", "ylabel") 58 | .attr("transform", "translate(-"+this.width+",0)rotate(-90)") 59 | .attr("y", 6) 60 | .attr("dy", ".71em") 61 | .style("text-anchor", "end") 62 | .text("Temperature (°F)"); 63 | 64 | } 65 | 66 | this.axes = this.pane.append("g") 67 | .attr("class", "axes") 68 | .attr("style", "clip-path:url(#pane)"); 69 | window.onresize = this.resize.bind(this); 70 | this.lines = {}; 71 | }; 72 | module.Graph.prototype.plot = function(data, className, marker) { 73 | this.x.domain(d3.extent(data, function(d) { return d.x; })); 74 | this.zoom.x(this.x); 75 | 76 | var line = d3.svg.line() 77 | .x(function(d) { return this.x(d.x); }.bind(this)) 78 | .y(function(d) { return this.y(d.y); }.bind(this)); 79 | 80 | this.axes.append("path") 81 | .datum(data) 82 | .attr("class", className) 83 | .attr("d", line); 84 | 85 | if (marker !== undefined && marker) { 86 | var selector = className.replace(/ /gi, "."); 87 | var key = data.id === undefined ? undefined : function(d){ return d.id;}; 88 | var marker = this.axes.append("g").selectAll("."+selector+".dot") 89 | .data(data, key).enter().append("circle") 90 | .attr("class", className+" dot") 91 | .attr("r", 10) 92 | .attr("cx", function(d) { return this.x(d.x); }.bind(this)) 93 | .attr("cy", function(d) { return this.y(d.y); }.bind(this)); 94 | } 95 | 96 | this.lines[className] = {line:line, data:data, marker:marker}; 97 | this.svg.call(this.zoom); 98 | this.draw(); 99 | this.recenter(.2); 100 | return this.lines[className]; 101 | } 102 | module.Graph.prototype.draw = function() { 103 | this.svg.select("g.x.axis").call(this.x_axis); 104 | this.svg.select("g.y.axis").call(this.y_axis); 105 | var line, data, marker; 106 | for (var name in this.lines) { 107 | line = this.lines[name].line; 108 | data = this.lines[name].data; 109 | marker = this.lines[name].marker; 110 | if (marker !== undefined) { 111 | this.axes.selectAll(".dot").data(data) 112 | .attr("cx", function(d) { return this.x(d.x)}.bind(this)) 113 | .attr("cy", function(d) { return this.y(d.y)}.bind(this)); 114 | } 115 | this.svg.select("path."+name).attr("d", line); 116 | } 117 | } 118 | module.Graph.prototype.resize = function() { 119 | var margin = this.margin; 120 | var width = $(this.obj).width() - margin.left - margin.right; 121 | var height = $(this.obj).height() - margin.top - margin.bottom; 122 | 123 | this.svg 124 | .attr("width", width + margin.left + margin.right) 125 | .attr("height", height + margin.top + margin.bottom) 126 | this.x.range([0, width]); 127 | this.y.range([height, 0]); 128 | 129 | this.width = width; 130 | this.height = height; 131 | this.draw(); 132 | } 133 | module.Graph.prototype.recenter = function(margin) { 134 | //Argument margin gives the fraction of (max - min) to add to the margin 135 | //Defaults to 0 136 | var extent = [], data, valid, 137 | low = this.x.domain()[0], high=this.x.domain()[1]; 138 | 139 | for (var name in this.lines) { 140 | data = this.lines[name].data; 141 | valid = data.filter(function(d) { return low <= d.x && d.x <= high; }) 142 | extent = extent.concat(valid); 143 | } 144 | extent = d3.extent(extent, function(d){return d.y}); 145 | if (margin > 0) { 146 | var range = extent[1]-extent[0]; 147 | extent[0] -= margin*range; 148 | extent[1] += margin*range; 149 | } 150 | this.y.domain(extent); 151 | this.draw(); 152 | } 153 | module.Graph.prototype.resample = function(data) { 154 | var npoints = 1000; 155 | if (data.length<=npoints) 156 | return data; 157 | var idxs = [] 158 | var within = []; 159 | var within_idx = []; 160 | for (var i=0; i=this.xlim()[0] && data[i].x<=this.xlim()[1]) { 162 | within.push(data[i]); 163 | within_idx.push(i); 164 | } 165 | } 166 | if (within.length<=npoints) 167 | return within; 168 | md = Math.floor(within.length/npoints); 169 | var fin = [] 170 | for (var i=0; i self.phase else -1 108 | output = self.pattern[self.phase%len(self.pattern)] 109 | for pin, out in zip(self.pins, output): 110 | GPIO.output(pin, out) 111 | 112 | if not self.queue.empty(): 113 | step, speed, block = self.queue.get() 114 | ispeed = 1. / (2.*speed) 115 | target += step 116 | if block: 117 | self._step(target - self.phase, speed) 118 | self.finished.set() 119 | 120 | diff = ispeed - (time.time() - now) 121 | if (diff) > 0: 122 | time.sleep(diff) 123 | else: 124 | warnings.warn("Step rate too high, stepping as fast as possible") 125 | 126 | def _step(self, step, speed): 127 | print "Stepping %d steps at %d steps / second"%(step, speed) 128 | if step < 0: 129 | steps = range(step, 0)[::-1] 130 | else: 131 | steps = range(step) 132 | 133 | for i in steps: 134 | now = time.time() 135 | output = self.pattern[(self.phase+i)%len(self.pattern)] 136 | for pin, out in zip(self.pins, output): 137 | GPIO.output(pin, out) 138 | 139 | diff = 1. / (2*speed) - (time.time() - now) 140 | if (diff) > 0: 141 | time.sleep(diff) 142 | 143 | self.phase += step 144 | 145 | class StepperSim(object): 146 | def __init__(self): 147 | self.phase = 0 148 | 149 | def step(self, num, speed=10, block=False): 150 | print "Simulated stepping %d steps at %d steps / second"%(num, speed) 151 | if block: 152 | time.sleep(1) 153 | 154 | def stop(self): 155 | print "stopping simulated regulator" 156 | 157 | class Regulator(threading.Thread): 158 | def __init__(self, maxsteps=4500, minsteps=2480, speed=150, ignite_pin=None, flame_pin=None, simulate=False): 159 | """Set up a stepper-controlled regulator. Implement some safety measures 160 | to make sure everything gets shut off at the end 161 | 162 | TODO: integrate flame sensor by converting this into a thread, and checking 163 | flame state regularly. If flame sensor off, immediately increase gas and attempt 164 | reignition, or shut off after 5 seconds of failure. 165 | 166 | Parameters 167 | ---------- 168 | maxsteps : int 169 | The max value for the regulator, in steps 170 | minsteps : int 171 | The minimum position to avoid extinguishing the flame 172 | speed : int 173 | Speed to turn the stepper, in steps per second 174 | ignite_pin : int or None 175 | If not None, turn on this pin during the ignite sequence 176 | """ 177 | 178 | if simulate: 179 | self.stepper = StepperSim() 180 | else: 181 | self.stepper = Stepper() 182 | self.stepper.start() 183 | 184 | self.current = 0 185 | self.max = maxsteps 186 | self.min = minsteps 187 | self.speed = speed 188 | 189 | self.ignite_pin = ignite_pin 190 | if ignite_pin is not None: 191 | GPIO.setup(ignite_pin, OUT) 192 | self.flame_pin = flame_pin 193 | if flame_pin is not None: 194 | GPIO.setup(flame_pin, IN) 195 | 196 | def exit(): 197 | if self.current != 0: 198 | self.off() 199 | self.stepper.stop() 200 | atexit.register(exit) 201 | 202 | def ignite(self, start=2800, delay=1): 203 | if self.current != 0: 204 | raise ValueError("Must be off to ignite") 205 | 206 | logger.info("Ignition start") 207 | self.stepper.step(start, self.speed, block=True) 208 | if self.ignite_pin is not None: 209 | GPIO.output(self.ignite_pin, True) 210 | time.sleep(delay) 211 | if self.ignite_pin is not None: 212 | GPIO.output(self.ignite_pin, False) 213 | self.stepper.step(self.min - start, self.speed, block=True) 214 | self.current = self.min 215 | logger.info("Ignition complete") 216 | 217 | def off(self, block=True): 218 | logger.info("Shutting off gas") 219 | self.stepper.step(-self.current, self.speed, block=block) 220 | #self.stepper.home() 221 | self.current = 0 222 | 223 | def set(self, value, block=False): 224 | if self.current == 0: 225 | raise ValueError("System must be ignited to set value") 226 | if not 0 <= value <= 1: 227 | raise ValueError("Must give fraction between 0 and 1") 228 | target = int(value * (self.max - self.min) + self.min) 229 | nsteps = target - self.current 230 | print "Currently at %d, target %d, stepping %d"%(self.current, target, nsteps) 231 | self.current = target 232 | self.stepper.step(nsteps, self.speed, block=block) 233 | 234 | @property 235 | def output(self): 236 | out = (self.current - self.min) / float(self.max - self.min) 237 | if out < 0: 238 | return -1 239 | return out 240 | 241 | def run(self): 242 | """Check the status of the flame sensor""" 243 | #since the flame sensor does not yet exist, we'll save this for later 244 | pass 245 | 246 | class Breakout(object): 247 | def __init__(self, addr, maxsteps=6500, minsteps=((2600, 0), (2300, 15)) ): 248 | import breakout 249 | self.device = breakout.Breakout(addr) 250 | self.min_interp = minsteps 251 | self.max = maxsteps 252 | 253 | def exit(): 254 | if self.device.motor != 0: 255 | self.off() 256 | atexit.register(exit) 257 | 258 | @property 259 | def min(self): 260 | temp = self.device.status.aux_temp0 261 | if temp > self.min_interp[1][1]: 262 | return self.min_interp[1][0] 263 | elif temp <= self.min_interp[0][1]: 264 | return self.min_interp[0][0] 265 | else: 266 | mrange = self.min_interp[0][0] - self.min_interp[1][0] 267 | trange = self.min_interp[1][1] - self.min_interp[0][1] 268 | mix = (temp - self.min_interp[0][1]) / float(trange) 269 | return mrange * mix + self.min_interp[1][0] 270 | 271 | def ignite(self, start=2400): 272 | logger.info("Igniting system") 273 | self.device.motor = start 274 | while self.device.motor != start: 275 | time.sleep(.1) 276 | self.device.motor = self.min 277 | 278 | @property 279 | def output(self): 280 | m = self.min 281 | out = (self.device.motor - m) / float(self.max - m) 282 | if out < 0: 283 | return -1 284 | return out 285 | 286 | def set(self, value): 287 | m = self.min 288 | if self.device.motor == 0: 289 | raise ValueError('Must ignite first') 290 | if not 0 <= value <= 1: 291 | raise ValueError('Must give value between 0 and 1') 292 | self.device.motor = int((self.max - m)*value + m) 293 | 294 | def off(self): 295 | self.device.motor = 0 296 | self.device.ignite = 0 297 | logger.info("Shutting off regulator") 298 | -------------------------------------------------------------------------------- /kiln/static/js/temp_profile.js: -------------------------------------------------------------------------------- 1 | var tempgraph = (function(module) { 2 | module.Profile = function(graph, scale, schedule, start_time) { 3 | var end = schedule[schedule.length-1][0]; 4 | this.length = end; 5 | this.time_total = juration.stringify(end); 6 | this.time_start = start_time; 7 | this.schedule = schedule; 8 | 9 | this.graph = graph; 10 | this.scalefunc = scale; 11 | 12 | this.drag_start = d3.behavior.drag() 13 | .on("dragstart", function() { 14 | d3.event.sourceEvent.stopPropagation(); 15 | }).on("drag.profile", this.dragStart.bind(this)); 16 | 17 | //Generate the highlight pane to indicate where the profile is running 18 | this.pane_stroke = this.graph.pane.insert("line", ":first-child") 19 | .attr("class", "profile-pane-stroke") 20 | .attr("y1", 0).attr("y2", this.graph.height) 21 | .call(this.drag_start); 22 | this.pane = this.graph.pane.insert("rect", ":first-child") 23 | .attr("class", "profile-pane") 24 | .attr("height", this.graph.height) 25 | .attr("clip-path", "url(#pane)"); 26 | 27 | this.line = this.graph.plot(this._schedule(), "profile-line", true); 28 | 29 | //immediately view range from 10 min before to end time of profile 30 | var now = new Date(); 31 | var rstart = new Date(now.getTime() - 10*60*100); 32 | var rend = this.time_finish(now); 33 | this.graph.xlim(rstart, rend); 34 | 35 | this.drag = d3.behavior.drag().origin(function(d) { 36 | return {x:this.graph.x(d.x), y:this.graph.y(d.y)}; 37 | }.bind(this)).on("dragstart", function(d) { 38 | d3.event.sourceEvent.stopPropagation(); 39 | this._node = this._findNode(d); 40 | }.bind(this)); 41 | 42 | this.update(); 43 | 44 | //events 45 | this._bindUI(); 46 | } 47 | module.Profile.prototype.time_finish = function() { 48 | var now = this.time_start instanceof Date ? this.time_start : new Date(); 49 | return new Date(now.getTime() + this.length*1000); 50 | } 51 | 52 | module.Profile.prototype.setScale = function(scale) { 53 | this.scalefunc = scale; 54 | this.update(); 55 | } 56 | module.Profile.prototype.update = function() { 57 | var start_time = this.time_start instanceof Date ? this.time_start : new Date(); 58 | var end_time = new Date(start_time.getTime()+this.length*1000); 59 | var width = this.graph.x(end_time) - this.graph.x(start_time); 60 | var x = this.graph.x(start_time); 61 | this.pane.attr("width", width) 62 | .attr("transform","translate("+this.graph.x(start_time)+",0)"); 63 | this.pane_stroke.attr("x1", x).attr("x2", x); 64 | 65 | var join = this.graph.update("profile-line", this._schedule()); 66 | join.on("mouseover.profile", this.hoverNode.bind(this)) 67 | .on("mouseout.profile", this._hideInfo.bind(this)) 68 | .on("dblclick.profile", this.delNode.bind(this)); 69 | join.call(this.drag); 70 | 71 | //update the profile info box 72 | var start = this.time_start instanceof Date ? module.format_time(this.time_start) : "Not started"; 73 | var finish = this.time_finish(); 74 | var remain = (finish - (new Date())) / 1000; 75 | $("#profile_time_finish").text(module.format_time(finish)); 76 | $("#profile_time_start").text(start); 77 | $("#profile_time_remain").text(juration.stringify(remain)); 78 | } 79 | 80 | module.Profile.prototype._bindUI = function() { 81 | $("#profile_name").attr("disabled", "disabled"); 82 | $("#profile_actions .btn-success").click(this.save.bind(this)); 83 | $("#profile_actions .btn-primary").click(this.start.bind(this)); 84 | $("#profile_actions .btn-default").click(this.pause.bind(this)); 85 | 86 | // Info pane events 87 | var updateNode = function() { 88 | clearTimeout(this.timeout_infoedit); 89 | var time = juration.parse($("#profile-node-info input.time").val()); 90 | var temp = parseFloat($("#profile-node-info input.temp").val()); 91 | this._updateNode(this._node, time, temp); 92 | }.bind(this) 93 | 94 | $("#profile-node-info").on("mouseout.profile", this._hideInfo.bind(this)); 95 | $("#profile-node-info").on("mouseover.profile", function() { 96 | clearTimeout(this.timeout_infohide); 97 | }.bind(this)); 98 | $("#profile-node-info input").on("keypress", function(e) { 99 | clearTimeout(this.timeout_infoedit); 100 | if (e.keyCode == 13) { 101 | updateNode(); 102 | } else { 103 | this.timeout_infoedit = setTimeout(updateNode, 2000); 104 | } 105 | }.bind(this)); 106 | $("#profile-node-info input").on("blur", function() { 107 | this._focused = false; 108 | updateNode(); 109 | this._hideInfo(); 110 | }.bind(this)); 111 | $("#profile-node-info input").on("focus", function() { 112 | this._focused = true; 113 | }.bind(this)); 114 | 115 | //Graph events 116 | this.graph.zoom.on("zoom.profile", this.update.bind(this)); 117 | this.setState(); 118 | } 119 | module.Profile.prototype._schedule = function() { 120 | var start_time = this.time_start instanceof Date ? this.time_start : new Date(); 121 | var schedule = []; 122 | for (var i = 0; i < this.schedule.length; i++) { 123 | var time = new Date(start_time.getTime() + this.schedule[i][0]*1000); 124 | var temp = this.scalefunc.scale(this.schedule[i][1]); 125 | schedule.push({id:i, x:time, y:temp}); 126 | } 127 | return schedule; 128 | } 129 | module.Profile.prototype._hideInfo = function() { 130 | this.timeout_infohide = setTimeout(function() { 131 | if (!this._focused) 132 | $("#profile-node-info").fadeOut(100); 133 | }.bind(this), 250); 134 | } 135 | module.Profile.prototype._findNode = function(d) { 136 | return d.id; 137 | } 138 | module.Profile.prototype._updateNode = function(node, time, temp) { 139 | var start_time = this.time_start instanceof Date ? this.time_start : new Date(); 140 | if (!(time instanceof Date)) { 141 | //This is probably just a direct offset, no need to compute time 142 | this.schedule[node][0] = time; 143 | } else if (node == 0) { 144 | this.schedule[node][0] = 0; 145 | } else { 146 | var newtime = (time - start_time.getTime()) / 1000; 147 | this.schedule[node][0] = newtime; 148 | } 149 | //update length only if we're editing the final node 150 | if (node == this.schedule.length-1) { 151 | this.length = this.schedule[node][0]; 152 | } 153 | this.schedule[node][1] = this.scalefunc.inverse(temp); 154 | 155 | //if we're dragging this node "behind" the previous, push it back as well 156 | //except if the previous one is the first node, in which case just set it to zero 157 | if (node > 0 && this.schedule[node-1][0] >= newtime) { 158 | if (node-1 == 0) 159 | this.schedule[node][0] = 0; 160 | else 161 | this.schedule[node-1][0] = newtime; 162 | } else if (node < this.schedule.length-1 && this.schedule[node+1][0] < newtime){ 163 | this.schedule[node+1][0] = newtime; 164 | if (node+1 == this.schedule.length-1) 165 | this.length = this.schedule[node+1][0]; 166 | } 167 | this._showInfo(node); 168 | this.update(); 169 | 170 | //Unlock the save buttons and names 171 | $("#profile_name").removeAttr("disabled"); 172 | $("#profile_actions .btn-success").removeClass("disabled"); 173 | $("#profile_actions .btn-primary").addClass("disabled"); 174 | } 175 | module.Profile.prototype._showInfo = function(node) { 176 | this._node = node; 177 | var start_time = this.time_start instanceof Date ? this.time_start : new Date(); 178 | var time = new Date(this.schedule[node][0]*1000 + start_time.getTime()); 179 | var temp = this.scalefunc.scale(this.schedule[node][1]); 180 | $("#profile-node-info") 181 | .css('left', this.graph.x(time)+80) 182 | .css('top', this.graph.y(temp)+50) 183 | .fadeIn(100); 184 | 185 | $("#profile-node-info div.name").text("Set point "+(node+1)); 186 | $("#profile-node-info input.temp").val(this.scalefunc.print(Math.round(temp*100)/100)); 187 | var timestr; 188 | try { 189 | timestr = juration.stringify(this.schedule[node][0]); 190 | } catch (e) { 191 | timestr = 0; 192 | } 193 | $("#profile-node-info input.time").val(timestr); 194 | } 195 | module.Profile.prototype.addNode = function() { 196 | d3.event.stopPropagation(); 197 | var mouse = d3.mouse(this.graph.pane[0][0]); 198 | var start_time = this.time_start instanceof Date ? this.time_start : new Date(); 199 | 200 | var secs = (this.graph.x.invert(mouse[0]) - start_time) / 1000; 201 | 202 | var start, end; 203 | for (var i = 0; i < this.schedule.length-1; i++) { 204 | start = this.schedule[i][0]; 205 | end = this.schedule[i+1][0]; 206 | if (start < secs && secs < end) { 207 | var t2 = this.schedule[i+1][1], t1 = this.schedule[i][1]; 208 | var frac = (secs - start) / (end - start); 209 | var temp = frac * (t2 - t1) + t1; 210 | this.schedule.splice(i+1, 0, [secs, temp]); 211 | } 212 | } 213 | this.update(); 214 | $("#profile_name").removeAttr("disabled"); 215 | $("#profile_actions .btn-success").removeClass("disabled"); 216 | $("#profile_actions .btn-primary").addClass("disabled"); 217 | } 218 | module.Profile.prototype.delNode = function(d) { 219 | d3.event.stopPropagation(); 220 | var node = this._findNode(d); 221 | //ignore attempts to delete the starting and ending nodes 222 | if (node != 0 && this.schedule.length > 2) { 223 | this.schedule.splice(node, 1); 224 | if (node == this.schedule.length) { 225 | this.length = this.schedule[node-1][0]; 226 | } 227 | } 228 | this.update(); 229 | $("#profile_name").removeAttr("disabled"); 230 | $("#profile_actions .btn-success").removeClass("disabled"); 231 | $("#profile_actions .btn-primary").addClass("disabled"); 232 | } 233 | module.Profile.prototype.dragNode = function(d) { 234 | var time = this.graph.x.invert(d3.event.x); 235 | var temp = this.graph.y.invert(d3.event.y); 236 | this._updateNode(d.id, time, temp); 237 | } 238 | module.Profile.prototype.hoverNode = function(d) { 239 | clearTimeout(this.timeout_infohide); 240 | this._showInfo(d.id); 241 | } 242 | module.Profile.prototype.dragStart = function() { 243 | this.time_start = this.graph.x.invert(d3.event.x); 244 | if (this.time_start > new Date()) 245 | this.time_start = null; 246 | this.update(); 247 | } 248 | 249 | module.Profile.prototype.save = function() { 250 | //convert name into filename 251 | var rawname = $("#profile_name").val(); 252 | var name = rawname.replace(/ /gi, "_"); 253 | name = name.replace(/Δ/gi, "^"); 254 | name += ".json"; 255 | 256 | var post = {schedule:JSON.stringify(this.schedule)}; 257 | 258 | $.post("/profile/"+name, post).done(function(result) { 259 | if (result.type == "success") { 260 | $("#profile_name").attr("disabled", "disabled"); 261 | $("#profile_actions .btn-success").addClass("disabled"); 262 | //Check if the name exists in the menu, otherwise add new entry 263 | var notnew = false; 264 | $("#profile_list a").each(function() { 265 | console.log($(this).data("fname"), $(this).data("fname") == name); 266 | notnew = $(this).data("fname") == name || notnew; 267 | }); 268 | if (!notnew) { 269 | //Add a new entry into the profile list dropdown 270 | $("#profile_list li").removeClass("active"); 271 | var html = "
  • "+rawname+"
  • "; 272 | $("#profile_list").append(html).addClass("active").select("a") 273 | .click(function(e) { 274 | $("#profile_list a").removeClass("active"); 275 | $(e.target).addClass("active"); 276 | $("#profile_name").val($(e.target).text()); 277 | var fname = $(e.target).attr("data-fname"); 278 | this.cleanup(); 279 | $.getJSON("/profile/"+fname, function(data) { 280 | monitor.setProfile(data); 281 | }); 282 | }.bind(this)); 283 | } 284 | this.setState(false); 285 | } else if (result.type == "error") { 286 | alert(result.msg); 287 | } 288 | }.bind(this)); 289 | 290 | } 291 | module.Profile.prototype.setState = function(running) { 292 | this.running = running === undefined ? this.running : running; 293 | if (this.running) { 294 | this.line.marker.on("dblclick.profile", null); 295 | this.graph.pane.on("dblclick.profile", null); 296 | $("#profile-node-info input").attr("disabled", "disabled"); 297 | this.drag.on("drag.profile", null); 298 | this.drag_start.on("drag.profile", null); 299 | $("#profile_actions .btn-success").addClass("disabled"); 300 | $("#profile_actions .btn-primary").addClass("disabled"); 301 | $("#profile_actions .btn-default").removeClass("disabled"); 302 | } else { 303 | this.line.marker.on("dblclick.profile", this.delNode.bind(this)); 304 | this.graph.pane.on("dblclick.profile", this.addNode.bind(this)); 305 | $("#profile-node-info input").removeAttr("disabled"); 306 | this.drag.on("drag.profile", this.dragNode.bind(this)); 307 | this.drag_start.on("drag.profile", this.dragStart.bind(this)); 308 | $("#profile_actions .btn-success").addClass("disabled"); 309 | $("#profile_actions .btn-primary").removeClass("disabled"); 310 | $("#profile_actions .btn-default").addClass("disabled"); 311 | } 312 | } 313 | module.Profile.prototype.cleanup = function() { 314 | this.graph.remove("profile-line"); 315 | this.pane.remove(); 316 | this.pane_stroke.remove(); 317 | } 318 | module.Profile.prototype.pause = function() { 319 | $("#profile_actions .btn-default").addClass("disabled"); 320 | 321 | //TODO: ajax query 322 | this.setState(false) 323 | } 324 | module.Profile.prototype.start = function() { 325 | $("#profile_actions .btn-primary").addClass("disabled"); 326 | var start_time = this.time_start instanceof Date ? this.time_start : new Date(); 327 | var params = { 328 | schedule:JSON.stringify(this.schedule), 329 | start_time:start_time.getTime() / 1000., 330 | interval:1, 331 | }; 332 | $.post("/do/start_profile", params, function(resp){ 333 | var obj = JSON.parse(resp); 334 | if (obj.type == "error") { 335 | alert(obj.msg); 336 | } else { 337 | this.setState(true); 338 | this.time_start = start_time; 339 | } 340 | }.bind(this)); 341 | } 342 | 343 | return module; 344 | }(tempgraph || {})); 345 | -------------------------------------------------------------------------------- /kiln/static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-o-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#2d6ca2));background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary:disabled,.btn-primary[disabled]{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f3f3f3));background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-o-linear-gradient(top,#222 0,#282828 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#222),to(#282828));background-image:linear-gradient(to bottom,#222 0,#282828 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-o-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3071a9));background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-o-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3278b3));background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);background-repeat:repeat-x;border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /models/involute_gears.scad: -------------------------------------------------------------------------------- 1 | // Parametric Involute Bevel and Spur Gears by GregFrost 2 | // It is licensed under the Creative Commons - GNU LGPL 2.1 license. 3 | // © 2010 by GregFrost, thingiverse.com/Amp 4 | // http://www.thingiverse.com/thing:3575 and http://www.thingiverse.com/thing:3752 5 | 6 | // Simple Test: 7 | //gear (circular_pitch=700, 8 | // gear_thickness = 12, 9 | // rim_thickness = 15, 10 | // hub_thickness = 17, 11 | // circles=8); 12 | 13 | //Complex Spur Gear Test: 14 | //test_gears (); 15 | 16 | // Meshing Double Helix: 17 | //test_meshing_double_helix (); 18 | 19 | module test_meshing_double_helix(){ 20 | meshing_double_helix (); 21 | } 22 | 23 | // Demonstrate the backlash option for Spur gears. 24 | //test_backlash (); 25 | 26 | // Demonstrate how to make meshing bevel gears. 27 | //test_bevel_gear_pair(); 28 | 29 | module test_bevel_gear_pair(){ 30 | bevel_gear_pair (); 31 | } 32 | 33 | module test_bevel_gear(){bevel_gear();} 34 | 35 | //bevel_gear(); 36 | 37 | pi=3.1415926535897932384626433832795; 38 | 39 | //================================================== 40 | // Bevel Gears: 41 | // Two gears with the same cone distance, circular pitch (measured at the cone distance) 42 | // and pressure angle will mesh. 43 | 44 | module bevel_gear_pair ( 45 | gear1_teeth = 41, 46 | gear2_teeth = 7, 47 | axis_angle = 90, 48 | outside_circular_pitch=1000) 49 | { 50 | outside_pitch_radius1 = gear1_teeth * outside_circular_pitch / 360; 51 | outside_pitch_radius2 = gear2_teeth * outside_circular_pitch / 360; 52 | pitch_apex1=outside_pitch_radius2 * sin (axis_angle) + 53 | (outside_pitch_radius2 * cos (axis_angle) + outside_pitch_radius1) / tan (axis_angle); 54 | cone_distance = sqrt (pow (pitch_apex1, 2) + pow (outside_pitch_radius1, 2)); 55 | pitch_apex2 = sqrt (pow (cone_distance, 2) - pow (outside_pitch_radius2, 2)); 56 | echo ("cone_distance", cone_distance); 57 | pitch_angle1 = asin (outside_pitch_radius1 / cone_distance); 58 | pitch_angle2 = asin (outside_pitch_radius2 / cone_distance); 59 | echo ("pitch_angle1, pitch_angle2", pitch_angle1, pitch_angle2); 60 | echo ("pitch_angle1 + pitch_angle2", pitch_angle1 + pitch_angle2); 61 | 62 | rotate([0,0,90]) 63 | translate ([0,0,pitch_apex1+20]) 64 | { 65 | translate([0,0,-pitch_apex1]) 66 | bevel_gear ( 67 | number_of_teeth=gear1_teeth, 68 | cone_distance=cone_distance, 69 | pressure_angle=30, 70 | outside_circular_pitch=outside_circular_pitch); 71 | 72 | rotate([0,-(pitch_angle1+pitch_angle2),0]) 73 | translate([0,0,-pitch_apex2]) 74 | bevel_gear ( 75 | number_of_teeth=gear2_teeth, 76 | cone_distance=cone_distance, 77 | pressure_angle=30, 78 | outside_circular_pitch=outside_circular_pitch); 79 | } 80 | } 81 | 82 | //Bevel Gear Finishing Options: 83 | bevel_gear_flat = 0; 84 | bevel_gear_back_cone = 1; 85 | 86 | module bevel_gear ( 87 | number_of_teeth=11, 88 | cone_distance=100, 89 | face_width=20, 90 | outside_circular_pitch=1000, 91 | pressure_angle=30, 92 | clearance = 0.2, 93 | bore_diameter=5, 94 | gear_thickness = 15, 95 | backlash = 0, 96 | involute_facets=0, 97 | finish = -1) 98 | { 99 | echo ("bevel_gear", 100 | "teeth", number_of_teeth, 101 | "cone distance", cone_distance, 102 | face_width, 103 | outside_circular_pitch, 104 | pressure_angle, 105 | clearance, 106 | bore_diameter, 107 | involute_facets, 108 | finish); 109 | 110 | // Pitch diameter: Diameter of pitch circle at the fat end of the gear. 111 | outside_pitch_diameter = number_of_teeth * outside_circular_pitch / 180; 112 | outside_pitch_radius = outside_pitch_diameter / 2; 113 | 114 | // The height of the pitch apex. 115 | pitch_apex = sqrt (pow (cone_distance, 2) - pow (outside_pitch_radius, 2)); 116 | pitch_angle = asin (outside_pitch_radius/cone_distance); 117 | 118 | echo ("Num Teeth:", number_of_teeth, " Pitch Angle:", pitch_angle); 119 | 120 | finish = (finish != -1) ? finish : (pitch_angle < 45) ? bevel_gear_flat : bevel_gear_back_cone; 121 | 122 | apex_to_apex=cone_distance / cos (pitch_angle); 123 | back_cone_radius = apex_to_apex * sin (pitch_angle); 124 | 125 | // Calculate and display the pitch angle. This is needed to determine the angle to mount two meshing cone gears. 126 | 127 | // Base Circle for forming the involute teeth shape. 128 | base_radius = back_cone_radius * cos (pressure_angle); 129 | 130 | // Diametrial pitch: Number of teeth per unit length. 131 | pitch_diametrial = number_of_teeth / outside_pitch_diameter; 132 | 133 | // Addendum: Radial distance from pitch circle to outside circle. 134 | addendum = 1 / pitch_diametrial; 135 | // Outer Circle 136 | outer_radius = back_cone_radius + addendum; 137 | 138 | // Dedendum: Radial distance from pitch circle to root diameter 139 | dedendum = addendum + clearance; 140 | dedendum_angle = atan (dedendum / cone_distance); 141 | root_angle = pitch_angle - dedendum_angle; 142 | 143 | root_cone_full_radius = tan (root_angle)*apex_to_apex; 144 | back_cone_full_radius=apex_to_apex / tan (pitch_angle); 145 | 146 | back_cone_end_radius = 147 | outside_pitch_radius - 148 | dedendum * cos (pitch_angle) - 149 | gear_thickness / tan (pitch_angle); 150 | back_cone_descent = dedendum * sin (pitch_angle) + gear_thickness; 151 | 152 | // Root diameter: Diameter of bottom of tooth spaces. 153 | root_radius = back_cone_radius - dedendum; 154 | 155 | half_tooth_thickness = outside_pitch_radius * sin (360 / (4 * number_of_teeth)) - backlash / 4; 156 | half_thick_angle = asin (half_tooth_thickness / back_cone_radius); 157 | 158 | face_cone_height = apex_to_apex-face_width / cos (pitch_angle); 159 | face_cone_full_radius = face_cone_height / tan (pitch_angle); 160 | face_cone_descent = dedendum * sin (pitch_angle); 161 | face_cone_end_radius = 162 | outside_pitch_radius - 163 | face_width / sin (pitch_angle) - 164 | face_cone_descent / tan (pitch_angle); 165 | 166 | // For the bevel_gear_flat finish option, calculate the height of a cube to select the portion of the gear that includes the full pitch face. 167 | bevel_gear_flat_height = pitch_apex - (cone_distance - face_width) * cos (pitch_angle); 168 | 169 | // translate([0,0,-pitch_apex]) 170 | difference () 171 | { 172 | intersection () 173 | { 174 | union() 175 | { 176 | rotate (half_thick_angle) 177 | translate ([0,0,pitch_apex-apex_to_apex]) 178 | cylinder ($fn=number_of_teeth*2, r1=root_cone_full_radius,r2=0,h=apex_to_apex); 179 | for (i = [1:number_of_teeth]) 180 | // for (i = [1:1]) 181 | { 182 | rotate ([0,0,i*360/number_of_teeth]) 183 | { 184 | involute_bevel_gear_tooth ( 185 | back_cone_radius = back_cone_radius, 186 | root_radius = root_radius, 187 | base_radius = base_radius, 188 | outer_radius = outer_radius, 189 | pitch_apex = pitch_apex, 190 | cone_distance = cone_distance, 191 | half_thick_angle = half_thick_angle, 192 | involute_facets = involute_facets); 193 | } 194 | } 195 | } 196 | 197 | if (finish == bevel_gear_back_cone) 198 | { 199 | translate ([0,0,-back_cone_descent]) 200 | cylinder ( 201 | $fn=number_of_teeth*2, 202 | r1=back_cone_end_radius, 203 | r2=back_cone_full_radius*2, 204 | h=apex_to_apex + back_cone_descent); 205 | } 206 | else 207 | { 208 | translate ([-1.5*outside_pitch_radius,-1.5*outside_pitch_radius,0]) 209 | cube ([3*outside_pitch_radius, 210 | 3*outside_pitch_radius, 211 | bevel_gear_flat_height]); 212 | } 213 | } 214 | 215 | if (finish == bevel_gear_back_cone) 216 | { 217 | translate ([0,0,-face_cone_descent]) 218 | cylinder ( 219 | r1=face_cone_end_radius, 220 | r2=face_cone_full_radius * 2, 221 | h=face_cone_height + face_cone_descent+pitch_apex); 222 | } 223 | 224 | translate ([0,0,pitch_apex - apex_to_apex]) 225 | cylinder (r=bore_diameter/2,h=apex_to_apex); 226 | } 227 | } 228 | 229 | module involute_bevel_gear_tooth ( 230 | back_cone_radius, 231 | root_radius, 232 | base_radius, 233 | outer_radius, 234 | pitch_apex, 235 | cone_distance, 236 | half_thick_angle, 237 | involute_facets) 238 | { 239 | // echo ("involute_bevel_gear_tooth", 240 | // back_cone_radius, 241 | // root_radius, 242 | // base_radius, 243 | // outer_radius, 244 | // pitch_apex, 245 | // cone_distance, 246 | // half_thick_angle); 247 | 248 | min_radius = max (base_radius*2,root_radius*2); 249 | 250 | pitch_point = 251 | involute ( 252 | base_radius*2, 253 | involute_intersect_angle (base_radius*2, back_cone_radius*2)); 254 | pitch_angle = atan2 (pitch_point[1], pitch_point[0]); 255 | centre_angle = pitch_angle + half_thick_angle; 256 | 257 | start_angle = involute_intersect_angle (base_radius*2, min_radius); 258 | stop_angle = involute_intersect_angle (base_radius*2, outer_radius*2); 259 | 260 | res=(involute_facets!=0)?involute_facets:($fn==0)?5:$fn/4; 261 | 262 | translate ([0,0,pitch_apex]) 263 | rotate ([0,-atan(back_cone_radius/cone_distance),0]) 264 | translate ([-back_cone_radius*2,0,-cone_distance*2]) 265 | union () 266 | { 267 | for (i=[1:res]) 268 | { 269 | assign ( 270 | point1= 271 | involute (base_radius*2,start_angle+(stop_angle - start_angle)*(i-1)/res), 272 | point2= 273 | involute (base_radius*2,start_angle+(stop_angle - start_angle)*(i)/res)) 274 | { 275 | assign ( 276 | side1_point1 = rotate_point (centre_angle, point1), 277 | side1_point2 = rotate_point (centre_angle, point2), 278 | side2_point1 = mirror_point (rotate_point (centre_angle, point1)), 279 | side2_point2 = mirror_point (rotate_point (centre_angle, point2))) 280 | { 281 | polyhedron ( 282 | points=[ 283 | [back_cone_radius*2+0.1,0,cone_distance*2], 284 | [side1_point1[0],side1_point1[1],0], 285 | [side1_point2[0],side1_point2[1],0], 286 | [side2_point2[0],side2_point2[1],0], 287 | [side2_point1[0],side2_point1[1],0], 288 | [0.1,0,0]], 289 | triangles=[[0,2,1],[0,3,2],[0,4,3],[0,1,5],[1,2,5],[2,3,5],[3,4,5],[0,5,4]]); 290 | } 291 | } 292 | } 293 | } 294 | } 295 | 296 | module gear ( 297 | number_of_teeth=15, 298 | circular_pitch=false, diametral_pitch=false, 299 | pressure_angle=28, 300 | clearance = 0.2, 301 | gear_thickness=5, 302 | rim_thickness=8, 303 | rim_width=5, 304 | hub_thickness=10, 305 | hub_diameter=15, 306 | bore_diameter=5, 307 | circles=0, 308 | backlash=0, 309 | twist=0, 310 | involute_facets=0, 311 | flat=false) 312 | { 313 | if (circular_pitch==false && diametral_pitch==false) 314 | echo("MCAD ERROR: gear module needs either a diametral_pitch or circular_pitch"); 315 | 316 | //Convert diametrial pitch to our native circular pitch 317 | circular_pitch = (circular_pitch!=false?circular_pitch:180/diametral_pitch); 318 | 319 | // Pitch diameter: Diameter of pitch circle. 320 | pitch_diameter = number_of_teeth * circular_pitch / 180; 321 | pitch_radius = pitch_diameter/2; 322 | echo ("Teeth:", number_of_teeth, " Pitch radius:", pitch_radius); 323 | 324 | // Base Circle 325 | base_radius = pitch_radius*cos(pressure_angle); 326 | 327 | // Diametrial pitch: Number of teeth per unit length. 328 | pitch_diametrial = number_of_teeth / pitch_diameter; 329 | 330 | // Addendum: Radial distance from pitch circle to outside circle. 331 | addendum = 1/pitch_diametrial; 332 | 333 | //Outer Circle 334 | outer_radius = pitch_radius+addendum; 335 | 336 | // Dedendum: Radial distance from pitch circle to root diameter 337 | dedendum = addendum + clearance; 338 | 339 | // Root diameter: Diameter of bottom of tooth spaces. 340 | root_radius = pitch_radius-dedendum; 341 | backlash_angle = backlash / pitch_radius * 180 / pi; 342 | half_thick_angle = (360 / number_of_teeth - backlash_angle) / 4; 343 | 344 | // Variables controlling the rim. 345 | rim_radius = root_radius - rim_width; 346 | 347 | // Variables controlling the circular holes in the gear. 348 | circle_orbit_diameter=hub_diameter/2+rim_radius; 349 | circle_orbit_curcumference=pi*circle_orbit_diameter; 350 | 351 | // Limit the circle size to 90% of the gear face. 352 | circle_diameter= 353 | min ( 354 | 0.70*circle_orbit_curcumference/circles, 355 | (rim_radius-hub_diameter/2)*0.9); 356 | 357 | difference() 358 | { 359 | union () 360 | { 361 | difference () 362 | { 363 | linear_exturde_flat_option(flat=flat, height=rim_thickness, convexity=10, twist=twist) 364 | gear_shape ( 365 | number_of_teeth, 366 | pitch_radius = pitch_radius, 367 | root_radius = root_radius, 368 | base_radius = base_radius, 369 | outer_radius = outer_radius, 370 | half_thick_angle = half_thick_angle, 371 | involute_facets=involute_facets); 372 | 373 | if (gear_thickness < rim_thickness) 374 | translate ([0,0,gear_thickness]) 375 | cylinder (r=rim_radius,h=rim_thickness-gear_thickness+1); 376 | } 377 | if (gear_thickness > rim_thickness) 378 | linear_exturde_flat_option(flat=flat, height=gear_thickness) 379 | circle (r=rim_radius); 380 | if (flat == false && hub_thickness > gear_thickness) 381 | translate ([0,0,gear_thickness]) 382 | linear_exturde_flat_option(flat=flat, height=hub_thickness-gear_thickness) 383 | circle (r=hub_diameter/2); 384 | } 385 | translate ([0,0,-1]) 386 | linear_exturde_flat_option(flat =flat, height=2+max(rim_thickness,hub_thickness,gear_thickness)) 387 | circle (r=bore_diameter/2); 388 | if (circles>0) 389 | { 390 | for(i=[0:circles-1]) 391 | rotate([0,0,i*360/circles]) 392 | translate([circle_orbit_diameter/2,0,-1]) 393 | linear_exturde_flat_option(flat =flat, height=max(gear_thickness,rim_thickness)+3) 394 | circle(r=circle_diameter/2); 395 | } 396 | } 397 | } 398 | 399 | module linear_exturde_flat_option(flat =false, height = 10, center = false, convexity = 2, twist = 0) 400 | { 401 | if(flat==false) 402 | { 403 | linear_extrude(height = height, center = center, convexity = convexity, twist= twist) child(0); 404 | } 405 | else 406 | { 407 | child(0); 408 | } 409 | 410 | } 411 | 412 | module gear_shape ( 413 | number_of_teeth, 414 | pitch_radius, 415 | root_radius, 416 | base_radius, 417 | outer_radius, 418 | half_thick_angle, 419 | involute_facets) 420 | { 421 | union() 422 | { 423 | rotate (half_thick_angle) circle ($fn=number_of_teeth*2, r=root_radius); 424 | 425 | for (i = [1:number_of_teeth]) 426 | { 427 | rotate ([0,0,i*360/number_of_teeth]) 428 | { 429 | involute_gear_tooth ( 430 | pitch_radius = pitch_radius, 431 | root_radius = root_radius, 432 | base_radius = base_radius, 433 | outer_radius = outer_radius, 434 | half_thick_angle = half_thick_angle, 435 | involute_facets=involute_facets); 436 | } 437 | } 438 | } 439 | } 440 | 441 | module involute_gear_tooth ( 442 | pitch_radius, 443 | root_radius, 444 | base_radius, 445 | outer_radius, 446 | half_thick_angle, 447 | involute_facets) 448 | { 449 | min_radius = max (base_radius,root_radius); 450 | 451 | pitch_point = involute (base_radius, involute_intersect_angle (base_radius, pitch_radius)); 452 | pitch_angle = atan2 (pitch_point[1], pitch_point[0]); 453 | centre_angle = pitch_angle + half_thick_angle; 454 | 455 | start_angle = involute_intersect_angle (base_radius, min_radius); 456 | stop_angle = involute_intersect_angle (base_radius, outer_radius); 457 | 458 | res=(involute_facets!=0)?involute_facets:($fn==0)?5:$fn/4; 459 | 460 | union () 461 | { 462 | for (i=[1:res]) 463 | assign ( 464 | point1=involute (base_radius,start_angle+(stop_angle - start_angle)*(i-1)/res), 465 | point2=involute (base_radius,start_angle+(stop_angle - start_angle)*i/res)) 466 | { 467 | assign ( 468 | side1_point1=rotate_point (centre_angle, point1), 469 | side1_point2=rotate_point (centre_angle, point2), 470 | side2_point1=mirror_point (rotate_point (centre_angle, point1)), 471 | side2_point2=mirror_point (rotate_point (centre_angle, point2))) 472 | { 473 | polygon ( 474 | points=[[0,0],side1_point1,side1_point2,side2_point2,side2_point1], 475 | paths=[[0,1,2,3,4,0]]); 476 | } 477 | } 478 | } 479 | } 480 | 481 | // Mathematical Functions 482 | //=============== 483 | 484 | // Finds the angle of the involute about the base radius at the given distance (radius) from it's center. 485 | //source: http://www.mathhelpforum.com/math-help/geometry/136011-circle-involute-solving-y-any-given-x.html 486 | 487 | function involute_intersect_angle (base_radius, radius) = sqrt (pow (radius/base_radius, 2) - 1) * 180 / pi; 488 | 489 | // Calculate the involute position for a given base radius and involute angle. 490 | 491 | function rotated_involute (rotate, base_radius, involute_angle) = 492 | [ 493 | cos (rotate) * involute (base_radius, involute_angle)[0] + sin (rotate) * involute (base_radius, involute_angle)[1], 494 | cos (rotate) * involute (base_radius, involute_angle)[1] - sin (rotate) * involute (base_radius, involute_angle)[0] 495 | ]; 496 | 497 | function mirror_point (coord) = 498 | [ 499 | coord[0], 500 | -coord[1] 501 | ]; 502 | 503 | function rotate_point (rotate, coord) = 504 | [ 505 | cos (rotate) * coord[0] + sin (rotate) * coord[1], 506 | cos (rotate) * coord[1] - sin (rotate) * coord[0] 507 | ]; 508 | 509 | function involute (base_radius, involute_angle) = 510 | [ 511 | base_radius*(cos (involute_angle) + involute_angle*pi/180*sin (involute_angle)), 512 | base_radius*(sin (involute_angle) - involute_angle*pi/180*cos (involute_angle)) 513 | ]; 514 | 515 | 516 | // Test Cases 517 | //=============== 518 | 519 | module test_gears() 520 | { 521 | translate([17,-15]) 522 | { 523 | gear (number_of_teeth=17, 524 | circular_pitch=500, 525 | circles=8); 526 | 527 | rotate ([0,0,360*4/17]) 528 | translate ([39.088888,0,0]) 529 | { 530 | gear (number_of_teeth=11, 531 | circular_pitch=500, 532 | hub_diameter=0, 533 | rim_width=65); 534 | translate ([0,0,8]) 535 | { 536 | gear (number_of_teeth=6, 537 | circular_pitch=300, 538 | hub_diameter=0, 539 | rim_width=5, 540 | rim_thickness=6, 541 | pressure_angle=31); 542 | rotate ([0,0,360*5/6]) 543 | translate ([22.5,0,1]) 544 | gear (number_of_teeth=21, 545 | circular_pitch=300, 546 | bore_diameter=2, 547 | hub_diameter=4, 548 | rim_width=1, 549 | hub_thickness=4, 550 | rim_thickness=4, 551 | gear_thickness=3, 552 | pressure_angle=31); 553 | } 554 | } 555 | 556 | translate ([-61.1111111,0,0]) 557 | { 558 | gear (number_of_teeth=27, 559 | circular_pitch=500, 560 | circles=5, 561 | hub_diameter=2*8.88888889); 562 | 563 | translate ([0,0,10]) 564 | { 565 | gear ( 566 | number_of_teeth=14, 567 | circular_pitch=200, 568 | pressure_angle=5, 569 | clearance = 0.2, 570 | gear_thickness = 10, 571 | rim_thickness = 10, 572 | rim_width = 15, 573 | bore_diameter=5, 574 | circles=0); 575 | translate ([13.8888888,0,1]) 576 | gear ( 577 | number_of_teeth=11, 578 | circular_pitch=200, 579 | pressure_angle=5, 580 | clearance = 0.2, 581 | gear_thickness = 10, 582 | rim_thickness = 10, 583 | rim_width = 15, 584 | hub_thickness = 20, 585 | hub_diameter=2*7.222222, 586 | bore_diameter=5, 587 | circles=0); 588 | } 589 | } 590 | 591 | rotate ([0,0,360*-5/17]) 592 | translate ([44.444444444,0,0]) 593 | gear (number_of_teeth=15, 594 | circular_pitch=500, 595 | hub_diameter=10, 596 | rim_width=5, 597 | rim_thickness=5, 598 | gear_thickness=4, 599 | hub_thickness=6, 600 | circles=9); 601 | 602 | rotate ([0,0,360*-1/17]) 603 | translate ([30.5555555,0,-1]) 604 | gear (number_of_teeth=5, 605 | circular_pitch=500, 606 | hub_diameter=0, 607 | rim_width=5, 608 | rim_thickness=10); 609 | } 610 | } 611 | 612 | module meshing_double_helix () 613 | { 614 | test_double_helix_gear (); 615 | 616 | mirror ([0,1,0]) 617 | translate ([58.33333333,0,0]) 618 | test_double_helix_gear (teeth=13,circles=6); 619 | } 620 | 621 | module test_double_helix_gear ( 622 | teeth=17, 623 | circles=8, pitch=700, hub_thickness=10) 624 | { 625 | //double helical gear 626 | { 627 | twist=150; 628 | height=10; 629 | pressure_angle=20; 630 | 631 | gear (number_of_teeth=teeth, 632 | circular_pitch=pitch, 633 | pressure_angle=pressure_angle, 634 | clearance = 0.2, 635 | gear_thickness = height/2, 636 | rim_thickness = height/2, 637 | rim_width = 5, 638 | hub_thickness = hub_thickness, 639 | hub_diameter=15, 640 | bore_diameter=5, 641 | circles=circles, 642 | twist=twist/teeth); 643 | mirror([0,0,1]) 644 | gear (number_of_teeth=teeth, 645 | circular_pitch=pitch, 646 | pressure_angle=pressure_angle, 647 | clearance = 0.2, 648 | gear_thickness = height/2, 649 | rim_thickness = height/2, 650 | rim_width = 5, 651 | hub_thickness = height/2, 652 | hub_diameter=15, 653 | bore_diameter=5, 654 | circles=circles, 655 | twist=twist/teeth); 656 | } 657 | } 658 | 659 | module test_backlash () 660 | { 661 | backlash = 2; 662 | teeth = 15; 663 | 664 | translate ([-29.166666,0,0]) 665 | { 666 | translate ([58.3333333,0,0]) 667 | rotate ([0,0,-360/teeth/4]) 668 | gear ( 669 | number_of_teeth = teeth, 670 | circular_pitch=700, 671 | gear_thickness = 12, 672 | rim_thickness = 15, 673 | rim_width = 5, 674 | hub_thickness = 17, 675 | hub_diameter=15, 676 | bore_diameter=5, 677 | backlash = 2, 678 | circles=8); 679 | 680 | rotate ([0,0,360/teeth/4]) 681 | gear ( 682 | number_of_teeth = teeth, 683 | circular_pitch=700, 684 | gear_thickness = 12, 685 | rim_thickness = 15, 686 | rim_width = 5, 687 | hub_thickness = 17, 688 | hub_diameter=15, 689 | bore_diameter=5, 690 | backlash = 2, 691 | circles=8); 692 | } 693 | 694 | color([0,0,128,0.5]) 695 | translate([0,0,-5]) 696 | cylinder ($fn=20,r=backlash / 4,h=25); 697 | } 698 | 699 | -------------------------------------------------------------------------------- /kiln/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('