├── public ├── assets │ ├── css │ │ ├── select2.png │ │ ├── select2x2.png │ │ ├── select2-spinner.gif │ │ ├── select2-bootstrap.css │ │ ├── bootstrap-modal.css │ │ ├── picoreflow.css │ │ ├── bootstrap-theme.min.css │ │ └── select2.css │ ├── fonts │ │ ├── tables.eot │ │ ├── tables.ttf │ │ ├── tables.woff │ │ ├── digital-7-webfont.eot │ │ ├── digital-7-webfont.ttf │ │ ├── digital-7-webfont.woff │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── digital-7-webfont.svg │ ├── images │ │ ├── page_bg.png │ │ └── panel_bg.png │ └── js │ │ ├── jquery.event.drag-2.2.js │ │ ├── jquery.bootstrap-growl.min.js │ │ ├── jquery.flot.resize.js │ │ ├── jquery.flot.draggable.js │ │ ├── picoreflow.js │ │ └── bootstrap.min.js └── index.html ├── storage └── profiles │ ├── lead.json │ └── leadfree.json ├── .gitignore ├── lib ├── init │ └── reflow ├── max31855spi.py ├── ovenWatcher.py ├── max6675.py ├── max31855.py └── oven.py ├── config.py.EXAMPLE ├── README.md └── picoreflowd.py /public/assets/css/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/css/select2.png -------------------------------------------------------------------------------- /public/assets/css/select2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/css/select2x2.png -------------------------------------------------------------------------------- /public/assets/fonts/tables.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/fonts/tables.eot -------------------------------------------------------------------------------- /public/assets/fonts/tables.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/fonts/tables.ttf -------------------------------------------------------------------------------- /public/assets/fonts/tables.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/fonts/tables.woff -------------------------------------------------------------------------------- /public/assets/images/page_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/images/page_bg.png -------------------------------------------------------------------------------- /public/assets/images/panel_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/images/panel_bg.png -------------------------------------------------------------------------------- /storage/profiles/lead.json: -------------------------------------------------------------------------------- 1 | {"type": "profile", "data": [[0, 20], [60, 100], [180, 82], [225, 176]], "name": "lead"} 2 | -------------------------------------------------------------------------------- /public/assets/css/select2-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/css/select2-spinner.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | \#* 4 | .#* 5 | *.swp 6 | .DStore/ 7 | thumbs.db 8 | storage/profiles 9 | config.py 10 | .idea/* 11 | -------------------------------------------------------------------------------- /public/assets/fonts/digital-7-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/fonts/digital-7-webfont.eot -------------------------------------------------------------------------------- /public/assets/fonts/digital-7-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/fonts/digital-7-webfont.ttf -------------------------------------------------------------------------------- /public/assets/js/jquery.event.drag-2.2.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/js/jquery.event.drag-2.2.js -------------------------------------------------------------------------------- /public/assets/fonts/digital-7-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/fonts/digital-7-webfont.woff -------------------------------------------------------------------------------- /storage/profiles/leadfree.json: -------------------------------------------------------------------------------- 1 | {"type": "profile", "data": [[0, 25], [90, 150], [180, 183], [211, 237], [234, 184], [313, 26]], "name": "leadfree"} -------------------------------------------------------------------------------- /public/assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-ng/picoReflow/HEAD/public/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /lib/init/reflow: -------------------------------------------------------------------------------- 1 | ### BEGIN INIT INFO 2 | # Provides: Start reflow server 3 | # Required-Start: $remote_fs $syslog 4 | # Required-Stop: $remote_fs $syslog 5 | # Default-Start: 2 3 4 5 6 | # Default-Stop: 0 1 6 7 | # Short-Description: Start Reflow Server 8 | # Description: picoFlow On Raspberry Pi 9 | ### END INIT INFO 10 | 11 | 12 | #! /bin/sh 13 | # /etc/init.d/reflow 14 | 15 | 16 | export HOME 17 | case "$1" in 18 | start) 19 | echo "Starting Reflow Server" 20 | python3 /home/pi/picoReflow/picoreflowd.py 2>&1 & 21 | ;; 22 | stop) 23 | echo "Stopping Reflow Server" 24 | reflow_PID=`ps auxwww | grep picoreflowd.py | head -1 | awk '{print $2}'` 25 | kill -9 $reflow_PID 26 | ;; 27 | *) 28 | echo "Usage: /etc/init.d/reflow {start|stop}" 29 | exit 1 30 | ;; 31 | esac 32 | exit 0 33 | -------------------------------------------------------------------------------- /public/assets/js/jquery.bootstrap-growl.min.js: -------------------------------------------------------------------------------- 1 | (function(){var t;t=jQuery,t.bootstrapGrowl=function(e,s){var a,o,l;switch(s=t.extend({},t.bootstrapGrowl.default_options,s),a=t("
"),a.attr("class","bootstrap-growl alert"),s.type&&a.addClass("alert-"+s.type),s.allow_dismiss&&a.append('×'),a.append(e),s.top_offset&&(s.offset={from:"top",amount:s.top_offset}),l=s.offset.amount,t(".bootstrap-growl").each(function(){return l=Math.max(l,parseInt(t(this).css(s.offset.from))+t(this).outerHeight()+s.stackup_spacing)}),o={position:"body"===s.ele?"fixed":"absolute",margin:0,"z-index":"9999",display:"none"},o[s.offset.from]=l+"px",a.css(o),"auto"!==s.width&&a.css("width",s.width+"px"),t(s.ele).append(a),s.align){case"center":a.css({left:"50%","margin-left":"-"+a.outerWidth()/2+"px"});break;case"left":a.css("left","20px");break;default:a.css("right","20px")}return a.fadeIn(),s.delay>0&&a.delay(s.delay).fadeOut(function(){return t(this).alert("close")}),a},t.bootstrapGrowl.default_options={ele:"body",type:"info",offset:{from:"top",amount:20},align:"right",width:250,delay:4e3,allow_dismiss:!0,stackup_spacing:10}}).call(this); 2 | -------------------------------------------------------------------------------- /lib/max31855spi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import logging 3 | 4 | from Adafruit_MAX31855 import MAX31855 5 | 6 | class MAX31855SPI(object): 7 | '''Python driver for [MAX38155 Cold-Junction Compensated Thermocouple-to-Digital Converter](http://www.maximintegrated.com/datasheet/index.mvp/id/7273) 8 | Requires: 9 | - adafruit's MAX31855 SPI-only device library 10 | 11 | ''' 12 | def __init__(self, spi_dev): 13 | self.max31855 = MAX31855.MAX31855(spi=spi_dev) 14 | self.log = logging.getLogger(__name__) 15 | 16 | def get(self): 17 | '''Reads SPI bus and returns current value of thermocouple.''' 18 | state = self.max31855.readState() 19 | self.log.debug("status %s" % state) 20 | if state['openCircuit']: 21 | raise MAX31855Error('Not Connected') 22 | elif state['shortGND']: 23 | raise MAX31855Error('Short to Ground') 24 | elif state['shortVCC']: 25 | raise MAX31855Error('Short to VCC') 26 | elif state['fault']: 27 | raise MAX31855Error('Unknown Error') 28 | return self.max31855.readLinearizedTempC() 29 | 30 | 31 | class MAX31855SPIError(Exception): 32 | def __init__(self, value): 33 | self.value = value 34 | 35 | def __str__(self): 36 | return repr(self.value) 37 | -------------------------------------------------------------------------------- /lib/ovenWatcher.py: -------------------------------------------------------------------------------- 1 | import threading,logging,json,time,datetime 2 | from oven import Oven 3 | log = logging.getLogger(__name__) 4 | 5 | class OvenWatcher(threading.Thread): 6 | def __init__(self,oven): 7 | self.last_profile = None 8 | self.last_log = [] 9 | self.started = None 10 | self.recording = False 11 | self.observers = [] 12 | threading.Thread.__init__(self) 13 | self.daemon = True 14 | self.log_skip_counter = 0 15 | 16 | self.oven = oven 17 | self.start() 18 | 19 | def run(self): 20 | while True: 21 | oven_state = self.oven.get_state() 22 | 23 | if oven_state.get("state") == Oven.STATE_RUNNING: 24 | if self.log_skip_counter==0: 25 | self.last_log.append(oven_state) 26 | else: 27 | self.recording = False 28 | self.notify_all(oven_state) 29 | self.log_skip_counter = (self.log_skip_counter +1)%20 30 | time.sleep(self.oven.time_step) 31 | 32 | def record(self, profile): 33 | self.last_profile = profile 34 | self.last_log = [] 35 | self.started = datetime.datetime.now() 36 | self.recording = True 37 | #we just turned on, add first state for nice graph 38 | self.last_log.append(self.oven.get_state()) 39 | 40 | def add_observer(self,observer): 41 | if self.last_profile: 42 | p = { 43 | "name": self.last_profile.name, 44 | "data": self.last_profile.data, 45 | "type" : "profile" 46 | } 47 | else: 48 | p = None 49 | 50 | backlog = { 51 | 'type': "backlog", 52 | 'profile': p, 53 | 'log': self.last_log, 54 | #'started': self.started 55 | } 56 | print(backlog) 57 | backlog_json = json.dumps(backlog) 58 | try: 59 | print(backlog_json) 60 | observer.send(backlog_json) 61 | except: 62 | log.error("Could not send backlog to new observer") 63 | 64 | self.observers.append(observer) 65 | 66 | def notify_all(self,message): 67 | message_json = json.dumps(message) 68 | log.debug("sending to %d clients: %s"%(len(self.observers),message_json)) 69 | for wsock in self.observers: 70 | if wsock: 71 | try: 72 | wsock.send(message_json) 73 | except: 74 | log.error("could not write to socket %s"%wsock) 75 | self.observers.remove(wsock) 76 | else: 77 | self.observers.remove(wsock) 78 | -------------------------------------------------------------------------------- /config.py.EXAMPLE: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | ######################################################################## 4 | # 5 | # General options 6 | 7 | ### Logging 8 | log_level = logging.INFO 9 | log_format = '%(asctime)s %(levelname)s %(name)s: %(message)s' 10 | 11 | ### Server 12 | listening_ip = "0.0.0.0" 13 | listening_port = 8081 14 | 15 | ### Cost Estimate 16 | kwh_rate = 0.26 # Rate in currency_type to calculate cost to run job 17 | currency_type = "EUR" # Currency Symbol to show when calculating cost to run job 18 | 19 | ######################################################################## 20 | # 21 | # GPIO Setup (BCM SoC Numbering Schema) 22 | # 23 | # Check the RasPi docs to see where these GPIOs are 24 | # connected on the P1 header for your board type/rev. 25 | # These were tested on a Pi B Rev2 but of course you 26 | # can use whichever GPIO you prefer/have available. 27 | 28 | ### Outputs 29 | gpio_heat = 11 # Switches zero-cross solid-state-relay 30 | gpio_cool = 10 # Regulates PWM for 12V DC Blower 31 | gpio_air = 9 # Switches 0-phase det. solid-state-relay 32 | 33 | heater_invert = 0 # switches the polarity of the heater control 34 | 35 | ### Inputs 36 | gpio_door = 18 37 | 38 | ### Thermocouple Adapter selection: 39 | # max31855 - bitbang SPI interface 40 | # max31855spi - kernel SPI interface 41 | # max6675 - bitbang SPI interface 42 | max31855 = 1 43 | max6675 = 0 44 | max31855spi = 0 # if you use this one, you MUST reassign the default GPIO pins 45 | 46 | ### Thermocouple Connection (using bitbang interfaces) 47 | gpio_sensor_cs = 27 48 | gpio_sensor_clock = 22 49 | gpio_sensor_data = 17 50 | 51 | ### Thermocouple SPI Connection (using adafrut drivers + kernel SPI interface) 52 | spi_sensor_chip_id = 0 53 | 54 | ### amount of time, in seconds, to wait between reads of the thermocouple 55 | sensor_time_wait = .5 56 | 57 | 58 | ######################################################################## 59 | # 60 | # PID parameters 61 | 62 | pid_ki = 0.1 # Integration 63 | pid_kd = 0.4 # Derivative 64 | pid_kp = 0.5 # Proportional 65 | 66 | 67 | ######################################################################## 68 | # 69 | # Simulation parameters 70 | 71 | sim_t_env = 25.0 # deg C 72 | sim_c_heat = 100.0 # J/K heat capacity of heat element 73 | sim_c_oven = 2000.0 # J/K heat capacity of oven 74 | sim_p_heat = 3500.0 # W heating power of oven 75 | sim_R_o_nocool = 1.0 # K/W thermal resistance oven -> environment 76 | sim_R_o_cool = 0.05 # K/W " with cooling 77 | sim_R_ho_noair = 0.1 # K/W thermal resistance heat element -> oven 78 | sim_R_ho_air = 0.05 # K/W " with internal air circulation 79 | 80 | 81 | ######################################################################## 82 | # 83 | # Time and Temperature parameters 84 | 85 | temp_scale = "c" # c = Celsius | f = Fahrenheit - Unit to display 86 | time_scale_slope = "s" # s = Seconds | m = Minutes | h = Hours - Slope displayed in temp_scale per time_scale_slope 87 | time_scale_profile = "s" # s = Seconds | m = Minutes | h = Hours - Enter and view target time in time_scale_profile 88 | 89 | -------------------------------------------------------------------------------- /public/assets/css/select2-bootstrap.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Select2 Bootstrap CSS 1.0 3 | * Compatible with select2 3.3.2 and bootstrap 2.3.1 4 | * MIT License 5 | */ 6 | .select2-container { 7 | vertical-align: middle; 8 | } 9 | .select2-container.input-mini { 10 | width: 60px; 11 | } 12 | .select2-container.input-small { 13 | width: 90px; 14 | } 15 | .select2-container.input-medium { 16 | width: 150px; 17 | } 18 | .select2-container.input-large { 19 | width: 210px; 20 | } 21 | .select2-container.input-xlarge { 22 | width: 270px; 23 | } 24 | .select2-container.input-xxlarge { 25 | width: 530px; 26 | } 27 | .select2-container.input-default { 28 | width: 220px; 29 | } 30 | .select2-container[class*="span"] { 31 | float: none; 32 | margin-left: 0; 33 | } 34 | 35 | .select2-container .select2-choice, 36 | .select2-container-multi .select2-choices { 37 | height: 28px; 38 | line-height: 29px; 39 | border: 1px solid #cccccc; 40 | -webkit-border-radius: 4px; 41 | -moz-border-radius: 4px; 42 | border-radius: 4px; 43 | background: none; 44 | background-color: white; 45 | filter: none; 46 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 47 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 48 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 49 | } 50 | 51 | .select2-container .select2-choice div, 52 | .select2-container.select2-container-disabled .select2-choice div { 53 | border-left: none; 54 | background: none; 55 | filter: none; 56 | } 57 | 58 | .control-group.error [class^="select2-choice"] { 59 | border-color: #b94a48; 60 | } 61 | 62 | .select2-container-multi .select2-choices .select2-search-field { 63 | height: 28px; 64 | line-height: 27px; 65 | } 66 | 67 | .select2-container-active .select2-choice, 68 | .select2-container-multi.select2-container-active .select2-choices { 69 | border-color: rgba(82, 168, 236, 0.8); 70 | outline: none; 71 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); 72 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); 73 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); 74 | } 75 | 76 | [class^="input-"] .select2-container { 77 | font-size: 14px; 78 | } 79 | 80 | .input-prepend [class^="select2-choice"] { 81 | border-top-left-radius: 0; 82 | border-bottom-left-radius: 0; 83 | } 84 | 85 | .input-append [class^="select2-choice"] { 86 | border-top-right-radius: 0; 87 | border-bottom-right-radius: 0; 88 | } 89 | 90 | .select2-dropdown-open [class^="select2-choice"] { 91 | border-bottom-left-radius: 0; 92 | border-bottom-right-radius: 0; 93 | } 94 | 95 | .select2-dropdown-open.select2-drop-above [class^="select2-choice"] { 96 | border-top-left-radius: 0; 97 | border-top-right-radius: 0; 98 | } 99 | 100 | [class^="input-"] .select2-offscreen { 101 | position: absolute; 102 | } 103 | 104 | /** 105 | * This stops the quick flash when a native selectbox is shown and 106 | * then replaced by a select2 input when javascript kicks in. This can be 107 | * removed if javascript is not present 108 | */ 109 | select.select2 { 110 | height: 28px; 111 | visibility: hidden; 112 | } 113 | -------------------------------------------------------------------------------- /public/assets/js/jquery.flot.resize.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for automatically redrawing plots as the placeholder resizes. 2 | 3 | Copyright (c) 2007-2013 IOLA and Ole Laursen. 4 | Licensed under the MIT license. 5 | 6 | It works by listening for changes on the placeholder div (through the jQuery 7 | resize event plugin) - if the size changes, it will redraw the plot. 8 | 9 | There are no options. If you need to disable the plugin for some plots, you 10 | can just fix the size of their placeholders. 11 | 12 | */ 13 | 14 | /* Inline dependency: 15 | * jQuery resize event - v1.1 - 3/14/2010 16 | * http://benalman.com/projects/jquery-resize-plugin/ 17 | * 18 | * Copyright (c) 2010 "Cowboy" Ben Alman 19 | * Dual licensed under the MIT and GPL licenses. 20 | * http://benalman.com/about/license/ 21 | */ 22 | 23 | (function($,t,n){function p(){for(var n=r.length-1;n>=0;n--){var o=$(r[n]);if(o[0]==t||o.is(":visible")){var h=o.width(),d=o.height(),v=o.data(a);!v||h===v.w&&d===v.h?i[f]=i[l]:(i[f]=i[c],o.trigger(u,[v.w=h,v.h=d]))}else v=o.data(a),v.w=0,v.h=0}s!==null&&(s=t.requestAnimationFrame(p))}var r=[],i=$.resize=$.extend($.resize,{}),s,o="setTimeout",u="resize",a=u+"-special-event",f="delay",l="pendingDelay",c="activeDelay",h="throttleWindow";i[l]=250,i[c]=20,i[f]=i[l],i[h]=!0,$.event.special[u]={setup:function(){if(!i[h]&&this[o])return!1;var t=$(this);r.push(this),t.data(a,{w:t.width(),h:t.height()}),r.length===1&&(s=n,p())},teardown:function(){if(!i[h]&&this[o])return!1;var t=$(this);for(var n=r.length-1;n>=0;n--)if(r[n]==this){r.splice(n,1);break}t.removeData(a),r.length||(cancelAnimationFrame(s),s=null)},add:function(t){function s(t,i,s){var o=$(this),u=o.data(a);u.w=i!==n?i:o.width(),u.h=s!==n?s:o.height(),r.apply(this,arguments)}if(!i[h]&&this[o])return!1;var r;if($.isFunction(t))return r=t,s;r=t.handler,t.handler=s}},t.requestAnimationFrame||(t.requestAnimationFrame=function(){return t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||t.msRequestAnimationFrame||function(e,n){return t.setTimeout(e,i[f])}}()),t.cancelAnimationFrame||(t.cancelAnimationFrame=function(){return t.webkitCancelRequestAnimationFrame||t.mozCancelRequestAnimationFrame||t.oCancelRequestAnimationFrame||t.msCancelRequestAnimationFrame||clearTimeout}())})(jQuery,this); 24 | 25 | (function ($) { 26 | var options = { }; // no options 27 | 28 | function init(plot) { 29 | function onResize() { 30 | var placeholder = plot.getPlaceholder(); 31 | 32 | // somebody might have hidden us and we can't plot 33 | // when we don't have the dimensions 34 | if (placeholder.width() == 0 || placeholder.height() == 0) 35 | return; 36 | 37 | plot.resize(); 38 | plot.setupGrid(); 39 | plot.draw(); 40 | } 41 | 42 | function bindEvents(plot, eventHolder) { 43 | plot.getPlaceholder().resize(onResize); 44 | } 45 | 46 | function shutdown(plot, eventHolder) { 47 | plot.getPlaceholder().unbind("resize", onResize); 48 | } 49 | 50 | plot.hooks.bindEvents.push(bindEvents); 51 | plot.hooks.shutdown.push(shutdown); 52 | } 53 | 54 | $.plot.plugins.push({ 55 | init: init, 56 | options: options, 57 | name: 'resize', 58 | version: '1.0' 59 | }); 60 | })(jQuery); 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | picoReflow 2 | ========== 3 | 4 | Turns a Raspberry Pi into a cheap, universal & web-enabled Reflow Oven Controller. 5 | Of course, since it is basically just a robot sensing temperature and controlling 6 | environmental agitators (heating/cooling) you can use it as inspiration / basis 7 | when you're in need of a PID based temperature controller for your project. 8 | Don't forget to share and drop a link, when you do :) 9 | 10 | **Standard Interface** 11 | 12 | ![Image](https://apollo.open-resource.org/_media/mission:resources:picoreflow_webinterface.jpg) 13 | 14 | **Curve Editor** 15 | 16 | ![Image](https://apollo.open-resource.org/_media/mission:resources:picoreflow_webinterface_edit.jpg) 17 | 18 | ## Hardware 19 | 20 | * Raspberry Pi (Rev 2B, Zero W) 21 | * MAX 31855/6675 Cold-Junction K-Type Thermocouple 22 | * GPIO driven Solid-State-Relays/MOSFETs 23 | 24 | ## Installation 25 | 26 | ### Dependencies 27 | 28 | We've tried to keep external dependencies to a minimum to make it easily 29 | deployable on any flavor of open-source operating system. If you deploy it 30 | successfully on any other OS, please update this: 31 | 32 | #### Currently tested versions 33 | 34 | * greenlet-0.4.2 35 | * bottle-0.12.4 36 | * gevent-1.0 37 | * gevent-websocket-0.9.3 38 | 39 | #### Ubuntu 40 | 41 | $ sudo apt-get install python3-pip python-dev libevent-dev 42 | $ sudo pip3 install ez-setup 43 | $ sudo pip3 install greenlet bottle gevent gevent-websocket 44 | 45 | #### Raspbian 46 | 47 | $ sudo apt-get install python3-pip python-dev libevent-dev 48 | $ sudo pip3 install ez-setup 49 | $ sudo apt-get install python-gevent python-gevent-websocket 50 | $ sudo pip3 install greenlet bottle 51 | 52 | #### Gentoo 53 | 54 | $ emerge -av dev-libs/libevent dev-python/pip 55 | $ pip install ez-setup 56 | $ pip install greenlet bottle gevent gevent-websocket 57 | 58 | #### Raspberry PI deployment 59 | 60 | If you want to deploy the code on a PI for production: 61 | 62 | $ pip3 install RPi.GPIO 63 | 64 | This **only applies to non-Raspbian installations**, since Raspbian ships 65 | RPi.GPIO with the default installation. 66 | 67 | If you also want to use the in-kernel SPI drivers with a MAX31855 sensor: 68 | 69 | $ sudo pip3 install Adafruit-MAX31855 70 | 71 | ### Clone repo 72 | 73 | $ git clone https://github.com/apollo-ng/picoReflow.git 74 | $ cd picoReflow 75 | 76 | ## Configuration 77 | 78 | All parameters are defined in config.py, just copy the example and review/change to your mind's content. 79 | 80 | $ cp config.py.EXAMPLE config.py 81 | 82 | ## Usage 83 | 84 | ### Server Startup 85 | 86 | $ python3 picoreflowd.py 87 | 88 | ### Autostart Server onBoot 89 | If you want the server to autostart on boot, run the following commands 90 | 91 | sudo cp /home/pi/picoReflow/lib/init/reflow /etc/init.d/ 92 | sudo chmod +x /etc/init.d/reflow 93 | sudo update-rc.d reflow defaults 94 | 95 | ### Client Access 96 | 97 | Open Browser and goto http://127.0.0.1:8081 (for local development) or the IP 98 | of your PI and the port defined in config.py (default 8081). 99 | 100 | ## License 101 | 102 | This program is free software: you can redistribute it and/or modify 103 | it under the terms of the GNU General Public License as published by 104 | the Free Software Foundation, either version 3 of the License, or 105 | (at your option) any later version. 106 | 107 | This program is distributed in the hope that it will be useful, 108 | but WITHOUT ANY WARRANTY; without even the implied warranty of 109 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 110 | GNU General Public License for more details. 111 | 112 | You should have received a copy of the GNU General Public License 113 | along with this program. If not, see . 114 | 115 | ## Support & Contact 116 | 117 | Please use the issue tracker for project related issues. 118 | 119 | More info: https://apollo.open-resource.org/mission:resources:picoreflow 120 | -------------------------------------------------------------------------------- /lib/max6675.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import RPi.GPIO as GPIO 3 | import time 4 | 5 | class MAX6675(object): 6 | '''Python driver for [MAX6675 Cold-Junction Compensated Thermocouple-to-Digital Converter](http://www.adafruit.com/datasheets/MAX6675.pdf) 7 | Requires: 8 | - The [GPIO Library](https://code.google.com/p/raspberry-gpio-python/) (Already on most Raspberry Pi OS builds) 9 | - A [Raspberry Pi](http://www.raspberrypi.org/) 10 | 11 | ''' 12 | def __init__(self, cs_pin, clock_pin, data_pin, units = "c", board = GPIO.BCM): 13 | '''Initialize Soft (Bitbang) SPI bus 14 | 15 | Parameters: 16 | - cs_pin: Chip Select (CS) / Slave Select (SS) pin (Any GPIO) 17 | - clock_pin: Clock (SCLK / SCK) pin (Any GPIO) 18 | - data_pin: Data input (SO / MOSI) pin (Any GPIO) 19 | - units: (optional) unit of measurement to return. ("c" (default) | "k" | "f") 20 | - board: (optional) pin numbering method as per RPi.GPIO library (GPIO.BCM (default) | GPIO.BOARD) 21 | 22 | ''' 23 | self.cs_pin = cs_pin 24 | self.clock_pin = clock_pin 25 | self.data_pin = data_pin 26 | self.units = units 27 | self.data = None 28 | self.board = board 29 | 30 | # Initialize needed GPIO 31 | GPIO.setmode(self.board) 32 | GPIO.setup(self.cs_pin, GPIO.OUT) 33 | GPIO.setup(self.clock_pin, GPIO.OUT) 34 | GPIO.setup(self.data_pin, GPIO.IN) 35 | 36 | # Pull chip select high to make chip inactive 37 | GPIO.output(self.cs_pin, GPIO.HIGH) 38 | 39 | def get(self): 40 | '''Reads SPI bus and returns current value of thermocouple.''' 41 | self.read() 42 | self.checkErrors() 43 | return getattr(self, "to_" + self.units)(self.data_to_tc_temperature()) 44 | 45 | def read(self): 46 | '''Reads 16 bits of the SPI bus & stores as an integer in self.data.''' 47 | bytesin = 0 48 | # Select the chip 49 | GPIO.output(self.cs_pin, GPIO.LOW) 50 | # Read in 16 bits 51 | for i in range(16): 52 | GPIO.output(self.clock_pin, GPIO.LOW) 53 | time.sleep(0.001) 54 | bytesin = bytesin << 1 55 | if (GPIO.input(self.data_pin)): 56 | bytesin = bytesin | 1 57 | GPIO.output(self.clock_pin, GPIO.HIGH) 58 | time.sleep(0.001) 59 | # Unselect the chip 60 | GPIO.output(self.cs_pin, GPIO.HIGH) 61 | # Save data 62 | self.data = bytesin 63 | 64 | def checkErrors(self, data_16 = None): 65 | '''Checks errors on bit D2''' 66 | if data_16 is None: 67 | data_16 = self.data 68 | noConnection = (data_16 & 0x4) != 0 # tc input bit, D2 69 | 70 | if noConnection: 71 | raise MAX6675Error("No Connection") # open thermocouple 72 | 73 | def data_to_tc_temperature(self, data_16 = None): 74 | '''Takes an integer and returns a thermocouple temperature in celsius.''' 75 | if data_16 is None: 76 | data_16 = self.data 77 | # Remove bits D0-3 78 | tc_data = ((data_16 >> 3) & 0xFFF) 79 | # 12-bit resolution 80 | return (tc_data * 0.25) 81 | 82 | def to_c(self, celsius): 83 | '''Celsius passthrough for generic to_* method.''' 84 | return celsius 85 | 86 | def to_k(self, celsius): 87 | '''Convert celsius to kelvin.''' 88 | return celsius + 273.15 89 | 90 | def to_f(self, celsius): 91 | '''Convert celsius to fahrenheit.''' 92 | return celsius * 9.0/5.0 + 32 93 | 94 | def cleanup(self): 95 | '''Selective GPIO cleanup''' 96 | GPIO.setup(self.cs_pin, GPIO.IN) 97 | GPIO.setup(self.clock_pin, GPIO.IN) 98 | 99 | class MAX6675Error(Exception): 100 | def __init__(self, value): 101 | self.value = value 102 | def __str__(self): 103 | return repr(self.value) 104 | 105 | if __name__ == "__main__": 106 | 107 | # default example 108 | cs_pin = 24 109 | clock_pin = 23 110 | data_pin = 22 111 | units = "c" 112 | thermocouple = MAX6675(cs_pin, clock_pin, data_pin, units) 113 | running = True 114 | while(running): 115 | try: 116 | try: 117 | tc = thermocouple.get() 118 | except MAX6675Error as e: 119 | tc = "Error: "+ e.value 120 | running = False 121 | print("tc: {}".format(tc)) 122 | time.sleep(1) 123 | except KeyboardInterrupt: 124 | running = False 125 | thermocouple.cleanup() 126 | -------------------------------------------------------------------------------- /public/assets/js/jquery.flot.draggable.js: -------------------------------------------------------------------------------- 1 | /* Flot plugin for adding point dragging capabilities to a plot. 2 | Author: Zach Dwiel - Heavy inspiration from Chris Leonello. Thank you! 3 | 4 | // dependencies: jquery.event.drag.js */ 5 | 6 | (function ($) { 7 | var options = { 8 | xaxis: { 9 | draggable: false, 10 | }, yaxis: { 11 | draggable: false, 12 | }, grid: { 13 | draggable: false, 14 | } 15 | }, 16 | drag = { pos: { x:null, y:null}, active: false }; 17 | 18 | function init(plot) { 19 | function bindEvents(plot, eventHolder) { 20 | var o = plot.getOptions(); 21 | var i; 22 | var series_draggable = false; 23 | var series = plot.getData(); 24 | for (i = 0; i < series.length; ++i) { 25 | if(series[i].draggable || series[i].draggablex || series[i].draggabley) { 26 | series_draggable = true; 27 | } 28 | } 29 | if (o.grid.draggable || o.xaxis.draggable || o.yaxis.draggable || series_draggable) { 30 | eventHolder.bind("dragstart", { distance: 10 }, function (e) { 31 | if (e.which != 1) // only accept left-click 32 | return false; 33 | var plotOffset = plot.getPlotOffset(); 34 | var offset = eventHolder.offset(), 35 | pos = { pageX: e.pageX, pageY: e.pageY }, 36 | canvasX = e.pageX - offset.left - plotOffset.left, 37 | canvasY = e.pageY - offset.top - plotOffset.top; 38 | drag.gridOffset = {top: offset.top + plotOffset.top, left: offset.left + plotOffset.left}; 39 | 40 | drag.item = plot.findNearbyItem(canvasX, canvasY, function (s) { return s["draggable"] != false; }); 41 | 42 | if (drag.item) { 43 | drag.item.pageX = parseInt(drag.item.series.xaxis.p2c(drag.item.datapoint[0]) + offset.left + plotOffset.left); 44 | drag.item.pageY = parseInt(drag.item.series.yaxis.p2c(drag.item.datapoint[1]) + offset.top + plotOffset.top); 45 | drag.active = true; 46 | } 47 | }); 48 | eventHolder.bind("drag", function (pos) { 49 | var axes = plot.getAxes(); 50 | var ax = axes.xaxis; 51 | var ay = axes.yaxis; 52 | var ax2 = axes.x2axis; 53 | var ay2 = axes.y2axis; 54 | var sidx = drag.item.seriesIndex; 55 | var didx = drag.item.dataIndex; 56 | var s = plot.getData()[sidx]; 57 | 58 | if (drag.item.series.yaxis == ay2) 59 | ay = ay2; 60 | if (drag.item.series.xaxis == ax2) 61 | ax = ax2; 62 | 63 | // Bring down to int 64 | var newx = Math.floor(ax.min + (pos.pageX-drag.gridOffset.left)/ax.scale); 65 | var newy = Math.floor(ay.max - (pos.pageY-drag.gridOffset.top)/ay.scale); 66 | 67 | series[sidx].data[didx] = [newx, newy]; 68 | plot.processData(); 69 | 70 | // change the raw data instead of processing every point all over again, not as clean, but faster 71 | var points = s.datapoints.points; 72 | var ps = s.datapoints.pointsize; 73 | if((o.grid.draggable || o.xaxis.draggable || s.draggablex || s.draggable) && (s.draggablex != false)) { 74 | points[didx*ps] = newx; 75 | } 76 | if((o.grid.draggable || o.yaxis.draggable || s.draggabley || s.draggable) && (s.draggabley != false)) { 77 | points[didx*ps+1] = newy; 78 | } 79 | 80 | var is_last = series[sidx].data.length == didx+1; 81 | 82 | // funny hack to make drag resizing usable 83 | if (newx > ax.max) 84 | { 85 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions()); 86 | } 87 | else if (newx < (ax.max*0.5) && newx >= ax.datamax && is_last) 88 | { 89 | ax.options.max = newx*2; 90 | plot.setupGrid(); 91 | } 92 | 93 | plot.draw(); 94 | 95 | // hack to update the profile points after dragging graph in edit mode 96 | updateProfileTable(); 97 | 98 | var retx = points[didx*ps]; 99 | var rety = points[didx*ps+1]; 100 | 101 | plot.getPlaceholder().trigger('plotSeriesChange', [sidx, didx, retx, rety]) 102 | }); 103 | eventHolder.bind("dragend", function (e) { 104 | var sidx = drag.item.seriesIndex; 105 | var didx = drag.item.dataIndex; 106 | var s = plot.getData()[sidx]; 107 | var ps = s.datapoints.pointsize; 108 | plot.getPlaceholder().trigger('plotFinalSeriesChange', [sidx, didx, s.datapoints.points[didx*ps], s.datapoints.points[didx*ps+1]]) 109 | }); 110 | } 111 | } 112 | 113 | plot.hooks.bindEvents.push(bindEvents); 114 | } 115 | 116 | $.plot.plugins.push({ 117 | init: init, 118 | options: options, 119 | name: 'draggable', 120 | version: '1.0' 121 | }); 122 | })(jQuery); 123 | -------------------------------------------------------------------------------- /public/assets/css/bootstrap-modal.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Modal 3 | * 4 | * Copyright Jordan Schroter 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Boostrap 3 patch for for bootstrap-modal. Include BEFORE bootstrap-modal.css! 9 | */ 10 | 11 | body.modal-open, 12 | .modal-open .navbar-fixed-top, 13 | .modal-open .navbar-fixed-bottom { 14 | margin-right: 0; 15 | } 16 | 17 | .modal { 18 | left: 50%; 19 | bottom: auto; 20 | right: auto; 21 | padding: 0; 22 | width: 500px; 23 | margin-left: -250px; 24 | background-clip: padding-box; 25 | } 26 | 27 | .modal.container { 28 | max-width: none; 29 | } 30 | 31 | 32 | /*! 33 | * Bootstrap Modal 34 | * 35 | * Copyright Jordan Schroter 36 | * Licensed under the Apache License v2.0 37 | * http://www.apache.org/licenses/LICENSE-2.0 38 | * 39 | */ 40 | 41 | .modal-open { 42 | overflow: hidden; 43 | } 44 | 45 | 46 | /* add a scroll bar to stop page from jerking around */ 47 | .modal-open.page-overflow .page-container, 48 | .modal-open.page-overflow .page-container .navbar-fixed-top, 49 | .modal-open.page-overflow .page-container .navbar-fixed-bottom, 50 | .modal-open.page-overflow .modal-scrollable { 51 | overflow-y: scroll; 52 | } 53 | 54 | @media (max-width: 979px) { 55 | .modal-open.page-overflow .page-container .navbar-fixed-top, 56 | .modal-open.page-overflow .page-container .navbar-fixed-bottom { 57 | overflow-y: visible; 58 | } 59 | } 60 | 61 | 62 | .modal-scrollable { 63 | position: fixed; 64 | top: 0; 65 | bottom: 0; 66 | left: 0; 67 | right: 0; 68 | overflow: auto; 69 | } 70 | 71 | .modal { 72 | outline: none; 73 | position: absolute; 74 | margin-top: 0; 75 | top: 50%; 76 | overflow: visible; /* allow content to popup out (i.e tooltips) */ 77 | } 78 | 79 | .modal.fade { 80 | top: -100%; 81 | -webkit-transition: opacity 0.3s linear, top 0.3s ease-out, bottom 0.3s ease-out, margin-top 0.3s ease-out; 82 | -moz-transition: opacity 0.3s linear, top 0.3s ease-out, bottom 0.3s ease-out, margin-top 0.3s ease-out; 83 | -o-transition: opacity 0.3s linear, top 0.3s ease-out, bottom 0.3s ease-out, margin-top 0.3s ease-out; 84 | transition: opacity 0.3s linear, top 0.3s ease-out, bottom 0.3s ease-out, margin-top 0.3s ease-out; 85 | } 86 | 87 | .modal.fade.in { 88 | top: 50%; 89 | } 90 | 91 | .modal-body { 92 | max-height: none; 93 | overflow: visible; 94 | } 95 | 96 | .modal.modal-absolute { 97 | position: absolute; 98 | z-index: 950; 99 | } 100 | 101 | .modal .loading-mask { 102 | position: absolute; 103 | top: 0; 104 | bottom: 0; 105 | left: 0; 106 | right: 0; 107 | background: #fff; 108 | border-radius: 6px; 109 | } 110 | 111 | .modal-backdrop.modal-absolute{ 112 | position: absolute; 113 | z-index: 940; 114 | } 115 | 116 | .modal-backdrop, 117 | .modal-backdrop.fade.in{ 118 | opacity: 0.7; 119 | filter: alpha(opacity=70); 120 | background: #3F3E3A; 121 | } 122 | 123 | .modal.container { 124 | width: 940px; 125 | margin-left: -470px; 126 | } 127 | 128 | /* Modal Overflow */ 129 | 130 | .modal-overflow.modal { 131 | top: 1%; 132 | } 133 | 134 | .modal-overflow.modal.fade { 135 | top: -100%; 136 | } 137 | 138 | .modal-overflow.modal.fade.in { 139 | top: 1%; 140 | } 141 | 142 | .modal-overflow .modal-body { 143 | overflow: auto; 144 | -webkit-overflow-scrolling: touch; 145 | } 146 | 147 | /* Responsive */ 148 | 149 | @media (min-width: 1200px) { 150 | .modal.container { 151 | width: 1170px; 152 | margin-left: -585px; 153 | } 154 | } 155 | 156 | @media (max-width: 979px) { 157 | .modal, 158 | .modal.container, 159 | .modal.modal-overflow { 160 | top: 1%; 161 | right: 1%; 162 | left: 1%; 163 | bottom: auto; 164 | width: auto !important; 165 | height: auto !important; 166 | margin: 0 !important; 167 | padding: 0 !important; 168 | } 169 | 170 | .modal.fade.in, 171 | .modal.container.fade.in, 172 | .modal.modal-overflow.fade.in { 173 | top: 1%; 174 | bottom: auto; 175 | } 176 | 177 | .modal-body, 178 | .modal-overflow .modal-body { 179 | position: static; 180 | margin: 0; 181 | height: auto !important; 182 | max-height: none !important; 183 | overflow: visible !important; 184 | } 185 | 186 | .modal-footer, 187 | .modal-overflow .modal-footer { 188 | position: static; 189 | } 190 | } 191 | 192 | .loading-spinner { 193 | position: absolute; 194 | top: 50%; 195 | left: 50%; 196 | margin: -12px 0 0 -12px; 197 | } 198 | 199 | /* 200 | Animate.css - http://daneden.me/animate 201 | Licensed under the ☺ license (http://licence.visualidiot.com/) 202 | 203 | Copyright (c) 2012 Dan Eden*/ 204 | 205 | .animated { 206 | -webkit-animation-duration: 1s; 207 | -moz-animation-duration: 1s; 208 | -o-animation-duration: 1s; 209 | animation-duration: 1s; 210 | -webkit-animation-fill-mode: both; 211 | -moz-animation-fill-mode: both; 212 | -o-animation-fill-mode: both; 213 | animation-fill-mode: both; 214 | } 215 | 216 | @-webkit-keyframes shake { 217 | 0%, 100% {-webkit-transform: translateX(0);} 218 | 10%, 30%, 50%, 70%, 90% {-webkit-transform: translateX(-10px);} 219 | 20%, 40%, 60%, 80% {-webkit-transform: translateX(10px);} 220 | } 221 | 222 | @-moz-keyframes shake { 223 | 0%, 100% {-moz-transform: translateX(0);} 224 | 10%, 30%, 50%, 70%, 90% {-moz-transform: translateX(-10px);} 225 | 20%, 40%, 60%, 80% {-moz-transform: translateX(10px);} 226 | } 227 | 228 | @-o-keyframes shake { 229 | 0%, 100% {-o-transform: translateX(0);} 230 | 10%, 30%, 50%, 70%, 90% {-o-transform: translateX(-10px);} 231 | 20%, 40%, 60%, 80% {-o-transform: translateX(10px);} 232 | } 233 | 234 | @keyframes shake { 235 | 0%, 100% {transform: translateX(0);} 236 | 10%, 30%, 50%, 70%, 90% {transform: translateX(-10px);} 237 | 20%, 40%, 60%, 80% {transform: translateX(10px);} 238 | } 239 | 240 | .shake { 241 | -webkit-animation-name: shake; 242 | -moz-animation-name: shake; 243 | -o-animation-name: shake; 244 | animation-name: shake; 245 | } 246 | -------------------------------------------------------------------------------- /lib/max31855.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import RPi.GPIO as GPIO 3 | 4 | class MAX31855(object): 5 | '''Python driver for [MAX38155 Cold-Junction Compensated Thermocouple-to-Digital Converter](http://www.maximintegrated.com/datasheet/index.mvp/id/7273) 6 | Requires: 7 | - The [GPIO Library](https://code.google.com/p/raspberry-gpio-python/) (Already on most Raspberry Pi OS builds) 8 | - A [Raspberry Pi](http://www.raspberrypi.org/) 9 | 10 | ''' 11 | def __init__(self, cs_pin, clock_pin, data_pin, units = "c", board = GPIO.BCM): 12 | '''Initialize Soft (Bitbang) SPI bus 13 | 14 | Parameters: 15 | - cs_pin: Chip Select (CS) / Slave Select (SS) pin (Any GPIO) 16 | - clock_pin: Clock (SCLK / SCK) pin (Any GPIO) 17 | - data_pin: Data input (SO / MOSI) pin (Any GPIO) 18 | - units: (optional) unit of measurement to return. ("c" (default) | "k" | "f") 19 | - board: (optional) pin numbering method as per RPi.GPIO library (GPIO.BCM (default) | GPIO.BOARD) 20 | 21 | ''' 22 | self.cs_pin = cs_pin 23 | self.clock_pin = clock_pin 24 | self.data_pin = data_pin 25 | self.units = units 26 | self.data = None 27 | self.board = board 28 | 29 | # Initialize needed GPIO 30 | GPIO.setmode(self.board) 31 | GPIO.setup(self.cs_pin, GPIO.OUT) 32 | GPIO.setup(self.clock_pin, GPIO.OUT) 33 | GPIO.setup(self.data_pin, GPIO.IN) 34 | 35 | # Pull chip select high to make chip inactive 36 | GPIO.output(self.cs_pin, GPIO.HIGH) 37 | 38 | def get(self): 39 | '''Reads SPI bus and returns current value of thermocouple.''' 40 | self.read() 41 | self.checkErrors() 42 | return getattr(self, "to_" + self.units)(self.data_to_tc_temperature()) 43 | 44 | def get_rj(self): 45 | '''Reads SPI bus and returns current value of reference junction.''' 46 | self.read() 47 | return getattr(self, "to_" + self.units)(self.data_to_rj_temperature()) 48 | 49 | def read(self): 50 | '''Reads 32 bits of the SPI bus & stores as an integer in self.data.''' 51 | bytesin = 0 52 | # Select the chip 53 | GPIO.output(self.cs_pin, GPIO.LOW) 54 | # Read in 32 bits 55 | for i in range(32): 56 | GPIO.output(self.clock_pin, GPIO.LOW) 57 | bytesin = bytesin << 1 58 | if (GPIO.input(self.data_pin)): 59 | bytesin = bytesin | 1 60 | GPIO.output(self.clock_pin, GPIO.HIGH) 61 | # Unselect the chip 62 | GPIO.output(self.cs_pin, GPIO.HIGH) 63 | # Save data 64 | self.data = bytesin 65 | 66 | def checkErrors(self, data_32 = None): 67 | '''Checks error bits to see if there are any SCV, SCG, or OC faults''' 68 | if data_32 is None: 69 | data_32 = self.data 70 | anyErrors = (data_32 & 0x10000) != 0 # Fault bit, D16 71 | noConnection = (data_32 & 0x00000001) != 0 # OC bit, D0 72 | shortToGround = (data_32 & 0x00000002) != 0 # SCG bit, D1 73 | shortToVCC = (data_32 & 0x00000004) != 0 # SCV bit, D2 74 | if anyErrors: 75 | if noConnection: 76 | raise MAX31855Error("No Connection") 77 | elif shortToGround: 78 | raise MAX31855Error("Thermocouple short to ground") 79 | elif shortToVCC: 80 | raise MAX31855Error("Thermocouple short to VCC") 81 | else: 82 | # Perhaps another SPI device is trying to send data? 83 | # Did you remember to initialize all other SPI devices? 84 | raise MAX31855Error("Unknown Error") 85 | 86 | def data_to_tc_temperature(self, data_32 = None): 87 | '''Takes an integer and returns a thermocouple temperature in celsius.''' 88 | if data_32 is None: 89 | data_32 = self.data 90 | tc_data = ((data_32 >> 18) & 0x3FFF) 91 | return self.convert_tc_data(tc_data) 92 | 93 | def data_to_rj_temperature(self, data_32 = None): 94 | '''Takes an integer and returns a reference junction temperature in celsius.''' 95 | if data_32 is None: 96 | data_32 = self.data 97 | rj_data = ((data_32 >> 4) & 0xFFF) 98 | return self.convert_rj_data(rj_data) 99 | 100 | def convert_tc_data(self, tc_data): 101 | '''Convert thermocouple data to a useful number (celsius).''' 102 | if tc_data & 0x2000: 103 | # two's compliment 104 | without_resolution = ~tc_data & 0x1FFF 105 | without_resolution += 1 106 | without_resolution *= -1 107 | else: 108 | without_resolution = tc_data & 0x1FFF 109 | return without_resolution * 0.25 110 | 111 | def convert_rj_data(self, rj_data): 112 | '''Convert reference junction data to a useful number (celsius).''' 113 | if rj_data & 0x800: 114 | without_resolution = ~rj_data & 0x7FF 115 | without_resolution += 1 116 | without_resolution *= -1 117 | else: 118 | without_resolution = rj_data & 0x7FF 119 | return without_resolution * 0.0625 120 | 121 | def to_c(self, celsius): 122 | '''Celsius passthrough for generic to_* method.''' 123 | return celsius 124 | 125 | def to_k(self, celsius): 126 | '''Convert celsius to kelvin.''' 127 | return celsius + 273.15 128 | 129 | def to_f(self, celsius): 130 | '''Convert celsius to fahrenheit.''' 131 | return celsius * 9.0/5.0 + 32 132 | 133 | def cleanup(self): 134 | '''Selective GPIO cleanup''' 135 | GPIO.setup(self.cs_pin, GPIO.IN) 136 | GPIO.setup(self.clock_pin, GPIO.IN) 137 | 138 | class MAX31855Error(Exception): 139 | def __init__(self, value): 140 | self.value = value 141 | def __str__(self): 142 | return repr(self.value) 143 | 144 | if __name__ == "__main__": 145 | 146 | # Multi-chip example 147 | import time 148 | cs_pins = [4, 17, 18, 24] 149 | clock_pin = 23 150 | data_pin = 22 151 | units = "f" 152 | thermocouples = [] 153 | for cs_pin in cs_pins: 154 | thermocouples.append(MAX31855(cs_pin, clock_pin, data_pin, units)) 155 | running = True 156 | while(running): 157 | try: 158 | for thermocouple in thermocouples: 159 | rj = thermocouple.get_rj() 160 | try: 161 | tc = thermocouple.get() 162 | except MAX31855Error as e: 163 | tc = "Error: "+ e.value 164 | running = False 165 | print("tc: {} and rj: {}".format(tc, rj)) 166 | time.sleep(1) 167 | except KeyboardInterrupt: 168 | running = False 169 | for thermocouple in thermocouples: 170 | thermocouple.cleanup() 171 | -------------------------------------------------------------------------------- /picoreflowd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | import logging 6 | import json 7 | 8 | import bottle 9 | import gevent 10 | import geventwebsocket 11 | from gevent.pywsgi import WSGIServer 12 | from geventwebsocket.handler import WebSocketHandler 13 | 14 | try: 15 | sys.dont_write_bytecode = True 16 | import config 17 | sys.dont_write_bytecode = False 18 | except: 19 | print("Could not import config file.") 20 | print("Copy config.py.EXAMPLE to config.py and adapt it for your setup.") 21 | exit(1) 22 | 23 | logging.basicConfig(level=config.log_level, format=config.log_format) 24 | log = logging.getLogger("picoreflowd") 25 | log.info("Starting picoreflowd") 26 | 27 | script_dir = os.path.dirname(os.path.realpath(__file__)) 28 | sys.path.insert(0, script_dir + '/lib/') 29 | profile_path = os.path.join(script_dir, "storage", "profiles") 30 | 31 | from oven import Oven, Profile 32 | from ovenWatcher import OvenWatcher 33 | 34 | app = bottle.Bottle() 35 | oven = Oven() 36 | ovenWatcher = OvenWatcher(oven) 37 | 38 | 39 | @app.route('/') 40 | def index(): 41 | return bottle.redirect('/picoreflow/index.html') 42 | 43 | 44 | @app.route('/picoreflow/:filename#.*#') 45 | def send_static(filename): 46 | log.debug("serving %s" % filename) 47 | return bottle.static_file(filename, root=os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "public")) 48 | 49 | 50 | def get_websocket_from_request(): 51 | env = bottle.request.environ 52 | wsock = env.get('wsgi.websocket') 53 | if not wsock: 54 | abort(400, 'Expected WebSocket request.') 55 | return wsock 56 | 57 | 58 | @app.route('/control') 59 | def handle_control(): 60 | wsock = get_websocket_from_request() 61 | log.info("websocket (control) opened") 62 | while True: 63 | try: 64 | message = wsock.receive() 65 | log.info("Received (control): %s" % message) 66 | msgdict = json.loads(message) 67 | if msgdict.get("cmd") == "RUN": 68 | log.info("RUN command received") 69 | profile_obj = msgdict.get('profile') 70 | if profile_obj: 71 | profile_json = json.dumps(profile_obj) 72 | profile = Profile(profile_json) 73 | oven.run_profile(profile) 74 | ovenWatcher.record(profile) 75 | elif msgdict.get("cmd") == "SIMULATE": 76 | log.info("SIMULATE command received") 77 | profile_obj = msgdict.get('profile') 78 | if profile_obj: 79 | profile_json = json.dumps(profile_obj) 80 | profile = Profile(profile_json) 81 | simulated_oven = Oven(simulate=True, time_step=0.05) 82 | simulation_watcher = OvenWatcher(simulated_oven) 83 | simulation_watcher.add_observer(wsock) 84 | #simulated_oven.run_profile(profile) 85 | #simulation_watcher.record(profile) 86 | elif msgdict.get("cmd") == "STOP": 87 | log.info("Stop command received") 88 | oven.abort_run() 89 | except WebSocketError: 90 | break 91 | log.info("websocket (control) closed") 92 | 93 | 94 | @app.route('/storage') 95 | def handle_storage(): 96 | wsock = get_websocket_from_request() 97 | log.info("websocket (storage) opened") 98 | while True: 99 | try: 100 | message = wsock.receive() 101 | if not message: 102 | break 103 | log.debug("websocket (storage) received: %s" % message) 104 | 105 | try: 106 | msgdict = json.loads(message) 107 | except: 108 | msgdict = {} 109 | 110 | if message == "GET": 111 | log.info("GET command recived") 112 | wsock.send(get_profiles()) 113 | elif msgdict.get("cmd") == "DELETE": 114 | log.info("DELETE command received") 115 | profile_obj = msgdict.get('profile') 116 | if delete_profile(profile_obj): 117 | msgdict["resp"] = "OK" 118 | wsock.send(json.dumps(msgdict)) 119 | #wsock.send(get_profiles()) 120 | elif msgdict.get("cmd") == "PUT": 121 | log.info("PUT command received") 122 | profile_obj = msgdict.get('profile') 123 | force = msgdict.get('force', False) 124 | if profile_obj: 125 | #del msgdict["cmd"] 126 | if save_profile(profile_obj, force): 127 | msgdict["resp"] = "OK" 128 | else: 129 | msgdict["resp"] = "FAIL" 130 | log.debug("websocket (storage) sent: %s" % message) 131 | 132 | wsock.send(json.dumps(msgdict)) 133 | wsock.send(get_profiles()) 134 | except WebSocketError: 135 | break 136 | log.info("websocket (storage) closed") 137 | 138 | 139 | @app.route('/config') 140 | def handle_config(): 141 | wsock = get_websocket_from_request() 142 | log.info("websocket (config) opened") 143 | while True: 144 | try: 145 | message = wsock.receive() 146 | wsock.send(get_config()) 147 | except WebSocketError: 148 | break 149 | log.info("websocket (config) closed") 150 | 151 | 152 | @app.route('/status') 153 | def handle_status(): 154 | wsock = get_websocket_from_request() 155 | ovenWatcher.add_observer(wsock) 156 | log.info("websocket (status) opened") 157 | while True: 158 | try: 159 | message = wsock.receive() 160 | wsock.send("Your message was: %r" % message) 161 | except WebSocketError: 162 | break 163 | log.info("websocket (status) closed") 164 | 165 | 166 | def get_profiles(): 167 | try: 168 | profile_files = os.listdir(profile_path) 169 | except: 170 | profile_files = [] 171 | profiles = [] 172 | for filename in profile_files: 173 | with open(os.path.join(profile_path, filename), 'r') as f: 174 | profiles.append(json.load(f)) 175 | return json.dumps(profiles) 176 | 177 | 178 | def save_profile(profile, force=False): 179 | profile_json = json.dumps(profile) 180 | filename = profile['name']+".json" 181 | filepath = os.path.join(profile_path, filename) 182 | if not force and os.path.exists(filepath): 183 | log.error("Could not write, %s already exists" % filepath) 184 | return False 185 | with open(filepath, 'w+') as f: 186 | f.write(profile_json) 187 | f.close() 188 | log.info("Wrote %s" % filepath) 189 | return True 190 | 191 | def delete_profile(profile): 192 | profile_json = json.dumps(profile) 193 | filename = profile['name']+".json" 194 | filepath = os.path.join(profile_path, filename) 195 | os.remove(filepath) 196 | log.info("Deleted %s" % filepath) 197 | return True 198 | 199 | 200 | def get_config(): 201 | return json.dumps({"temp_scale": config.temp_scale, 202 | "time_scale_slope": config.time_scale_slope, 203 | "time_scale_profile": config.time_scale_profile, 204 | "kwh_rate": config.kwh_rate, 205 | "currency_type": config.currency_type}) 206 | 207 | 208 | def main(): 209 | ip = config.listening_ip 210 | port = config.listening_port 211 | log.info("listening on %s:%d" % (ip, port)) 212 | 213 | server = WSGIServer((ip, port), app, 214 | handler_class=WebSocketHandler) 215 | server.serve_forever() 216 | 217 | 218 | if __name__ == "__main__": 219 | main() 220 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | picoReflow 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 |
Sensor Temp
31 |
Target Temp
32 |
Status
33 |
34 |
35 |
36 |
25°C
37 |
---°C
38 |
39 |
\l[I
40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 | 55 | 56 |
57 |
58 | 62 | 63 |
64 | 85 |
86 |
87 |
88 |
89 | 90 |
91 |
92 | 93 | 116 | 117 | 136 | 137 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /lib/oven.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import random 4 | import datetime 5 | import logging 6 | import json 7 | 8 | import config 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | try: 13 | if config.max31855 + config.max6675 + config.max31855spi > 1: 14 | log.error("choose (only) one converter IC") 15 | exit() 16 | if config.max31855: 17 | from max31855 import MAX31855, MAX31855Error 18 | log.info("import MAX31855") 19 | if config.max31855spi: 20 | import Adafruit_GPIO.SPI as SPI 21 | from max31855spi import MAX31855SPI, MAX31855SPIError 22 | log.info("import MAX31855SPI") 23 | spi_reserved_gpio = [7, 8, 9, 10, 11] 24 | if config.gpio_air in spi_reserved_gpio: 25 | raise Exception("gpio_air pin %s collides with SPI pins %s" % (config.gpio_air, spi_reserved_gpio)) 26 | if config.gpio_cool in spi_reserved_gpio: 27 | raise Exception("gpio_cool pin %s collides with SPI pins %s" % (config.gpio_cool, spi_reserved_gpio)) 28 | if config.gpio_door in spi_reserved_gpio: 29 | raise Exception("gpio_door pin %s collides with SPI pins %s" % (config.gpio_door, spi_reserved_gpio)) 30 | if config.gpio_heat in spi_reserved_gpio: 31 | raise Exception("gpio_heat pin %s collides with SPI pins %s" % (config.gpio_heat, spi_reserved_gpio)) 32 | if config.max6675: 33 | from max6675 import MAX6675, MAX6675Error 34 | log.info("import MAX6675") 35 | sensor_available = True 36 | except ImportError: 37 | log.exception("Could not initialize temperature sensor, using dummy values!") 38 | sensor_available = False 39 | 40 | try: 41 | import RPi.GPIO as GPIO 42 | GPIO.setmode(GPIO.BCM) 43 | GPIO.setwarnings(False) 44 | GPIO.setup(config.gpio_heat, GPIO.OUT) 45 | GPIO.setup(config.gpio_cool, GPIO.OUT) 46 | GPIO.setup(config.gpio_air, GPIO.OUT) 47 | GPIO.setup(config.gpio_door, GPIO.IN, pull_up_down=GPIO.PUD_UP) 48 | 49 | gpio_available = True 50 | except ImportError: 51 | msg = "Could not initialize GPIOs, oven operation will only be simulated!" 52 | log.warning(msg) 53 | gpio_available = False 54 | 55 | 56 | class Oven (threading.Thread): 57 | STATE_IDLE = "IDLE" 58 | STATE_RUNNING = "RUNNING" 59 | 60 | def __init__(self, simulate=False, time_step=config.sensor_time_wait): 61 | threading.Thread.__init__(self) 62 | self.daemon = True 63 | self.simulate = simulate 64 | self.time_step = time_step 65 | self.reset() 66 | if simulate: 67 | self.temp_sensor = TempSensorSimulate(self, 0.5, self.time_step) 68 | if sensor_available: 69 | self.temp_sensor = TempSensorReal(self.time_step) 70 | else: 71 | self.temp_sensor = TempSensorSimulate(self, 72 | self.time_step, 73 | self.time_step) 74 | self.temp_sensor.start() 75 | self.start() 76 | 77 | def reset(self): 78 | self.profile = None 79 | self.start_time = 0 80 | self.runtime = 0 81 | self.totaltime = 0 82 | self.target = 0 83 | self.door = self.get_door_state() 84 | self.state = Oven.STATE_IDLE 85 | self.set_heat(False) 86 | self.set_cool(False) 87 | self.set_air(False) 88 | self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp) 89 | 90 | def run_profile(self, profile): 91 | log.info("Running profile %s" % profile.name) 92 | self.profile = profile 93 | self.totaltime = profile.get_duration() 94 | self.state = Oven.STATE_RUNNING 95 | self.start_time = datetime.datetime.now() 96 | log.info("Starting") 97 | 98 | def abort_run(self): 99 | self.reset() 100 | 101 | def run(self): 102 | temperature_count = 0 103 | last_temp = 0 104 | pid = 0 105 | while True: 106 | self.door = self.get_door_state() 107 | 108 | if self.state == Oven.STATE_RUNNING: 109 | if self.simulate: 110 | self.runtime += 0.5 111 | else: 112 | runtime_delta = datetime.datetime.now() - self.start_time 113 | self.runtime = runtime_delta.total_seconds() 114 | log.info("running at %.1f deg C (Target: %.1f) , heat %.2f, cool %.2f, air %.2f, door %s (%.1fs/%.0f)" % (self.temp_sensor.temperature, self.target, self.heat, self.cool, self.air, self.door, self.runtime, self.totaltime)) 115 | self.target = self.profile.get_target_temperature(self.runtime) 116 | pid = self.pid.compute(self.target, self.temp_sensor.temperature) 117 | 118 | log.info("pid: %.3f" % pid) 119 | 120 | self.set_cool(pid <= -1) 121 | if(pid > 0): 122 | # The temp should be changing with the heat on 123 | # Count the number of time_steps encountered with no change and the heat on 124 | if last_temp == self.temp_sensor.temperature: 125 | temperature_count += 1 126 | else: 127 | temperature_count = 0 128 | # If the heat is on and nothing is changing, reset 129 | # The direction or amount of change does not matter 130 | # This prevents runaway in the event of a sensor read failure 131 | if temperature_count > 20: 132 | log.info("Error reading sensor, oven temp not responding to heat.") 133 | self.reset() 134 | else: 135 | temperature_count = 0 136 | 137 | #Capture the last temperature value. This must be done before set_heat, since there is a sleep in there now. 138 | last_temp = self.temp_sensor.temperature 139 | 140 | self.set_heat(pid) 141 | 142 | #if self.profile.is_rising(self.runtime): 143 | # self.set_cool(False) 144 | # self.set_heat(self.temp_sensor.temperature < self.target) 145 | #else: 146 | # self.set_heat(False) 147 | # self.set_cool(self.temp_sensor.temperature > self.target) 148 | 149 | if self.temp_sensor.temperature > 200: 150 | self.set_air(False) 151 | elif self.temp_sensor.temperature < 180: 152 | self.set_air(True) 153 | 154 | if self.runtime >= self.totaltime: 155 | self.reset() 156 | 157 | 158 | if pid > 0: 159 | time.sleep(self.time_step * (1 - pid)) 160 | else: 161 | time.sleep(self.time_step) 162 | 163 | def set_heat(self, value): 164 | if value > 0: 165 | self.heat = 1.0 166 | if gpio_available: 167 | if config.heater_invert: 168 | GPIO.output(config.gpio_heat, GPIO.LOW) 169 | time.sleep(self.time_step * value) 170 | GPIO.output(config.gpio_heat, GPIO.HIGH) 171 | else: 172 | GPIO.output(config.gpio_heat, GPIO.HIGH) 173 | time.sleep(self.time_step * value) 174 | GPIO.output(config.gpio_heat, GPIO.LOW) 175 | else: 176 | self.heat = 0.0 177 | if gpio_available: 178 | if config.heater_invert: 179 | GPIO.output(config.gpio_heat, GPIO.HIGH) 180 | else: 181 | GPIO.output(config.gpio_heat, GPIO.LOW) 182 | 183 | def set_cool(self, value): 184 | if value: 185 | self.cool = 1.0 186 | if gpio_available: 187 | GPIO.output(config.gpio_cool, GPIO.LOW) 188 | else: 189 | self.cool = 0.0 190 | if gpio_available: 191 | GPIO.output(config.gpio_cool, GPIO.HIGH) 192 | 193 | def set_air(self, value): 194 | if value: 195 | self.air = 1.0 196 | if gpio_available: 197 | GPIO.output(config.gpio_air, GPIO.LOW) 198 | else: 199 | self.air = 0.0 200 | if gpio_available: 201 | GPIO.output(config.gpio_air, GPIO.HIGH) 202 | 203 | def get_state(self): 204 | state = { 205 | 'runtime': self.runtime, 206 | 'temperature': self.temp_sensor.temperature, 207 | 'target': self.target, 208 | 'state': self.state, 209 | 'heat': self.heat, 210 | 'cool': self.cool, 211 | 'air': self.air, 212 | 'totaltime': self.totaltime, 213 | 'door': self.door 214 | } 215 | return state 216 | 217 | def get_door_state(self): 218 | if gpio_available: 219 | return "OPEN" if GPIO.input(config.gpio_door) else "CLOSED" 220 | else: 221 | return "UNKNOWN" 222 | 223 | 224 | class TempSensor(threading.Thread): 225 | def __init__(self, time_step): 226 | threading.Thread.__init__(self) 227 | self.daemon = True 228 | self.temperature = 0 229 | self.time_step = time_step 230 | 231 | 232 | class TempSensorReal(TempSensor): 233 | def __init__(self, time_step): 234 | TempSensor.__init__(self, time_step) 235 | if config.max6675: 236 | log.info("init MAX6675") 237 | self.thermocouple = MAX6675(config.gpio_sensor_cs, 238 | config.gpio_sensor_clock, 239 | config.gpio_sensor_data, 240 | config.temp_scale) 241 | 242 | if config.max31855: 243 | log.info("init MAX31855") 244 | self.thermocouple = MAX31855(config.gpio_sensor_cs, 245 | config.gpio_sensor_clock, 246 | config.gpio_sensor_data, 247 | config.temp_scale) 248 | 249 | if config.max31855spi: 250 | log.info("init MAX31855-spi") 251 | self.thermocouple = MAX31855SPI(spi_dev=SPI.SpiDev(port=0, device=config.spi_sensor_chip_id)) 252 | 253 | def run(self): 254 | while True: 255 | try: 256 | self.temperature = self.thermocouple.get() 257 | except Exception: 258 | log.exception("problem reading temp") 259 | time.sleep(self.time_step) 260 | 261 | 262 | class TempSensorSimulate(TempSensor): 263 | def __init__(self, oven, time_step, sleep_time): 264 | TempSensor.__init__(self, time_step) 265 | self.oven = oven 266 | self.sleep_time = sleep_time 267 | 268 | def run(self): 269 | t_env = config.sim_t_env 270 | c_heat = config.sim_c_heat 271 | c_oven = config.sim_c_oven 272 | p_heat = config.sim_p_heat 273 | R_o_nocool = config.sim_R_o_nocool 274 | R_o_cool = config.sim_R_o_cool 275 | R_ho_noair = config.sim_R_ho_noair 276 | R_ho_air = config.sim_R_ho_air 277 | 278 | t = t_env # deg C temp in oven 279 | t_h = t # deg C temp of heat element 280 | while True: 281 | #heating energy 282 | Q_h = p_heat * self.time_step * self.oven.heat 283 | 284 | #temperature change of heat element by heating 285 | t_h += Q_h / c_heat 286 | 287 | if self.oven.air: 288 | R_ho = R_ho_air 289 | else: 290 | R_ho = R_ho_noair 291 | 292 | #energy flux heat_el -> oven 293 | p_ho = (t_h - t) / R_ho 294 | 295 | #temperature change of oven and heat el 296 | t += p_ho * self.time_step / c_oven 297 | t_h -= p_ho * self.time_step / c_heat 298 | 299 | #energy flux oven -> env 300 | if self.oven.cool: 301 | p_env = (t - t_env) / R_o_cool 302 | else: 303 | p_env = (t - t_env) / R_o_nocool 304 | 305 | #temperature change of oven by cooling to env 306 | t -= p_env * self.time_step / c_oven 307 | log.debug("energy sim: -> %dW heater: %.0f -> %dW oven: %.0f -> %dW env" % (int(p_heat * self.oven.heat), t_h, int(p_ho), t, int(p_env))) 308 | self.temperature = t 309 | 310 | time.sleep(self.sleep_time) 311 | 312 | 313 | class Profile(): 314 | def __init__(self, json_data): 315 | obj = json.loads(json_data) 316 | self.name = obj["name"] 317 | self.data = sorted(obj["data"]) 318 | 319 | def get_duration(self): 320 | return max([t for (t, x) in self.data]) 321 | 322 | def get_surrounding_points(self, time): 323 | if time > self.get_duration(): 324 | return (None, None) 325 | 326 | prev_point = None 327 | next_point = None 328 | 329 | for i in range(len(self.data)): 330 | if time < self.data[i][0]: 331 | prev_point = self.data[i-1] 332 | next_point = self.data[i] 333 | break 334 | 335 | return (prev_point, next_point) 336 | 337 | def is_rising(self, time): 338 | (prev_point, next_point) = self.get_surrounding_points(time) 339 | if prev_point and next_point: 340 | return prev_point[1] < next_point[1] 341 | else: 342 | return False 343 | 344 | def get_target_temperature(self, time): 345 | if time > self.get_duration(): 346 | return 0 347 | 348 | (prev_point, next_point) = self.get_surrounding_points(time) 349 | 350 | incl = float(next_point[1] - prev_point[1]) / float(next_point[0] - prev_point[0]) 351 | temp = prev_point[1] + (time - prev_point[0]) * incl 352 | return temp 353 | 354 | 355 | class PID(): 356 | def __init__(self, ki=1, kp=1, kd=1): 357 | self.ki = ki 358 | self.kp = kp 359 | self.kd = kd 360 | self.lastNow = datetime.datetime.now() 361 | self.iterm = 0 362 | self.lastErr = 0 363 | 364 | def compute(self, setpoint, ispoint): 365 | now = datetime.datetime.now() 366 | timeDelta = (now - self.lastNow).total_seconds() 367 | 368 | error = float(setpoint - ispoint) 369 | self.iterm += (error * timeDelta * self.ki) 370 | self.iterm = sorted([-1, self.iterm, 1])[1] 371 | dErr = (error - self.lastErr) / timeDelta 372 | 373 | output = self.kp * error + self.iterm + self.kd * dErr 374 | output = sorted([-1, output, 1])[1] 375 | self.lastErr = error 376 | self.lastNow = now 377 | 378 | return output 379 | -------------------------------------------------------------------------------- /public/assets/css/picoreflow.css: -------------------------------------------------------------------------------- 1 | @font-face{ 2 | font-family: "Digi"; 3 | src: url('/picoreflow/assets/fonts/digital-7-webfont.eot'); 4 | src: local("Digital 7"), 5 | url('/picoreflow/assets/fonts/digital-7-webfont.woff') format("woff"), 6 | url('/picoreflow/assets/fonts/digital-7-webfont.ttf') format("truetype"); 7 | } 8 | 9 | @font-face{ 10 | font-family: "Tables"; 11 | src: url('/picoreflow/assets/fonts/tables.eot'); 12 | src: local("Tables"), 13 | url('/picoreflow/assets/fonts/tables.woff') format("woff"), 14 | url('/picoreflow/assets/fonts/tables.ttf') format("truetype"); 15 | } 16 | 17 | body { 18 | background: #b9b6af; 19 | } 20 | 21 | #status { 22 | margin-top: 15px; 23 | color: #d8d3c5; 24 | font-weight: normal; 25 | -webkit-border-radius: 7px; 26 | -moz-border-radius: 7px; 27 | border-radius: 7px; 28 | border: 1px solid #ddd; 29 | height: 80px; 30 | -moz-box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.55); 31 | -webkit-box-shadow: 0 0 1.5em 0 rgba(0,0,0,0.55); 32 | box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.55); 33 | } 34 | 35 | .display { 36 | display: inline-block; 37 | text-align: right; 38 | padding-right: 5px; 39 | font-size: 40px; 40 | font-weight: normal; 41 | height: 40px; 42 | line-height: 40px; 43 | vertical-align: middle; 44 | color: #d8d3c5; 45 | border-color: #000000; 46 | } 47 | 48 | .ds-panel { 49 | background: #3F3E3A url('/picoreflow/assets/images/panel_bg.png') repeat; 50 | -moz-box-shadow: inset 0 0 42px 0 #000; 51 | -webkit-box-shadow: inset 0 0 42px 0 #000; 52 | box-shadow: inset 0 0 42px 0 #000; 53 | } 54 | 55 | .ds-title-panel { 56 | -webkit-border-top-left-radius: 6px; 57 | -webkit-border-top-right-radius: 6px; 58 | -moz-border-radius-topleft: 6px; 59 | -moz-border-radius-topright: 6px; 60 | border-top-left-radius: 6px; 61 | border-top-right-radius: 6px; 62 | background-image: -webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8)); 63 | background-image: -webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%); 64 | background-image: -moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%); 65 | background-image: linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%); 66 | background-repeat: repeat-x; 67 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0); 68 | height: 18px; 69 | } 70 | 71 | .ds-title { 72 | display: inline-block; 73 | font-size: 10px; 74 | height: 18px; 75 | line-height: 18px; 76 | width: 100px; 77 | border-right: 1px solid #b9b6af; 78 | vertical-align: top; 79 | text-align: center; 80 | color: #8B8989; 81 | text-shadow: 1px 1px 0px rgba(255, 255, 255, 0.75); 82 | } 83 | 84 | .ds-num { 85 | width: 100px; 86 | font-family: "Digi"; 87 | border-right: 1px solid #b9b6af; 88 | white-space: nowrap; 89 | text-shadow: 0 0 25px rgba(0, 0, 0, 1); 90 | } 91 | 92 | .ds-input { 93 | color: #d8d3c5; 94 | font-family: "Digi"; 95 | font-size: 24px; 96 | text-shadow: 0 0 12px rgba(0, 0, 0, 1); 97 | background: #3F3E3A url('/picoreflow/assets/images/panel_bg.png') repeat; 98 | -moz-box-shadow: inset 0 0 12px 0 #000; 99 | -webkit-box-shadow: inset 0 0 12px 0 #000; 100 | box-shadow: inset 0 0 12px 0 #000; 101 | text-align: right; 102 | padding: 0; 103 | padding-right: 7px; 104 | 105 | } 106 | 107 | .ds-unit { 108 | font-family: "Arial"; 109 | font-size: 22px; 110 | vertical-align: top; 111 | line-height: 32px; 112 | margin-left: 4px; 113 | } 114 | 115 | .ds-led { 116 | font-family: "Tables"; 117 | font-size: 38px; 118 | text-align: center; 119 | color: #1F1E1A; 120 | text-shadow: 0 0 5px #000, 0 0 5px #000; 121 | border-left: 1px solid #b9b6af; 122 | height: 40px; 123 | width: 42px; 124 | display: inline-block; 125 | } 126 | 127 | 128 | .ds-led-hazard-active { 129 | color: rgb(255, 204, 0); 130 | background: -moz-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%, rgba(241,218,54,0.26) 100%); /* FF3.6+ */ 131 | background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(242,195,67,1)), color-stop(100%,rgba(241,218,54,0.26))); /* Chrome,Safari4+ */ 132 | background: -webkit-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* Chrome10+,Safari5.1+ */ 133 | background: -o-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* Opera 12+ */ 134 | background: -ms-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* IE10+ */ 135 | background: radial-gradient(ellipse at center, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* W3C */ 136 | } 137 | 138 | .ds-led-door-open { 139 | color: rgb(214, 42, 0); 140 | background: -moz-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%, rgba(241,218,54,0.26) 100%); /* FF3.6+ */ 141 | background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(242,195,67,1)), color-stop(100%,rgba(241,218,54,0.26))); /* Chrome,Safari4+ */ 142 | background: -webkit-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* Chrome10+,Safari5.1+ */ 143 | background: -o-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* Opera 12+ */ 144 | background: -ms-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* IE10+ */ 145 | background: radial-gradient(ellipse at center, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* W3C */ 146 | } 147 | 148 | .ds-led-cool-active { 149 | color: rgb(74, 159, 255); 150 | background: -moz-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%, rgba(48,144,209,0.26) 100%); /* FF3.6+ */ 151 | background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(124,197,239,1)), color-stop(100%,rgba(48,144,209,0.26))); /* Chrome,Safari4+ */ 152 | background: -webkit-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* Chrome10+,Safari5.1+ */ 153 | background: -o-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* Opera 12+ */ 154 | background: -ms-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* IE10+ */ 155 | background: radial-gradient(ellipse at center, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* W3C */ 156 | } 157 | 158 | .ds-led-heat-active { 159 | color: rgb(214, 42, 0); 160 | background: -moz-radial-gradient(center, ellipse cover, rgba(214,25,25,1) 0%, rgba(214,42,0,0.26) 100%); /* FF3.6+ */ 161 | background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(214,25,25,1)), color-stop(100%,rgba(214,42,0,0.26))); /* Chrome,Safari4+ */ 162 | background: -webkit-radial-gradient(center, ellipse cover, rgba(214,25,25,1) 0%,rgba(214,42,0,0.26) 100%); /* Chrome10+,Safari5.1+ */ 163 | background: -o-radial-gradient(center, ellipse cover, rgba(214,25,25,1) 0%,rgba(214,42,0,0.26) 100%); /* Opera 12+ */ 164 | background: -ms-radial-gradient(center, ellipse cover, rgba(214,25,25,1) 0%,rgba(214,42,0,0.26) 100%); /* IE10+ */ 165 | background: radial-gradient(ellipse at center, rgba(214,25,25,1) 0%,rgba(214,42,0,0.26) 100%); /* W3C */ 166 | } 167 | 168 | .ds-led-air-active { 169 | color: rgb(240, 240, 240); 170 | background: -moz-radial-gradient(center, ellipse cover, rgba(221,221,221,1) 0%, rgba(221,221,221,0.26) 100%); /* FF3.6+ */ 171 | background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(221,221,221,1)), color-stop(100%,rgba(221,221,221,0.26))); /* Chrome,Safari4+ */ 172 | background: -webkit-radial-gradient(center, ellipse cover, rgba(221,221,221,1) 0%,rgba(221,221,221,0.26) 100%); /* Chrome10+,Safari5.1+ */ 173 | background: -o-radial-gradient(center, ellipse cover, rgba(221,221,221,1) 0%,rgba(221,221,221,0.26) 100%); /* Opera 12+ */ 174 | background: -ms-radial-gradient(center, ellipse cover, rgba(221,221,221,1) 0%,rgba(221,221,221,0.26) 100%); /* IE10+ */ 175 | background: radial-gradient(ellipse at center, rgba(221,221,221,1) 0%,rgba(221,221,221,0.26) 100%); /* W3C */ 176 | } 177 | 178 | .ds-trend { 179 | top: 0; 180 | text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.25), -1px -1px 0 rgba(0, 0, 0, 0.4); 181 | color: rgb(233, 233, 233); 182 | font-size: 20px; 183 | } 184 | 185 | .ds-target { 186 | color: #75890c; 187 | } 188 | 189 | .ds-state { 190 | border: none; 191 | width: initial; 192 | text-align: center; 193 | width: 168px; 194 | padding: 0; 195 | } 196 | 197 | .ds-state { 198 | border: none; 199 | width: initial; 200 | text-align: center; 201 | width: 210px; 202 | padding: 0; 203 | } 204 | 205 | .ds-text { 206 | border: none; 207 | width: initial; 208 | font-family: sans-serif; 209 | font-size: 32px; 210 | } 211 | 212 | .progress { 213 | -webkit-border-radius: 0; 214 | -moz-border-radius: 0; 215 | background: #3f3e3a; 216 | border-color: #000000; 217 | border-top: 1px solid #b9b6af; 218 | margin: 0; 219 | -webkit-border-bottom-left-radius: 7px; 220 | -webkit-border-bottom-right-radius: 7px; 221 | -moz-border-radius-bottomleft: 7px; 222 | -moz-border-radius-bottomright: 7px; 223 | } 224 | 225 | .progress-bar { 226 | background-color: #75890c; 227 | font-family: "Digi"; 228 | font-size: 16px; 229 | } 230 | 231 | 232 | .panel-default { 233 | -webkit-border-radius: 7px; 234 | -moz-border-radius: 7px; 235 | border-radius: 7px; 236 | -moz-box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.55); 237 | -webkit-box-shadow: 0 0 1.5em 0 rgba(0,0,0,0.55); 238 | box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.55); 239 | margin-top: 15px; 240 | background: #3F3E3A url('/picoreflow/assets/images/panel_bg.png') repeat; 241 | } 242 | 243 | .panel-heading { 244 | background: #fafafa url('/picoreflow/assets/images/page_bg.png') repeat-x; 245 | overflow: hidden; 246 | padding: 10px; 247 | -webkit-border-top-left-radius: 5px; 248 | -webkit-border-top-right-radius: 5px; 249 | -moz-border-radius-topleft: 5px; 250 | -moz-border-radius-topright: 5px; 251 | border-top-left-radius: 5px; 252 | border-top-right-radius: 5px; 253 | } 254 | 255 | .panel-body { 256 | -moz-box-shadow: inset 0 0 42px 0 #000; 257 | -webkit-box-shadow: inset 0 0 42px 0 #000; 258 | box-shadow: inset 0 0 42px 0 #000; 259 | -webkit-border-bottom-left-radius: 7px; 260 | -webkit-border-bottom-right-radius: 7px; 261 | -moz-border-radius-bottomleft: 7px; 262 | -moz-border-radius-bottomright: 7px; 263 | background: rgba(0,0,0,0.2) 264 | } 265 | 266 | #profile_selector { 267 | border: 1px solid rgb(194, 194, 194); 268 | -webkit-border-radius: 5px; 269 | -moz-border-radius: 5px; 270 | border-radius: 5px; 271 | background: rgb(229,229,229); 272 | background: -moz-linear-gradient(top, rgba(229,229,229,1) 0%, rgba(255,255,255,1) 100%); 273 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(229,229,229,1)), color-stop(100%,rgba(255,255,255,1))); 274 | background: -webkit-linear-gradient(top, rgba(229,229,229,1) 0%,rgba(255,255,255,1) 100%); 275 | background: -o-linear-gradient(top, rgba(229,229,229,1) 0%,rgba(255,255,255,1) 100%); 276 | background: -ms-linear-gradient(top, rgba(229,229,229,1) 0%,rgba(255,255,255,1) 100%); 277 | background: linear-gradient(to bottom, rgba(229,229,229,1) 0%,rgba(255,255,255,1) 100%); 278 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e5e5e5', endColorstr='#ffffff',GradientType=0 ); 279 | padding: 5px; 280 | padding-bottom: 3px; 281 | padding-top: 3px; 282 | -moz-box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.05),inset 0 0 9px 2px #000; 283 | -webkit-box-shadow: 0 0 1.5em 0 rgba(0,0,0,0.05),inset 0 0 0 0 #000; 284 | box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.05),inset 0 0 0 0 #000; 285 | } 286 | 287 | .select2-container { 288 | position: relative; 289 | top: -3px; 290 | width: 190px; 291 | height: 34px; 292 | } 293 | 294 | .select2-container .select2-choice { 295 | line-height: 32px; 296 | } 297 | 298 | .graph { 299 | width: 100%; 300 | height: 300px; 301 | font-size: 14px; 302 | line-height: 1.2em; 303 | } 304 | 305 | .edit-points { 306 | margin-bottom: 5px; 307 | } 308 | 309 | .edit-points h3 { 310 | margin-top: 5px; 311 | margin-bottom: 15px; 312 | } 313 | 314 | .edit-points .row{ 315 | margin-bottom: 5px; 316 | } 317 | 318 | .btn-success { 319 | background: rgb(164,179,87); 320 | background: -moz-linear-gradient(top, rgba(164,179,87,1) 0%, rgba(117,137,12,1) 100%); 321 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(164,179,87,1)), color-stop(100%,rgba(117,137,12,1))); 322 | background: -webkit-linear-gradient(top, rgba(164,179,87,1) 0%,rgba(117,137,12,1) 100%); 323 | background: -o-linear-gradient(top, rgba(164,179,87,1) 0%,rgba(117,137,12,1) 100%); 324 | background: -ms-linear-gradient(top, rgba(164,179,87,1) 0%,rgba(117,137,12,1) 100%); 325 | background: linear-gradient(to bottom, rgba(164,179,87,1) 0%,rgba(117,137,12,1) 100%); 326 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a4b357', endColorstr='#75890c',GradientType=0 ); 327 | background-color: #75890c; 328 | } 329 | 330 | .modal-content { 331 | background-image: linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%); 332 | } 333 | 334 | .modal-body { 335 | background: #fafafa; 336 | } 337 | 338 | .modal-footer { 339 | margin-top: 0; 340 | } 341 | 342 | .modal-body .table { 343 | margin-bottom: 0; 344 | } 345 | 346 | .select2-container .select2-choice { 347 | height: 34px; 348 | margin-top: 1px; 349 | } 350 | 351 | .modal.fade.in { 352 | top: 10%; 353 | } 354 | 355 | .alert { 356 | background-image: -webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8)); 357 | background-image: -webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%); 358 | background-image: -moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%); 359 | background-image: linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%); 360 | background-repeat: repeat-x; 361 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0); 362 | -moz-box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.75); 363 | -webkit-box-shadow: 0 0 1.5em 0 rgba(0,0,0,0.75); 364 | box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.75); 365 | } 366 | 367 | .alert-success { 368 | background: rgb(164,179,87); 369 | background: -moz-linear-gradient(top, rgba(164,179,87,1) 0%, rgba(117,137,12,1) 100%); 370 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(164,179,87,1)), color-stop(100%,rgba(117,137,12,1))); 371 | background: -webkit-linear-gradient(top, rgba(164,179,87,1) 0%,rgba(117,137,12,1) 100%); 372 | background: -o-linear-gradient(top, rgba(164,179,87,1) 0%,rgba(117,137,12,1) 100%); 373 | background: -ms-linear-gradient(top, rgba(164,179,87,1) 0%,rgba(117,137,12,1) 100%); 374 | background: linear-gradient(to bottom, rgba(164,179,87,1) 0%,rgba(117,137,12,1) 100%); 375 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a4b357', endColorstr='#75890c',GradientType=0 ); 376 | color: #fff; 377 | } 378 | 379 | .alert-error { 380 | background: rgb(206,57,20); 381 | background: -moz-linear-gradient(top, rgba(206,57,20,1) 0%, rgba(163,38,0,1) 100%); 382 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(206,57,20,1)), color-stop(100%,rgba(163,38,0,1))); 383 | background: -webkit-linear-gradient(top, rgba(206,57,20,1) 0%,rgba(163,38,0,1) 100%); 384 | background: -o-linear-gradient(top, rgba(206,57,20,1) 0%,rgba(163,38,0,1) 100%); 385 | background: -ms-linear-gradient(top, rgba(206,57,20,1) 0%,rgba(163,38,0,1) 100%); 386 | background: linear-gradient(to bottom, rgba(206,57,20,1) 0%,rgba(163,38,0,1) 100%); 387 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ce3914', endColorstr='#a32600',GradientType=0 ); 388 | color: #fff; 389 | } 390 | 391 | .modal-body td { 392 | width: 50%; 393 | } 394 | 395 | .table-responsive { 396 | overflow-x: hidden; 397 | overflow-y: hidden; 398 | } 399 | -------------------------------------------------------------------------------- /public/assets/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.2 by @fat and @mdo 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | * 6 | * Designed and built with all the love in the world by @mdo and @fat. 7 | */ 8 | 9 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,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,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#e0e0e0));background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-moz-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe0e0e0',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.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-primary{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#2d6ca2));background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-moz-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);background-repeat:repeat-x;border-color:#2b669a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff2d6ca2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.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-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#419641));background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);background-repeat:repeat-x;border-color:#3e8f3e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff419641',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.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-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#eb9316));background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);background-repeat:repeat-x;border-color:#e38d13;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffeb9316',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.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-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c12e2a));background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);background-repeat:repeat-x;border-color:#b92c28;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc12e2a',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.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-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#2aabd2));background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);background-repeat:repeat-x;border-color:#28a4c9;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2aabd2',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.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}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar-default{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#f8f8f8));background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-moz-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f3f3f3));background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff3f3f3',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.075);box-shadow:inset 0 3px 9px rgba(0,0,0,0.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-gradient(linear,left 0,left 100%,from(#3c3c3c),to(#222));background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-moz-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-gradient(linear,left 0,left 100%,from(#222),to(#282828));background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-moz-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff282828',GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,0.25);box-shadow:inset 0 3px 9px rgba(0,0,0,0.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,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,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#c8e5bc));background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#b9def0));background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#f8efc0));background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#e7c3c3));background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-moz-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,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-gradient(linear,left 0,left 100%,from(#428bca),to(#3278b3));background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-moz-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#d0e9c6));background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#c4e3f3));background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#faf2cc));background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#ebcccc));background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-moz-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-gradient(linear,left 0,left 100%,from(#e8e8e8),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-moz-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} -------------------------------------------------------------------------------- /public/assets/css/select2.css: -------------------------------------------------------------------------------- 1 | /* 2 | Version: 3.4.1 Timestamp: Thu Jun 27 18:02:10 PDT 2013 3 | */ 4 | .select2-container { 5 | margin: 0; 6 | position: relative; 7 | display: inline-block; 8 | /* inline-block for ie7 */ 9 | zoom: 1; 10 | *display: inline; 11 | vertical-align: middle; 12 | } 13 | 14 | .select2-container, 15 | .select2-drop, 16 | .select2-search, 17 | .select2-search input{ 18 | /* 19 | Force border-box so that % widths fit the parent 20 | container without overlap because of margin/padding. 21 | 22 | More Info : http://www.quirksmode.org/css/box.html 23 | */ 24 | -webkit-box-sizing: border-box; /* webkit */ 25 | -khtml-box-sizing: border-box; /* konqueror */ 26 | -moz-box-sizing: border-box; /* firefox */ 27 | -ms-box-sizing: border-box; /* ie */ 28 | box-sizing: border-box; /* css3 */ 29 | } 30 | 31 | .select2-container .select2-choice { 32 | display: block; 33 | height: 26px; 34 | padding: 0 0 0 8px; 35 | overflow: hidden; 36 | position: relative; 37 | 38 | border: 1px solid #aaa; 39 | white-space: nowrap; 40 | line-height: 26px; 41 | color: #444; 42 | text-decoration: none; 43 | 44 | -webkit-border-radius: 4px; 45 | -moz-border-radius: 4px; 46 | border-radius: 4px; 47 | 48 | -webkit-background-clip: padding-box; 49 | -moz-background-clip: padding; 50 | background-clip: padding-box; 51 | 52 | -webkit-touch-callout: none; 53 | -webkit-user-select: none; 54 | -khtml-user-select: none; 55 | -moz-user-select: none; 56 | -ms-user-select: none; 57 | user-select: none; 58 | 59 | background-color: #fff; 60 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white)); 61 | background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%); 62 | background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%); 63 | background-image: -o-linear-gradient(bottom, #eeeeee 0%, #ffffff 50%); 64 | background-image: -ms-linear-gradient(top, #ffffff 0%, #eeeeee 50%); 65 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0); 66 | background-image: linear-gradient(top, #ffffff 0%, #eeeeee 50%); 67 | } 68 | 69 | .select2-container.select2-drop-above .select2-choice { 70 | border-bottom-color: #aaa; 71 | 72 | -webkit-border-radius:0 0 4px 4px; 73 | -moz-border-radius:0 0 4px 4px; 74 | border-radius:0 0 4px 4px; 75 | 76 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.9, white)); 77 | background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 90%); 78 | background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 90%); 79 | background-image: -o-linear-gradient(bottom, #eeeeee 0%, white 90%); 80 | background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 90%); 81 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); 82 | background-image: linear-gradient(top, #eeeeee 0%,#ffffff 90%); 83 | } 84 | 85 | .select2-container.select2-allowclear .select2-choice .select2-chosen { 86 | margin-right: 42px; 87 | } 88 | 89 | .select2-container .select2-choice > .select2-chosen { 90 | margin-right: 26px; 91 | display: block; 92 | overflow: hidden; 93 | 94 | white-space: nowrap; 95 | 96 | -ms-text-overflow: ellipsis; 97 | -o-text-overflow: ellipsis; 98 | text-overflow: ellipsis; 99 | } 100 | 101 | .select2-container .select2-choice abbr { 102 | display: none; 103 | width: 12px; 104 | height: 12px; 105 | position: absolute; 106 | right: 24px; 107 | top: 8px; 108 | 109 | font-size: 1px; 110 | text-decoration: none; 111 | 112 | border: 0; 113 | background: url('select2.png') right top no-repeat; 114 | cursor: pointer; 115 | outline: 0; 116 | } 117 | 118 | .select2-container.select2-allowclear .select2-choice abbr { 119 | display: inline-block; 120 | } 121 | 122 | .select2-container .select2-choice abbr:hover { 123 | background-position: right -11px; 124 | cursor: pointer; 125 | } 126 | 127 | .select2-drop-undermask { 128 | border: 0; 129 | margin: 0; 130 | padding: 0; 131 | position: absolute; 132 | left: 0; 133 | top: 0; 134 | z-index: 9998; 135 | background-color: transparent; 136 | filter: alpha(opacity=0); 137 | } 138 | 139 | .select2-drop-mask { 140 | border: 0; 141 | margin: 0; 142 | padding: 0; 143 | position: absolute; 144 | left: 0; 145 | top: 0; 146 | z-index: 9998; 147 | /* styles required for IE to work */ 148 | background-color: #fff; 149 | opacity: 0; 150 | filter: alpha(opacity=0); 151 | } 152 | 153 | .select2-drop { 154 | width: 100%; 155 | margin-top: -1px; 156 | position: absolute; 157 | z-index: 9999; 158 | top: 100%; 159 | 160 | background: #fff; 161 | color: #000; 162 | border: 1px solid #aaa; 163 | border-top: 0; 164 | 165 | -webkit-border-radius: 0 0 4px 4px; 166 | -moz-border-radius: 0 0 4px 4px; 167 | border-radius: 0 0 4px 4px; 168 | 169 | -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 170 | -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 171 | box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 172 | } 173 | 174 | .select2-drop-auto-width { 175 | border-top: 1px solid #aaa; 176 | width: auto; 177 | } 178 | 179 | .select2-drop-auto-width .select2-search { 180 | padding-top: 4px; 181 | } 182 | 183 | .select2-drop.select2-drop-above { 184 | margin-top: 1px; 185 | border-top: 1px solid #aaa; 186 | border-bottom: 0; 187 | 188 | -webkit-border-radius: 4px 4px 0 0; 189 | -moz-border-radius: 4px 4px 0 0; 190 | border-radius: 4px 4px 0 0; 191 | 192 | -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 193 | -moz-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 194 | box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 195 | } 196 | 197 | .select2-drop-active { 198 | border: 1px solid #75890c; 199 | border-top: none; 200 | } 201 | 202 | .select2-drop.select2-drop-above.select2-drop-active { 203 | border-top: 1px solid #75890c; 204 | } 205 | 206 | .select2-container .select2-choice .select2-arrow { 207 | display: inline-block; 208 | width: 18px; 209 | height: 100%; 210 | position: absolute; 211 | right: 0; 212 | top: 0; 213 | 214 | border-left: 1px solid #aaa; 215 | -webkit-border-radius: 0 4px 4px 0; 216 | -moz-border-radius: 0 4px 4px 0; 217 | border-radius: 0 4px 4px 0; 218 | 219 | -webkit-background-clip: padding-box; 220 | -moz-background-clip: padding; 221 | background-clip: padding-box; 222 | 223 | background: #ccc; 224 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); 225 | background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); 226 | background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); 227 | background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%); 228 | background-image: -ms-linear-gradient(top, #cccccc 0%, #eeeeee 60%); 229 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0); 230 | background-image: linear-gradient(top, #cccccc 0%, #eeeeee 60%); 231 | } 232 | 233 | .select2-container .select2-choice .select2-arrow b { 234 | display: block; 235 | width: 100%; 236 | height: 100%; 237 | background: url('select2.png') no-repeat 0 1px; 238 | } 239 | 240 | .select2-search { 241 | display: inline-block; 242 | width: 100%; 243 | min-height: 26px; 244 | margin: 0; 245 | padding-left: 4px; 246 | padding-right: 4px; 247 | 248 | position: relative; 249 | z-index: 10000; 250 | 251 | white-space: nowrap; 252 | } 253 | 254 | .select2-search input { 255 | width: 100%; 256 | height: auto !important; 257 | min-height: 26px; 258 | padding: 4px 20px 4px 5px; 259 | margin: 0; 260 | 261 | outline: 0; 262 | font-family: sans-serif; 263 | font-size: 1em; 264 | 265 | border: 1px solid #aaa; 266 | -webkit-border-radius: 0; 267 | -moz-border-radius: 0; 268 | border-radius: 0; 269 | 270 | -webkit-box-shadow: none; 271 | -moz-box-shadow: none; 272 | box-shadow: none; 273 | 274 | background: #fff url('select2.png') no-repeat 100% -22px; 275 | background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 276 | background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 277 | background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 278 | background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 279 | background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); 280 | background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%); 281 | } 282 | 283 | .select2-drop.select2-drop-above .select2-search input { 284 | margin-top: 4px; 285 | } 286 | 287 | .select2-search input.select2-active { 288 | background: #fff url('select2-spinner.gif') no-repeat 100%; 289 | background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 290 | background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 291 | background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 292 | background: url('select2-spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 293 | background: url('select2-spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); 294 | background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(top, #ffffff 85%, #eeeeee 99%); 295 | } 296 | 297 | .select2-container-active .select2-choice, 298 | .select2-container-active .select2-choices { 299 | border: 1px solid #75890c; 300 | outline: none; 301 | 302 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 303 | -moz-box-shadow: 0 0 5px rgba(0,0,0,.3); 304 | box-shadow: 0 0 5px rgba(0,0,0,.3); 305 | } 306 | 307 | .select2-dropdown-open .select2-choice { 308 | border-bottom-color: transparent; 309 | -webkit-box-shadow: 0 1px 0 #fff inset; 310 | -moz-box-shadow: 0 1px 0 #fff inset; 311 | box-shadow: 0 1px 0 #fff inset; 312 | 313 | -webkit-border-bottom-left-radius: 0; 314 | -moz-border-radius-bottomleft: 0; 315 | border-bottom-left-radius: 0; 316 | 317 | -webkit-border-bottom-right-radius: 0; 318 | -moz-border-radius-bottomright: 0; 319 | border-bottom-right-radius: 0; 320 | 321 | background-color: #eee; 322 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee)); 323 | background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%); 324 | background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%); 325 | background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%); 326 | background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%); 327 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); 328 | background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%); 329 | } 330 | 331 | .select2-dropdown-open.select2-drop-above .select2-choice, 332 | .select2-dropdown-open.select2-drop-above .select2-choices { 333 | border: 1px solid #75890c; 334 | border-top-color: transparent; 335 | 336 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, white), color-stop(0.5, #eeeeee)); 337 | background-image: -webkit-linear-gradient(center top, white 0%, #eeeeee 50%); 338 | background-image: -moz-linear-gradient(center top, white 0%, #eeeeee 50%); 339 | background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); 340 | background-image: -ms-linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); 341 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); 342 | background-image: linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); 343 | } 344 | 345 | .select2-dropdown-open .select2-choice .select2-arrow { 346 | background: transparent; 347 | border-left: none; 348 | filter: none; 349 | } 350 | .select2-dropdown-open .select2-choice .select2-arrow b { 351 | background-position: -18px 1px; 352 | } 353 | 354 | /* results */ 355 | .select2-results { 356 | max-height: 200px; 357 | padding: 0 0 0 4px; 358 | margin: 4px 4px 4px 0; 359 | position: relative; 360 | overflow-x: hidden; 361 | overflow-y: auto; 362 | -webkit-tap-highlight-color: rgba(0,0,0,0); 363 | } 364 | 365 | .select2-results ul.select2-result-sub { 366 | margin: 0; 367 | padding-left: 0; 368 | } 369 | 370 | .select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px } 371 | .select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 40px } 372 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 60px } 373 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 80px } 374 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 100px } 375 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 110px } 376 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 120px } 377 | 378 | .select2-results li { 379 | list-style: none; 380 | display: list-item; 381 | background-image: none; 382 | } 383 | 384 | .select2-results li.select2-result-with-children > .select2-result-label { 385 | font-weight: bold; 386 | } 387 | 388 | .select2-results .select2-result-label { 389 | padding: 3px 7px 4px; 390 | margin: 0; 391 | cursor: pointer; 392 | 393 | min-height: 1em; 394 | 395 | -webkit-touch-callout: none; 396 | -webkit-user-select: none; 397 | -khtml-user-select: none; 398 | -moz-user-select: none; 399 | -ms-user-select: none; 400 | user-select: none; 401 | } 402 | 403 | .select2-results .select2-highlighted { 404 | background: #75890c; 405 | color: #fff; 406 | } 407 | 408 | .select2-results li em { 409 | background: #feffde; 410 | font-style: normal; 411 | } 412 | 413 | .select2-results .select2-highlighted em { 414 | background: transparent; 415 | } 416 | 417 | .select2-results .select2-highlighted ul { 418 | background: white; 419 | color: #000; 420 | } 421 | 422 | 423 | .select2-results .select2-no-results, 424 | .select2-results .select2-searching, 425 | .select2-results .select2-selection-limit { 426 | background: #f4f4f4; 427 | display: list-item; 428 | } 429 | 430 | /* 431 | disabled look for disabled choices in the results dropdown 432 | */ 433 | .select2-results .select2-disabled.select2-highlighted { 434 | color: #666; 435 | background: #f4f4f4; 436 | display: list-item; 437 | cursor: default; 438 | } 439 | .select2-results .select2-disabled { 440 | background: #f4f4f4; 441 | display: list-item; 442 | cursor: default; 443 | } 444 | 445 | .select2-results .select2-selected { 446 | display: none; 447 | } 448 | 449 | .select2-more-results.select2-active { 450 | background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%; 451 | } 452 | 453 | .select2-more-results { 454 | background: #f4f4f4; 455 | display: list-item; 456 | } 457 | 458 | /* disabled styles */ 459 | 460 | .select2-container.select2-container-disabled .select2-choice { 461 | background-color: #f4f4f4; 462 | background-image: none; 463 | border: 1px solid #ddd; 464 | cursor: default; 465 | } 466 | 467 | .select2-container.select2-container-disabled .select2-choice .select2-arrow { 468 | background-color: #f4f4f4; 469 | background-image: none; 470 | border-left: 0; 471 | } 472 | 473 | .select2-container.select2-container-disabled .select2-choice abbr { 474 | display: none; 475 | } 476 | 477 | 478 | /* multiselect */ 479 | 480 | .select2-container-multi .select2-choices { 481 | height: auto !important; 482 | height: 1%; 483 | margin: 0; 484 | padding: 0; 485 | position: relative; 486 | 487 | border: 1px solid #aaa; 488 | cursor: text; 489 | overflow: hidden; 490 | 491 | background-color: #fff; 492 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); 493 | background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 494 | background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 495 | background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 496 | background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%); 497 | background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%); 498 | } 499 | 500 | .select2-locked { 501 | padding: 3px 5px 3px 5px !important; 502 | } 503 | 504 | .select2-container-multi .select2-choices { 505 | min-height: 26px; 506 | } 507 | 508 | .select2-container-multi.select2-container-active .select2-choices { 509 | border: 1px solid #75890c; 510 | outline: none; 511 | 512 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 513 | -moz-box-shadow: 0 0 5px rgba(0,0,0,.3); 514 | box-shadow: 0 0 5px rgba(0,0,0,.3); 515 | } 516 | .select2-container-multi .select2-choices li { 517 | float: left; 518 | list-style: none; 519 | } 520 | .select2-container-multi .select2-choices .select2-search-field { 521 | margin: 0; 522 | padding: 0; 523 | white-space: nowrap; 524 | } 525 | 526 | .select2-container-multi .select2-choices .select2-search-field input { 527 | padding: 5px; 528 | margin: 1px 0; 529 | 530 | font-family: sans-serif; 531 | font-size: 100%; 532 | color: #666; 533 | outline: 0; 534 | border: 0; 535 | -webkit-box-shadow: none; 536 | -moz-box-shadow: none; 537 | box-shadow: none; 538 | background: transparent !important; 539 | } 540 | 541 | .select2-container-multi .select2-choices .select2-search-field input.select2-active { 542 | background: #fff url('select2-spinner.gif') no-repeat 100% !important; 543 | } 544 | 545 | .select2-default { 546 | color: #999 !important; 547 | } 548 | 549 | .select2-container-multi .select2-choices .select2-search-choice { 550 | padding: 3px 5px 3px 18px; 551 | margin: 3px 0 3px 5px; 552 | position: relative; 553 | 554 | line-height: 13px; 555 | color: #333; 556 | cursor: default; 557 | border: 1px solid #aaaaaa; 558 | 559 | -webkit-border-radius: 3px; 560 | -moz-border-radius: 3px; 561 | border-radius: 3px; 562 | 563 | -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); 564 | -moz-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); 565 | box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); 566 | 567 | -webkit-background-clip: padding-box; 568 | -moz-background-clip: padding; 569 | background-clip: padding-box; 570 | 571 | -webkit-touch-callout: none; 572 | -webkit-user-select: none; 573 | -khtml-user-select: none; 574 | -moz-user-select: none; 575 | -ms-user-select: none; 576 | user-select: none; 577 | 578 | background-color: #e4e4e4; 579 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0 ); 580 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); 581 | background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 582 | background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 583 | background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 584 | background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 585 | background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); 586 | } 587 | .select2-container-multi .select2-choices .select2-search-choice .select2-chosen { 588 | cursor: default; 589 | } 590 | .select2-container-multi .select2-choices .select2-search-choice-focus { 591 | background: #d4d4d4; 592 | } 593 | 594 | .select2-search-choice-close { 595 | display: block; 596 | width: 12px; 597 | height: 13px; 598 | position: absolute; 599 | right: 3px; 600 | top: 4px; 601 | 602 | font-size: 1px; 603 | outline: none; 604 | background: url('select2.png') right top no-repeat; 605 | } 606 | 607 | .select2-container-multi .select2-search-choice-close { 608 | left: 3px; 609 | } 610 | 611 | .select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover { 612 | background-position: right -11px; 613 | } 614 | .select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close { 615 | background-position: right -11px; 616 | } 617 | 618 | /* disabled styles */ 619 | .select2-container-multi.select2-container-disabled .select2-choices{ 620 | background-color: #f4f4f4; 621 | background-image: none; 622 | border: 1px solid #ddd; 623 | cursor: default; 624 | } 625 | 626 | .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice { 627 | padding: 3px 5px 3px 5px; 628 | border: 1px solid #ddd; 629 | background-image: none; 630 | background-color: #f4f4f4; 631 | } 632 | 633 | .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; 634 | background:none; 635 | } 636 | /* end multiselect */ 637 | 638 | 639 | .select2-result-selectable .select2-match, 640 | .select2-result-unselectable .select2-match { 641 | text-decoration: underline; 642 | } 643 | 644 | .select2-offscreen, .select2-offscreen:focus { 645 | clip: rect(0 0 0 0); 646 | width: 1px; 647 | height: 1px; 648 | border: 0; 649 | margin: 0; 650 | padding: 0; 651 | overflow: hidden; 652 | position: absolute; 653 | outline: 0; 654 | left: 0px; 655 | } 656 | 657 | .select2-display-none { 658 | display: none; 659 | } 660 | 661 | .select2-measure-scrollbar { 662 | position: absolute; 663 | top: -10000px; 664 | left: -10000px; 665 | width: 100px; 666 | height: 100px; 667 | overflow: scroll; 668 | } 669 | /* Retina-ize icons */ 670 | 671 | @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { 672 | .select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice .select2-arrow b { 673 | background-image: url('select2x2.png') !important; 674 | background-repeat: no-repeat !important; 675 | background-size: 60px 40px !important; 676 | } 677 | .select2-search input { 678 | background-position: 100% -21px !important; 679 | } 680 | } 681 | -------------------------------------------------------------------------------- /public/assets/js/picoreflow.js: -------------------------------------------------------------------------------- 1 | var state = "IDLE"; 2 | var state_last = ""; 3 | var graph = [ 'profile', 'live']; 4 | var points = []; 5 | var profiles = []; 6 | var time_mode = 0; 7 | var selected_profile = 0; 8 | var selected_profile_name = 'leadfree'; 9 | var temp_scale = "c"; 10 | var time_scale_slope = "s"; 11 | var time_scale_profile = "s"; 12 | var time_scale_long = "Seconds"; 13 | var temp_scale_display = "C"; 14 | var kwh_rate = 0.26; 15 | var currency_type = "EUR"; 16 | 17 | var host = "ws://" + window.location.hostname + ":" + window.location.port; 18 | var ws_status = new WebSocket(host+"/status"); 19 | var ws_control = new WebSocket(host+"/control"); 20 | var ws_config = new WebSocket(host+"/config"); 21 | var ws_storage = new WebSocket(host+"/storage"); 22 | 23 | 24 | if(window.webkitRequestAnimationFrame) window.requestAnimationFrame = window.webkitRequestAnimationFrame; 25 | 26 | graph.profile = 27 | { 28 | label: "Profile", 29 | data: [], 30 | points: { show: false }, 31 | color: "#75890c", 32 | draggable: false 33 | }; 34 | 35 | graph.live = 36 | { 37 | label: "Live", 38 | data: [], 39 | points: { show: false }, 40 | color: "#d8d3c5", 41 | draggable: false 42 | }; 43 | 44 | 45 | function updateProfile(id) 46 | { 47 | selected_profile = id; 48 | selected_profile_name = profiles[id].name; 49 | var job_seconds = profiles[id].data.length === 0 ? 0 : parseInt(profiles[id].data[profiles[id].data.length-1][0]); 50 | var kwh = (3850*job_seconds/3600/1000).toFixed(2); 51 | var cost = (kwh*kwh_rate).toFixed(2); 52 | var job_time = new Date(job_seconds * 1000).toISOString().substr(11, 8); 53 | $('#sel_prof').html(profiles[id].name); 54 | $('#sel_prof_eta').html(job_time); 55 | $('#sel_prof_cost').html(kwh + ' kWh ('+ currency_type +': '+ cost +')'); 56 | graph.profile.data = profiles[id].data; 57 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions()); 58 | } 59 | 60 | function deleteProfile() 61 | { 62 | var profile = { "type": "profile", "data": "", "name": selected_profile_name }; 63 | var delete_struct = { "cmd": "DELETE", "profile": profile }; 64 | 65 | var delete_cmd = JSON.stringify(delete_struct); 66 | console.log("Delete profile:" + selected_profile_name); 67 | 68 | ws_storage.send(delete_cmd); 69 | 70 | ws_storage.send('GET'); 71 | selected_profile_name = profiles[0].name; 72 | 73 | state="IDLE"; 74 | $('#edit').hide(); 75 | $('#profile_selector').show(); 76 | $('#btn_controls').show(); 77 | $('#status').slideDown(); 78 | $('#profile_table').slideUp(); 79 | $('#e2').select2('val', 0); 80 | graph.profile.points.show = false; 81 | graph.profile.draggable = false; 82 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions()); 83 | } 84 | 85 | 86 | function updateProgress(percentage) 87 | { 88 | if(state=="RUNNING") 89 | { 90 | if(percentage > 100) percentage = 100; 91 | $('#progressBar').css('width', percentage+'%'); 92 | if(percentage>5) $('#progressBar').html(parseInt(percentage)+'%'); 93 | } 94 | else 95 | { 96 | $('#progressBar').css('width', 0+'%'); 97 | $('#progressBar').html(''); 98 | } 99 | } 100 | 101 | function updateProfileTable() 102 | { 103 | var dps = 0; 104 | var slope = ""; 105 | var color = ""; 106 | 107 | var html = '

