├── 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 | 
13 |
14 | **Curve Editor**
15 |
16 | 
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 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | Profile Name
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
100 |
101 |
102 | | Selected Profile | |
103 | | Estimated Runtime | |
104 | | Estimated Power consumption | |
105 |
106 |
107 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
124 |
125 | Do your really want to delete this profile?
126 |
127 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
144 |
145 | Do your really want to overwrite this profile?
146 |
147 |
153 |
154 |
155 |
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
';
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 |
--------------------------------------------------------------------------------
/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('').insertAfter(a(this)).on("click",b),f.trigger(d=a.Event("show.bs.dropdown")),d.isDefaultPrevented())return;f.toggleClass("open").trigger("shown.bs.dropdown"),e.focus()}return!1}},f.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var f=c(d),g=f.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&f.find(e).focus(),d.click();var h=a("[role=menu] li:not(.divider):visible a",f);if(h.length){var i=h.index(h.filter(":focus"));38==b.keyCode&&i>0&&i--,40==b.keyCode&&i').appendTo(document.body),this.$element.on("click.dismiss.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),d&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;d?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b).emulateTransitionEnd(150):b()):b&&b()};var c=a.fn.modal;a.fn.modal=function(c,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},b.DEFAULTS,e.data(),"object"==typeof c&&c);f||e.data("bs.modal",f=new b(this,g)),"string"==typeof c?f[c](d):g.show&&f.show(d)})},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f,this).one("hide",function(){c.is(":visible")&&c.focus()})}),a(document).on("show.bs.modal",".modal",function(){a(document.body).addClass("modal-open")}).on("hidden.bs.modal",".modal",function(){a(document.body).removeClass("modal-open")})}(jQuery),+function(a){"use strict";var b=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};b.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},b.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focus",i="hover"==g?"mouseleave":"blur";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},b.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},b.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show),void 0):c.show()},b.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget)[this.type](this.getDelegateOptions()).data("bs."+this.type);return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide),void 0):c.hide()},b.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){if(this.$element.trigger(b),b.isDefaultPrevented())return;var c=this.tip();this.setContent(),this.options.animation&&c.addClass("fade");var d="function"==typeof this.options.placement?this.options.placement.call(this,c[0],this.$element[0]):this.options.placement,e=/\s?auto?\s?/i,f=e.test(d);f&&(d=d.replace(e,"")||"top"),c.detach().css({top:0,left:0,display:"block"}).addClass(d),this.options.container?c.appendTo(this.options.container):c.insertAfter(this.$element);var g=this.getPosition(),h=c[0].offsetWidth,i=c[0].offsetHeight;if(f){var j=this.$element.parent(),k=d,l=document.documentElement.scrollTop||document.body.scrollTop,m="body"==this.options.container?window.innerWidth:j.outerWidth(),n="body"==this.options.container?window.innerHeight:j.outerHeight(),o="body"==this.options.container?0:j.offset().left;d="bottom"==d&&g.top+g.height+i-l>n?"top":"top"==d&&g.top-l-i<0?"bottom":"right"==d&&g.right+h>m?"left":"left"==d&&g.left-h '}),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);
--------------------------------------------------------------------------------