kopia lustrzana https://github.com/botheredbybees/kilnController
commit
d7e7eebe45
|
@ -12,6 +12,10 @@ log_format = '%(asctime)s %(levelname)s %(name)s: %(message)s'
|
|||
listening_ip = "0.0.0.0"
|
||||
listening_port = 8081
|
||||
|
||||
### Cost Estimate
|
||||
kwh_rate = 0.26 # Rate in currency_type to calculate cost to run job
|
||||
currency_type = "EUR" # Currency Symbol to show when calculating cost to run job
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# GPIO Setup (BCM SoC Numbering Schema)
|
||||
|
@ -26,6 +30,8 @@ gpio_heat = 11 # Switches zero-cross solid-state-relay
|
|||
gpio_cool = 10 # Regulates PWM for 12V DC Blower
|
||||
gpio_air = 9 # Switches 0-phase det. solid-state-relay
|
||||
|
||||
heater_invert = 0 # switches the polarity of the heater control
|
||||
|
||||
### Inputs
|
||||
gpio_door = 18
|
||||
|
||||
|
@ -60,3 +66,13 @@ sim_R_o_nocool = 1.0 # K/W thermal resistance oven -> environment
|
|||
sim_R_o_cool = 0.05 # K/W " with cooling
|
||||
sim_R_ho_noair = 0.1 # K/W thermal resistance heat element -> oven
|
||||
sim_R_ho_air = 0.05 # K/W " with internal air circulation
|
||||
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# Time and Temperature parameters
|
||||
|
||||
temp_scale = "c" # c = Celsius | f = Fahrenheit - Unit to display
|
||||
time_scale_slope = "s" # s = Seconds | m = Minutes | h = Hours - Slope displayed in temp_scale per time_scale_slope
|
||||
time_scale_profile = "s" # s = Seconds | m = Minutes | h = Hours - Enter and view target time in time_scale_profile
|
||||
|
||||
|
|
15
lib/oven.py
15
lib/oven.py
|
@ -146,11 +146,17 @@ class Oven (threading.Thread):
|
|||
if value:
|
||||
self.heat = 1.0
|
||||
if gpio_available:
|
||||
GPIO.output(config.gpio_heat, GPIO.LOW)
|
||||
if config.heater_invert:
|
||||
GPIO.output(config.gpio_heat, GPIO.LOW)
|
||||
else:
|
||||
GPIO.output(config.gpio_heat, GPIO.HIGH)
|
||||
else:
|
||||
self.heat = 0.0
|
||||
if gpio_available:
|
||||
GPIO.output(config.gpio_heat, GPIO.HIGH)
|
||||
if config.heater_invert:
|
||||
GPIO.output(config.gpio_heat, GPIO.HIGH)
|
||||
else:
|
||||
GPIO.output(config.gpio_heat, GPIO.LOW)
|
||||
|
||||
def set_cool(self, value):
|
||||
if value:
|
||||
|
@ -209,13 +215,14 @@ class TempSensorReal(TempSensor):
|
|||
self.thermocouple = MAX6675(config.gpio_sensor_cs,
|
||||
config.gpio_sensor_clock,
|
||||
config.gpio_sensor_data,
|
||||
"c")
|
||||
config.temp_scale)
|
||||
|
||||
if config.max31855:
|
||||
log.info("init MAX31855")
|
||||
self.thermocouple = MAX31855(config.gpio_sensor_cs,
|
||||
config.gpio_sensor_clock,
|
||||
config.gpio_sensor_data,
|
||||
"c")
|
||||
config.temp_scale)
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
|
|
|
@ -44,7 +44,7 @@ def index():
|
|||
@app.route('/picoreflow/:filename#.*#')
|
||||
def send_static(filename):
|
||||
log.debug("serving %s" % filename)
|
||||
return bottle.static_file(filename, root='./public/')
|
||||
return bottle.static_file(filename, root=os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "public"))
|
||||
|
||||
|
||||
def get_websocket_from_request():
|
||||
|
@ -110,6 +110,13 @@ def handle_storage():
|
|||
if message == "GET":
|
||||
log.info("GET command recived")
|
||||
wsock.send(get_profiles())
|
||||
elif msgdict.get("cmd") == "DELETE":
|
||||
log.info("DELETE command received")
|
||||
profile_obj = msgdict.get('profile')
|
||||
if delete_profile(profile_obj):
|
||||
msgdict["resp"] = "OK"
|
||||
wsock.send(json.dumps(msgdict))
|
||||
#wsock.send(get_profiles())
|
||||
elif msgdict.get("cmd") == "PUT":
|
||||
log.info("PUT command received")
|
||||
profile_obj = msgdict.get('profile')
|
||||
|
@ -129,6 +136,19 @@ def handle_storage():
|
|||
log.info("websocket (storage) closed")
|
||||
|
||||
|
||||
@app.route('/config')
|
||||
def handle_config():
|
||||
wsock = get_websocket_from_request()
|
||||
log.info("websocket (config) opened")
|
||||
while True:
|
||||
try:
|
||||
message = wsock.receive()
|
||||
wsock.send(get_config())
|
||||
except WebSocketError:
|
||||
break
|
||||
log.info("websocket (config) closed")
|
||||
|
||||
|
||||
@app.route('/status')
|
||||
def handle_status():
|
||||
wsock = get_websocket_from_request()
|
||||
|
@ -168,6 +188,22 @@ def save_profile(profile, force=False):
|
|||
log.info("Wrote %s" % filepath)
|
||||
return True
|
||||
|
||||
def delete_profile(profile):
|
||||
profile_json = json.dumps(profile)
|
||||
filename = profile['name']+".json"
|
||||
filepath = os.path.join(profile_path, filename)
|
||||
os.remove(filepath)
|
||||
log.info("Deleted %s" % filepath)
|
||||
return True
|
||||
|
||||
|
||||
def get_config():
|
||||
return json.dumps({"temp_scale": config.temp_scale,
|
||||
"time_scale_slope": config.time_scale_slope,
|
||||
"time_scale_profile": config.time_scale_profile,
|
||||
"kwh_rate": config.kwh_rate,
|
||||
"currency_type": config.currency_type})
|
||||
|
||||
|
||||
def main():
|
||||
ip = config.listening_ip
|
||||
|
|
|
@ -135,6 +135,16 @@ body {
|
|||
background: radial-gradient(ellipse at center, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* W3C */
|
||||
}
|
||||
|
||||
.ds-led-door-open {
|
||||
color: rgb(214, 42, 0);
|
||||
background: -moz-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%, rgba(241,218,54,0.26) 100%); /* FF3.6+ */
|
||||
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+ */
|
||||
background: -webkit-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* Chrome10+,Safari5.1+ */
|
||||
background: -o-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* Opera 12+ */
|
||||
background: -ms-radial-gradient(center, ellipse cover, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* IE10+ */
|
||||
background: radial-gradient(ellipse at center, rgba(242,195,67,1) 0%,rgba(241,218,54,0.26) 100%); /* W3C */
|
||||
}
|
||||
|
||||
.ds-led-cool-active {
|
||||
color: rgb(74, 159, 255);
|
||||
background: -moz-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%, rgba(48,144,209,0.26) 100%); /* FF3.6+ */
|
||||
|
@ -188,7 +198,7 @@ body {
|
|||
border: none;
|
||||
width: initial;
|
||||
text-align: center;
|
||||
width: 168px;
|
||||
width: 210px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,15 +3,24 @@ var state_last = "";
|
|||
var graph = [ 'profile', 'live'];
|
||||
var points = [];
|
||||
var profiles = [];
|
||||
var selected_profile = 0;
|
||||
var time_mode = 0;
|
||||
var selected_profile_name = "leadfree";
|
||||
var selected_profile = 0;
|
||||
var selected_profile_name = 'leadfree';
|
||||
var temp_scale = "c";
|
||||
var time_scale_slope = "s";
|
||||
var time_scale_profile = "s";
|
||||
var time_scale_long = "Seconds";
|
||||
var temp_scale_display = "C";
|
||||
var kwh_rate = 0.26;
|
||||
var currency_type = "EUR";
|
||||
|
||||
var host = "ws://" + window.location.hostname + ":" + window.location.port;
|
||||
var ws_status = new WebSocket(host+"/status");
|
||||
var ws_control = new WebSocket(host+"/control");
|
||||
var ws_config = new WebSocket(host+"/config");
|
||||
var ws_storage = new WebSocket(host+"/storage");
|
||||
|
||||
|
||||
if(window.webkitRequestAnimationFrame) window.requestAnimationFrame = window.webkitRequestAnimationFrame;
|
||||
|
||||
graph.profile =
|
||||
|
@ -36,23 +45,39 @@ graph.live =
|
|||
function updateProfile(id)
|
||||
{
|
||||
selected_profile = id;
|
||||
job_time = parseInt(profiles[id].data[profiles[id].data.length-1][0]);
|
||||
var kwh = (3850*job_time/3600/1000).toFixed(2);
|
||||
var cost = (kwh*0.26).toFixed(2);
|
||||
var minutes = Math.floor(job_time/60), seconds = job_time-minutes*60;
|
||||
job_time = minutes+':'+ (seconds < 10 ? "0" : "") + seconds;
|
||||
var job_seconds = parseInt(profiles[id].data[profiles[id].data.length-1][0]);
|
||||
var kwh = (3850*job_seconds/3600/1000).toFixed(2);
|
||||
var cost = (kwh*kwh_rate).toFixed(2);
|
||||
var job_time = new Date(job_seconds * 1000).toISOString().substr(11, 8);
|
||||
$('#sel_prof').html(profiles[id].name);
|
||||
$('#sel_prof_eta').html(job_time);
|
||||
$('#sel_prof_cost').html(kwh + ' kWh (EUR: '+ cost +')');
|
||||
$('#sel_prof_cost').html(kwh + ' kWh ('+ currency_type +': '+ cost +')');
|
||||
graph.profile.data = profiles[id].data;
|
||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
|
||||
}
|
||||
|
||||
function deleteProfile()
|
||||
{
|
||||
var profile = { "type": "profile", "data": "", "name": selected_profile_name };
|
||||
var delete_struct = { "cmd": "DELETE", "profile": profile };
|
||||
|
||||
var delete_cmd = JSON.stringify(delete_struct);
|
||||
console.log("Delete profile:" + selected_profile_name);
|
||||
// FIXME: Add cmd for socket communication to delete stored profile
|
||||
leaveEditMode();
|
||||
|
||||
ws_storage.send(delete_cmd);
|
||||
|
||||
selected_profile_name = profiles[0].name;
|
||||
ws_storage.send('GET');
|
||||
state="IDLE";
|
||||
$('#edit').hide();
|
||||
$('#profile_selector').show();
|
||||
$('#btn_controls').show();
|
||||
$('#status').slideDown();
|
||||
$('#profile_table').slideUp();
|
||||
$('#e2').select2('val', 0);
|
||||
graph.profile.points.show = false;
|
||||
graph.profile.draggable = false;
|
||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
|
||||
}
|
||||
|
||||
|
||||
|
@ -78,20 +103,20 @@ function updateProfileTable()
|
|||
var color = "";
|
||||
|
||||
var html = '<h3>Profile Points</h3><div class="table-responsive" style="scroll: none"><table class="table table-striped">';
|
||||
html += '<tr><th style="width: 50px">#</th><th>Target Time</th><th>Target Temperature in °C</th><th>Slope in °C/s</th><th></th></tr>';
|
||||
html += '<tr><th style="width: 50px">#</th><th>Target Time in ' + time_scale_long+ '</th><th>Target Temperature in °'+temp_scale_display+'</th><th>Slope in °'+temp_scale_display+'/'+time_scale_slope+'</th><th></th></tr>';
|
||||
|
||||
for(var i=0; i<graph.profile.data.length;i++)
|
||||
{
|
||||
if (i>=1) dps = Math.round( (graph.profile.data[i][1]-graph.profile.data[i-1][1])/(graph.profile.data[i][0]-graph.profile.data[i-1][0]) * 10) / 10;
|
||||
|
||||
if (i>=1) dps = ((graph.profile.data[i][1]-graph.profile.data[i-1][1])/(graph.profile.data[i][0]-graph.profile.data[i-1][0]) * 10) / 10;
|
||||
if (dps > 0) { slope = "up"; color="rgba(206, 5, 5, 1)"; } else
|
||||
if (dps < 0) { slope = "down"; color="rgba(23, 108, 204, 1)"; dps *= -1; } else
|
||||
if (dps == 0) { slope = "right"; color="grey"; }
|
||||
|
||||
html += '<tr><td><h4>' + (i+1) + '</h4></td>';
|
||||
html += '<td><input type="text" class="form-control" id="profiletable-0-'+i+'" value="'+ graph.profile.data[i][0] + '" style="width: 60px" /></td>';
|
||||
html += '<td><input type="text" class="form-control" id="profiletable-0-'+i+'" value="'+ timeProfileFormatter(graph.profile.data[i][0],true) + '" style="width: 60px" /></td>';
|
||||
html += '<td><input type="text" class="form-control" id="profiletable-1-'+i+'" value="'+ graph.profile.data[i][1] + '" style="width: 60px" /></td>';
|
||||
html += '<td><div class="input-group"><span class="glyphicon glyphicon-circle-arrow-' + slope +
|
||||
' input-group-addon ds-trend" style="background: '+color+'"></span><input type="text" class="form-control ds-input" readonly value="' + dps + '" style="width: 50px" /></div></td>';
|
||||
html += '<td><div class="input-group"><span class="glyphicon glyphicon-circle-arrow-' + slope + ' input-group-addon ds-trend" style="background: '+color+'"></span><input type="text" class="form-control ds-input" readonly value="' + formatDPS(dps) + '" style="width: 100px" /></div></td>';
|
||||
html += '<td> </td></tr>';
|
||||
}
|
||||
|
||||
|
@ -107,13 +132,53 @@ function updateProfileTable()
|
|||
var fields = id.split("-");
|
||||
var col = parseInt(fields[1]);
|
||||
var row = parseInt(fields[2]);
|
||||
|
||||
graph.profile.data[row][col] = value;
|
||||
|
||||
if (col == 0) {
|
||||
graph.profile.data[row][col] = timeProfileFormatter(value,false);
|
||||
}
|
||||
else {
|
||||
graph.profile.data[row][col] = value;
|
||||
}
|
||||
|
||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
|
||||
updateProfileTable();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function timeProfileFormatter(val, down) {
|
||||
var rval = val
|
||||
switch(time_scale_profile){
|
||||
case "m":
|
||||
if (down) {rval = val / 60;} else {rval = val * 60;}
|
||||
break;
|
||||
case "h":
|
||||
if (down) {rval = val / 3600;} else {rval = val * 3600;}
|
||||
break;
|
||||
}
|
||||
return Math.round(rval);
|
||||
}
|
||||
|
||||
function formatDPS(val) {
|
||||
var tval = val;
|
||||
if (time_scale_slope == "m") {
|
||||
tval = val * 60;
|
||||
}
|
||||
if (time_scale_slope == "h") {
|
||||
tval = (val * 60) * 60;
|
||||
}
|
||||
return Math.round(tval);
|
||||
}
|
||||
|
||||
function hazardTemp(){
|
||||
if (temp_scale == "f") {
|
||||
return (45 * 9 / 5) + 32
|
||||
}
|
||||
else {
|
||||
return 45
|
||||
}
|
||||
}
|
||||
|
||||
function timeTickFormatter(val)
|
||||
{
|
||||
if (val < 1800)
|
||||
|
@ -193,6 +258,7 @@ function enterEditMode()
|
|||
$('#edit').show();
|
||||
$('#profile_selector').hide();
|
||||
$('#btn_controls').hide();
|
||||
console.log(profiles);
|
||||
$('#form_profile_name').attr('value', profiles[selected_profile].name);
|
||||
graph.profile.points.show = true;
|
||||
graph.profile.draggable = true;
|
||||
|
@ -291,10 +357,6 @@ function saveProfile()
|
|||
leaveEditMode();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function getOptions()
|
||||
{
|
||||
|
||||
|
@ -336,9 +398,7 @@ function getOptions()
|
|||
|
||||
yaxis:
|
||||
{
|
||||
tickSize: 25,
|
||||
min: 0,
|
||||
max: 250,
|
||||
tickDecimals: 0,
|
||||
draggable: false,
|
||||
tickColor: 'rgba(216, 211, 197, 0.2)',
|
||||
|
@ -444,11 +504,6 @@ $(document).ready(function()
|
|||
{
|
||||
state = x.state;
|
||||
|
||||
if (x.door == "OPEN")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
if (state!=state_last)
|
||||
{
|
||||
if(state_last == "RUNNING")
|
||||
|
@ -477,14 +532,13 @@ $(document).ready(function()
|
|||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
|
||||
|
||||
left = parseInt(x.totaltime-x.runtime);
|
||||
var minutes = Math.floor(left / 60);
|
||||
var seconds = left - minutes * 60;
|
||||
eta = minutes+':'+ (seconds < 10 ? "0" : "") + seconds;
|
||||
eta = new Date(left * 1000).toISOString().substr(11, 8);
|
||||
|
||||
updateProgress(parseFloat(x.runtime)/parseFloat(x.totaltime)*100);
|
||||
$('#state').html('<span class="glyphicon glyphicon-time" style="font-size: 22px; font-weight: normal"></span><span style="font-family: Digi; font-size: 40px;">' + eta + '</span>');
|
||||
$('#target_temp').html(parseInt(x.target));
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -494,17 +548,55 @@ $(document).ready(function()
|
|||
}
|
||||
|
||||
$('#act_temp').html(parseInt(x.temperature));
|
||||
|
||||
|
||||
if (x.heat > 0.5) { $('#heat').addClass("ds-led-heat-active"); } else { $('#heat').removeClass("ds-led-heat-active"); }
|
||||
if (x.cool > 0.5) { $('#cool').addClass("ds-led-cool-active"); } else { $('#cool').removeClass("ds-led-cool-active"); }
|
||||
if (x.air > 0.5) { $('#air').addClass("ds-led-air-active"); } else { $('#air').removeClass("ds-led-air-active"); }
|
||||
if (x.temperature > 45) { $('#hazard').addClass("ds-led-hazard-active"); } else { $('#hazard').removeClass("ds-led-hazard-active"); }
|
||||
if (x.temperature > hazardTemp()) { $('#hazard').addClass("ds-led-hazard-active"); } else { $('#hazard').removeClass("ds-led-hazard-active"); }
|
||||
if ((x.door == "OPEN") || (x.door == "UNKNOWN")) { $('#door').addClass("ds-led-door-open"); } else { $('#door').removeClass("ds-led-door-open"); }
|
||||
|
||||
state_last = state;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// Config Socket /////////////////////////////////
|
||||
|
||||
ws_config.onopen = function()
|
||||
{
|
||||
ws_config.send('GET');
|
||||
};
|
||||
|
||||
ws_config.onmessage = function(e)
|
||||
{
|
||||
console.log (e.data);
|
||||
x = JSON.parse(e.data);
|
||||
temp_scale = x.temp_scale;
|
||||
time_scale_slope = x.time_scale_slope;
|
||||
time_scale_profile = x.time_scale_profile;
|
||||
kwh_rate = x.kwh_rate;
|
||||
currency_type = x.currency_type;
|
||||
|
||||
if (temp_scale == "c") {temp_scale_display = "C";} else {temp_scale_display = "F";}
|
||||
|
||||
|
||||
$('#act_temp_scale').html('º'+temp_scale_display);
|
||||
$('#target_temp_scale').html('º'+temp_scale_display);
|
||||
|
||||
switch(time_scale_profile){
|
||||
case "s":
|
||||
time_scale_long = "Seconds";
|
||||
break;
|
||||
case "m":
|
||||
time_scale_long = "Minutes";
|
||||
break;
|
||||
case "h":
|
||||
time_scale_long = "Hours";
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Control Socket ////////////////////////////////
|
||||
|
||||
ws_control.onopen = function()
|
||||
|
@ -558,6 +650,16 @@ $(document).ready(function()
|
|||
profiles = message;
|
||||
//delete old options in select
|
||||
$('#e2').find('option').remove().end();
|
||||
// check if current selected value is a valid profile name
|
||||
// if not, update with first available profile name
|
||||
var valid_profile_names = profiles.map(function(a) {return a.name;});
|
||||
if (
|
||||
valid_profile_names.length > 0 &&
|
||||
$.inArray(selected_profile_name, valid_profile_names) === -1
|
||||
) {
|
||||
selected_profile = 0;
|
||||
selected_profile_name = valid_profile_names[0];
|
||||
}
|
||||
|
||||
// fill select with new options from websocket
|
||||
for (var i=0; i<profiles.length; i++)
|
||||
|
@ -579,7 +681,7 @@ $(document).ready(function()
|
|||
$("#e2").select2(
|
||||
{
|
||||
placeholder: "Select Profile",
|
||||
allowClear: false,
|
||||
allowClear: true,
|
||||
minimumResultsForSearch: -1
|
||||
});
|
||||
|
||||
|
|
|
@ -33,10 +33,10 @@
|
|||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="ds-panel">
|
||||
<div class="display ds-num"><span id="act_temp">25</span><span class="ds-unit">°C</span></div>
|
||||
<div class="display ds-num ds-target"><span id="target_temp">---</span><span class="ds-unit">°C</span></div>
|
||||
<div class="display ds-num"><span id="act_temp">25</span><span class="ds-unit" id="act_temp_scale" >°C</span></div>
|
||||
<div class="display ds-num ds-target"><span id="target_temp">---</span><span class="ds-unit" id="target_temp_scale">°C</span></div>
|
||||
<div class="display ds-num ds-text" id="state"></div>
|
||||
<div class="display pull-right ds-state" style="padding-right:0"><span class="ds-led" id="heat">\</span><span class="ds-led" id="cool">l</span><span class="ds-led" id="air">[</span><span class="ds-led" id="hazard">I</span></div>
|
||||
<div class="display pull-right ds-state" style="padding-right:0"><span class="ds-led" id="heat">\</span><span class="ds-led" id="cool">l</span><span class="ds-led" id="air">[</span><span class="ds-led" id="hazard">I</span><span class="ds-led" id="door">♨</span></div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div>
|
||||
|
|
Ładowanie…
Reference in New Issue