Profile Points

'; 108 | html += ''; 109 | 110 | for(var i=0; i=1) dps = ((graph.profile.data[i][1]-graph.profile.data[i-1][1])/(graph.profile.data[i][0]-graph.profile.data[i-1][0]) * 10) / 10; 114 | if (dps > 0) { slope = "up"; color="rgba(206, 5, 5, 1)"; } else 115 | if (dps < 0) { slope = "down"; color="rgba(23, 108, 204, 1)"; dps *= -1; } else 116 | if (dps == 0) { slope = "right"; color="grey"; } 117 | 118 | html += ''; 119 | html += ''; 120 | html += ''; 121 | html += ''; 122 | html += ''; 123 | } 124 | 125 | html += '
#Target Time in ' + time_scale_long+ 'Target Temperature in °'+temp_scale_display+'Slope in °'+temp_scale_display+'/'+time_scale_slope+'

' + (i+1) + '

 
'; 126 | 127 | $('#profile_table').html(html); 128 | 129 | //Link table to graph 130 | $(".form-control").change(function(e) 131 | { 132 | var id = $(this)[0].id; //e.currentTarget.attributes.id 133 | var value = parseInt($(this)[0].value); 134 | var fields = id.split("-"); 135 | var col = parseInt(fields[1]); 136 | var row = parseInt(fields[2]); 137 | 138 | if (graph.profile.data.length > 0) { 139 | if (col == 0) { 140 | graph.profile.data[row][col] = timeProfileFormatter(value,false); 141 | } 142 | else { 143 | graph.profile.data[row][col] = value; 144 | } 145 | 146 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions()); 147 | } 148 | updateProfileTable(); 149 | 150 | }); 151 | } 152 | 153 | function timeProfileFormatter(val, down) { 154 | var rval = val 155 | switch(time_scale_profile){ 156 | case "m": 157 | if (down) {rval = val / 60;} else {rval = val * 60;} 158 | break; 159 | case "h": 160 | if (down) {rval = val / 3600;} else {rval = val * 3600;} 161 | break; 162 | } 163 | return Math.round(rval); 164 | } 165 | 166 | function formatDPS(val) { 167 | var tval = val; 168 | if (time_scale_slope == "m") { 169 | tval = val * 60; 170 | } 171 | if (time_scale_slope == "h") { 172 | tval = (val * 60) * 60; 173 | } 174 | return Math.round(tval); 175 | } 176 | 177 | function hazardTemp(){ 178 | if (temp_scale == "f") { 179 | return (45 * 9 / 5) + 32 180 | } 181 | else { 182 | return 45 183 | } 184 | } 185 | 186 | function timeTickFormatter(val) 187 | { 188 | if (val < 1800) 189 | { 190 | return val; 191 | } 192 | else 193 | { 194 | var hours = Math.floor(val / (3600)); 195 | var div_min = val % (3600); 196 | var minutes = Math.floor(div_min / 60); 197 | 198 | if (hours < 10) {hours = "0"+hours;} 199 | if (minutes < 10) {minutes = "0"+minutes;} 200 | 201 | return hours+":"+minutes; 202 | } 203 | } 204 | 205 | function runTask() 206 | { 207 | var cmd = 208 | { 209 | "cmd": "RUN", 210 | "profile": profiles[selected_profile] 211 | } 212 | 213 | graph.live.data = []; 214 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions()); 215 | 216 | ws_control.send(JSON.stringify(cmd)); 217 | 218 | } 219 | 220 | function runTaskSimulation() 221 | { 222 | var cmd = 223 | { 224 | "cmd": "SIMULATE", 225 | "profile": profiles[selected_profile] 226 | } 227 | 228 | graph.live.data = []; 229 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions()); 230 | 231 | ws_control.send(JSON.stringify(cmd)); 232 | 233 | } 234 | 235 | 236 | function abortTask() 237 | { 238 | var cmd = {"cmd": "STOP"}; 239 | ws_control.send(JSON.stringify(cmd)); 240 | } 241 | 242 | function enterNewMode() 243 | { 244 | state="EDIT" 245 | $('#status').slideUp(); 246 | $('#edit').show(); 247 | $('#profile_selector').hide(); 248 | $('#btn_controls').hide(); 249 | $('#form_profile_name').attr('value', ''); 250 | $('#form_profile_name').attr('placeholder', 'Please enter a name'); 251 | graph.profile.points.show = true; 252 | graph.profile.draggable = true; 253 | graph.profile.data = []; 254 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions()); 255 | updateProfileTable(); 256 | } 257 | 258 | function enterEditMode() 259 | { 260 | state="EDIT" 261 | $('#status').slideUp(); 262 | $('#edit').show(); 263 | $('#profile_selector').hide(); 264 | $('#btn_controls').hide(); 265 | console.log(profiles); 266 | $('#form_profile_name').val(profiles[selected_profile].name); 267 | graph.profile.points.show = true; 268 | graph.profile.draggable = true; 269 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions()); 270 | updateProfileTable(); 271 | } 272 | 273 | function leaveEditMode() 274 | { 275 | selected_profile_name = $('#form_profile_name').val(); 276 | ws_storage.send('GET'); 277 | state="IDLE"; 278 | $('#edit').hide(); 279 | $('#profile_selector').show(); 280 | $('#btn_controls').show(); 281 | $('#status').slideDown(); 282 | $('#profile_table').slideUp(); 283 | graph.profile.points.show = false; 284 | graph.profile.draggable = false; 285 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions()); 286 | } 287 | 288 | function newPoint() 289 | { 290 | if(graph.profile.data.length > 0) 291 | { 292 | var pointx = parseInt(graph.profile.data[graph.profile.data.length-1][0])+15; 293 | } 294 | else 295 | { 296 | var pointx = 0; 297 | } 298 | graph.profile.data.push([pointx, Math.floor((Math.random()*230)+25)]); 299 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions()); 300 | updateProfileTable(); 301 | } 302 | 303 | function delPoint() 304 | { 305 | graph.profile.data.splice(-1,1) 306 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions()); 307 | updateProfileTable(); 308 | } 309 | 310 | function toggleTable() 311 | { 312 | if($('#profile_table').css('display') == 'none') 313 | { 314 | $('#profile_table').slideDown(); 315 | } 316 | else 317 | { 318 | $('#profile_table').slideUp(); 319 | } 320 | } 321 | 322 | function saveProfile() 323 | { 324 | name = $('#form_profile_name').val(); 325 | var rawdata = graph.plot.getData()[0].data 326 | var data = []; 327 | var last = -1; 328 | 329 | for(var i=0; i last) 332 | { 333 | data.push([rawdata[i][0], rawdata[i][1]]); 334 | } 335 | else 336 | { 337 | $.bootstrapGrowl(" ERROR 88:
An oven is not a time-machine", { 338 | ele: 'body', // which element to append to 339 | type: 'alert', // (null, 'info', 'error', 'success') 340 | offset: {from: 'top', amount: 250}, // 'top', or 'bottom' 341 | align: 'center', // ('left', 'right', or 'center') 342 | width: 385, // (integer, or 'auto') 343 | delay: 5000, 344 | allow_dismiss: true, 345 | stackup_spacing: 10 // spacing between consecutively stacked growls. 346 | }); 347 | 348 | return false; 349 | } 350 | 351 | last = rawdata[i][0]; 352 | } 353 | 354 | var profile = { "type": "profile", "data": data, "name": name } 355 | var put = { "cmd": "PUT", "profile": profile } 356 | 357 | var put_cmd = JSON.stringify(put); 358 | 359 | ws_storage.send(put_cmd); 360 | 361 | leaveEditMode(); 362 | } 363 | 364 | function getOptions() 365 | { 366 | 367 | var options = 368 | { 369 | 370 | series: 371 | { 372 | lines: 373 | { 374 | show: true 375 | }, 376 | 377 | points: 378 | { 379 | show: true, 380 | radius: 5, 381 | symbol: "circle" 382 | }, 383 | 384 | shadowSize: 3 385 | 386 | }, 387 | 388 | xaxis: 389 | { 390 | min: 0, 391 | tickColor: 'rgba(216, 211, 197, 0.2)', 392 | tickFormatter: timeTickFormatter, 393 | font: 394 | { 395 | size: 14, 396 | lineHeight: 14, weight: "normal", 397 | family: "Digi", 398 | variant: "small-caps", 399 | color: "rgba(216, 211, 197, 0.85)" 400 | } 401 | }, 402 | 403 | yaxis: 404 | { 405 | min: 0, 406 | tickDecimals: 0, 407 | draggable: false, 408 | tickColor: 'rgba(216, 211, 197, 0.2)', 409 | font: 410 | { 411 | size: 14, 412 | lineHeight: 14, 413 | weight: "normal", 414 | family: "Digi", 415 | variant: "small-caps", 416 | color: "rgba(216, 211, 197, 0.85)" 417 | } 418 | }, 419 | 420 | grid: 421 | { 422 | color: 'rgba(216, 211, 197, 0.55)', 423 | borderWidth: 1, 424 | labelMargin: 10, 425 | mouseActiveRadius: 50 426 | }, 427 | 428 | legend: 429 | { 430 | show: false 431 | } 432 | } 433 | 434 | return options; 435 | 436 | } 437 | 438 | 439 | 440 | $(document).ready(function() 441 | { 442 | 443 | if(!("WebSocket" in window)) 444 | { 445 | $('#chatLog, input, button, #examples').fadeOut("fast"); 446 | $('

Oh no, you need a browser that supports WebSockets. How about Google Chrome?

').appendTo('#container'); 447 | } 448 | else 449 | { 450 | 451 | // Status Socket //////////////////////////////// 452 | 453 | ws_status.onopen = function() 454 | { 455 | console.log("Status Socket has been opened"); 456 | 457 | $.bootstrapGrowl(" Yay
I'm alive", 458 | { 459 | ele: 'body', // which element to append to 460 | type: 'success', // (null, 'info', 'error', 'success') 461 | offset: {from: 'top', amount: 250}, // 'top', or 'bottom' 462 | align: 'center', // ('left', 'right', or 'center') 463 | width: 385, // (integer, or 'auto') 464 | delay: 2500, 465 | allow_dismiss: true, 466 | stackup_spacing: 10 // spacing between consecutively stacked growls. 467 | }); 468 | }; 469 | 470 | ws_status.onclose = function() 471 | { 472 | $.bootstrapGrowl(" ERROR 1:
Status Websocket not available", { 473 | ele: 'body', // which element to append to 474 | type: 'error', // (null, 'info', 'error', 'success') 475 | offset: {from: 'top', amount: 250}, // 'top', or 'bottom' 476 | align: 'center', // ('left', 'right', or 'center') 477 | width: 385, // (integer, or 'auto') 478 | delay: 5000, 479 | allow_dismiss: true, 480 | stackup_spacing: 10 // spacing between consecutively stacked growls. 481 | }); 482 | }; 483 | 484 | ws_status.onmessage = function(e) 485 | { 486 | x = JSON.parse(e.data); 487 | 488 | if (x.type == "backlog") 489 | { 490 | if (x.profile) 491 | { 492 | selected_profile_name = x.profile.name; 493 | $.each(profiles, function(i,v) { 494 | if(v.name == x.profile.name) { 495 | updateProfile(i); 496 | $('#e2').select2('val', i); 497 | } 498 | }); 499 | } 500 | 501 | $.each(x.log, function(i,v) { 502 | graph.live.data.push([v.runtime, v.temperature]); 503 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions()); 504 | }); 505 | } 506 | 507 | if(state!="EDIT") 508 | { 509 | state = x.state; 510 | 511 | if (state!=state_last) 512 | { 513 | if(state_last == "RUNNING") 514 | { 515 | $('#target_temp').html('---'); 516 | updateProgress(0); 517 | $.bootstrapGrowl(" Run completed", { 518 | ele: 'body', // which element to append to 519 | type: 'success', // (null, 'info', 'error', 'success') 520 | offset: {from: 'top', amount: 250}, // 'top', or 'bottom' 521 | align: 'center', // ('left', 'right', or 'center') 522 | width: 385, // (integer, or 'auto') 523 | delay: 0, 524 | allow_dismiss: true, 525 | stackup_spacing: 10 // spacing between consecutively stacked growls. 526 | }); 527 | } 528 | } 529 | 530 | if(state=="RUNNING") 531 | { 532 | $("#nav_start").hide(); 533 | $("#nav_stop").show(); 534 | 535 | graph.live.data.push([x.runtime, x.temperature]); 536 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions()); 537 | 538 | left = parseInt(x.totaltime-x.runtime); 539 | eta = new Date(left * 1000).toISOString().substr(11, 8); 540 | 541 | updateProgress(parseFloat(x.runtime)/parseFloat(x.totaltime)*100); 542 | $('#state').html('' + eta + ''); 543 | $('#target_temp').html(parseInt(x.target)); 544 | 545 | 546 | } 547 | else 548 | { 549 | $("#nav_start").show(); 550 | $("#nav_stop").hide(); 551 | $('#state').html('

'+state+'

'); 552 | } 553 | 554 | $('#act_temp').html(parseInt(x.temperature)); 555 | 556 | if (x.heat > 0.5) { $('#heat').addClass("ds-led-heat-active"); } else { $('#heat').removeClass("ds-led-heat-active"); } 557 | if (x.cool > 0.5) { $('#cool').addClass("ds-led-cool-active"); } else { $('#cool').removeClass("ds-led-cool-active"); } 558 | if (x.air > 0.5) { $('#air').addClass("ds-led-air-active"); } else { $('#air').removeClass("ds-led-air-active"); } 559 | if (x.temperature > hazardTemp()) { $('#hazard').addClass("ds-led-hazard-active"); } else { $('#hazard').removeClass("ds-led-hazard-active"); } 560 | if ((x.door == "OPEN") || (x.door == "UNKNOWN")) { $('#door').addClass("ds-led-door-open"); } else { $('#door').removeClass("ds-led-door-open"); } 561 | 562 | state_last = state; 563 | 564 | } 565 | }; 566 | 567 | // Config Socket ///////////////////////////////// 568 | 569 | ws_config.onopen = function() 570 | { 571 | ws_config.send('GET'); 572 | }; 573 | 574 | ws_config.onmessage = function(e) 575 | { 576 | console.log (e.data); 577 | x = JSON.parse(e.data); 578 | temp_scale = x.temp_scale; 579 | time_scale_slope = x.time_scale_slope; 580 | time_scale_profile = x.time_scale_profile; 581 | kwh_rate = x.kwh_rate; 582 | currency_type = x.currency_type; 583 | 584 | if (temp_scale == "c") {temp_scale_display = "C";} else {temp_scale_display = "F";} 585 | 586 | 587 | $('#act_temp_scale').html('º'+temp_scale_display); 588 | $('#target_temp_scale').html('º'+temp_scale_display); 589 | 590 | switch(time_scale_profile){ 591 | case "s": 592 | time_scale_long = "Seconds"; 593 | break; 594 | case "m": 595 | time_scale_long = "Minutes"; 596 | break; 597 | case "h": 598 | time_scale_long = "Hours"; 599 | break; 600 | } 601 | 602 | } 603 | 604 | // Control Socket //////////////////////////////// 605 | 606 | ws_control.onopen = function() 607 | { 608 | 609 | }; 610 | 611 | ws_control.onmessage = function(e) 612 | { 613 | //Data from Simulation 614 | console.log (e.data); 615 | x = JSON.parse(e.data); 616 | graph.live.data.push([x.runtime, x.temperature]); 617 | graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions()); 618 | 619 | } 620 | 621 | // Storage Socket /////////////////////////////// 622 | 623 | ws_storage.onopen = function() 624 | { 625 | ws_storage.send('GET'); 626 | }; 627 | 628 | 629 | ws_storage.onmessage = function(e) 630 | { 631 | message = JSON.parse(e.data); 632 | 633 | if(message.resp) 634 | { 635 | if(message.resp == "FAIL") 636 | { 637 | if (confirm('Overwrite?')) 638 | { 639 | message.force=true; 640 | console.log("Sending: " + JSON.stringify(message)); 641 | ws_storage.send(JSON.stringify(message)); 642 | } 643 | else 644 | { 645 | //do nothing 646 | } 647 | } 648 | 649 | return; 650 | } 651 | 652 | //the message is an array of profiles 653 | //FIXME: this should be better, maybe a {"profiles": ...} container? 654 | profiles = message; 655 | //delete old options in select 656 | $('#e2').find('option').remove().end(); 657 | // check if current selected value is a valid profile name 658 | // if not, update with first available profile name 659 | var valid_profile_names = profiles.map(function(a) {return a.name;}); 660 | if ( 661 | valid_profile_names.length > 0 && 662 | $.inArray(selected_profile_name, valid_profile_names) === -1 663 | ) { 664 | selected_profile = 0; 665 | selected_profile_name = valid_profile_names[0]; 666 | } 667 | 668 | // fill select with new options from websocket 669 | for (var i=0; i'+profile.name+''); 674 | 675 | if (profile.name == selected_profile_name) 676 | { 677 | selected_profile = i; 678 | $('#e2').select2('val', i); 679 | updateProfile(i); 680 | } 681 | } 682 | }; 683 | 684 | 685 | $("#e2").select2( 686 | { 687 | placeholder: "Select Profile", 688 | allowClear: true, 689 | minimumResultsForSearch: -1 690 | }); 691 | 692 | 693 | $("#e2").on("change", function(e) 694 | { 695 | updateProfile(e.val); 696 | }); 697 | 698 | } 699 | }); 700 | -------------------------------------------------------------------------------- /public/assets/fonts/digital-7-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 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 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /public/assets/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.0.2 by @fat and @mdo 3 | * Copyright 2013 Twitter, Inc. 4 | * Licensed under http://www.apache.org/licenses/LICENSE-2.0 5 | * 6 | * Designed and built with all the love in the world by @mdo and @fat. 7 | */ 8 | 9 | if("undefined"==typeof jQuery)throw new Error("Bootstrap 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]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,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()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.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(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active")).trigger("change");"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),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",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.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},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.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]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.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?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); --------------------------------------------------------------------------------