kopia lustrzana https://github.com/botheredbybees/kilnController
Kiln waits to reach target temperature before transitioning to the next temperature.
rodzic
e8957f7dec
commit
af483e34f8
124
lib/oven.py
124
lib/oven.py
|
@ -59,6 +59,11 @@ class Oven (threading.Thread):
|
||||||
|
|
||||||
def __init__(self, simulate=False, time_step=config.sensor_time_wait):
|
def __init__(self, simulate=False, time_step=config.sensor_time_wait):
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
|
self.profile = None
|
||||||
|
self.start_time = 0
|
||||||
|
self.runtime = 0
|
||||||
|
self.target = 0
|
||||||
|
self.state = Oven.STATE_IDLE
|
||||||
self.daemon = True
|
self.daemon = True
|
||||||
self.simulate = simulate
|
self.simulate = simulate
|
||||||
self.time_step = time_step
|
self.time_step = time_step
|
||||||
|
@ -78,7 +83,6 @@ class Oven (threading.Thread):
|
||||||
self.profile = None
|
self.profile = None
|
||||||
self.start_time = 0
|
self.start_time = 0
|
||||||
self.runtime = 0
|
self.runtime = 0
|
||||||
self.totaltime = 0
|
|
||||||
self.target = 0
|
self.target = 0
|
||||||
self.door = self.get_door_state()
|
self.door = self.get_door_state()
|
||||||
self.state = Oven.STATE_IDLE
|
self.state = Oven.STATE_IDLE
|
||||||
|
@ -90,7 +94,7 @@ class Oven (threading.Thread):
|
||||||
def run_profile(self, profile):
|
def run_profile(self, profile):
|
||||||
log.info("Running profile %s" % profile.name)
|
log.info("Running profile %s" % profile.name)
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
self.totaltime = profile.get_duration()
|
self.profile.running = True
|
||||||
self.state = Oven.STATE_RUNNING
|
self.state = Oven.STATE_RUNNING
|
||||||
self.start_time = datetime.datetime.now()
|
self.start_time = datetime.datetime.now()
|
||||||
log.info("Starting")
|
log.info("Starting")
|
||||||
|
@ -111,8 +115,8 @@ class Oven (threading.Thread):
|
||||||
else:
|
else:
|
||||||
runtime_delta = datetime.datetime.now() - self.start_time
|
runtime_delta = datetime.datetime.now() - self.start_time
|
||||||
self.runtime = runtime_delta.total_seconds()
|
self.runtime = runtime_delta.total_seconds()
|
||||||
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))
|
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.profile.get_duration()))
|
||||||
self.target = self.profile.get_target_temperature(self.runtime)
|
self.target = self.profile.get_target_temperature(self.runtime, self.temp_sensor.temperature)
|
||||||
pid = self.pid.compute(self.target, self.temp_sensor.temperature)
|
pid = self.pid.compute(self.target, self.temp_sensor.temperature)
|
||||||
|
|
||||||
log.info("pid: %.3f" % pid)
|
log.info("pid: %.3f" % pid)
|
||||||
|
@ -134,27 +138,18 @@ class Oven (threading.Thread):
|
||||||
else:
|
else:
|
||||||
temperature_count = 0
|
temperature_count = 0
|
||||||
|
|
||||||
#Capture the last temperature value. This must be done before set_heat, since there is a sleep in there now.
|
# Capture the last temperature value. This must be done before set_heat, since there is a sleep
|
||||||
last_temp = self.temp_sensor.temperature
|
last_temp = self.temp_sensor.temperature
|
||||||
|
|
||||||
self.set_heat(pid)
|
self.set_heat(pid)
|
||||||
|
|
||||||
#if self.profile.is_rising(self.runtime):
|
|
||||||
# self.set_cool(False)
|
|
||||||
# self.set_heat(self.temp_sensor.temperature < self.target)
|
|
||||||
#else:
|
|
||||||
# self.set_heat(False)
|
|
||||||
# self.set_cool(self.temp_sensor.temperature > self.target)
|
|
||||||
|
|
||||||
if self.temp_sensor.temperature > 200:
|
if self.temp_sensor.temperature > 200:
|
||||||
self.set_air(False)
|
self.set_air(False)
|
||||||
elif self.temp_sensor.temperature < 180:
|
elif self.temp_sensor.temperature < 180:
|
||||||
self.set_air(True)
|
self.set_air(True)
|
||||||
|
|
||||||
if self.runtime >= self.totaltime:
|
if self.profile.finished():
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
|
|
||||||
if pid > 0:
|
if pid > 0:
|
||||||
time.sleep(self.time_step * (1 - pid))
|
time.sleep(self.time_step * (1 - pid))
|
||||||
else:
|
else:
|
||||||
|
@ -209,7 +204,7 @@ class Oven (threading.Thread):
|
||||||
'heat': self.heat,
|
'heat': self.heat,
|
||||||
'cool': self.cool,
|
'cool': self.cool,
|
||||||
'air': self.air,
|
'air': self.air,
|
||||||
'totaltime': self.totaltime,
|
'totaltime': self.profile.get_duration() if self.profile else 0,
|
||||||
'door': self.door
|
'door': self.door
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
@ -310,46 +305,99 @@ class TempSensorSimulate(TempSensor):
|
||||||
time.sleep(self.sleep_time)
|
time.sleep(self.sleep_time)
|
||||||
|
|
||||||
|
|
||||||
class Profile():
|
class Profile:
|
||||||
def __init__(self, json_data):
|
def __init__(self, json_data):
|
||||||
obj = json.loads(json_data)
|
obj = json.loads(json_data)
|
||||||
self.name = obj["name"]
|
self.name = obj["name"]
|
||||||
self.data = sorted(obj["data"])
|
self.data = [ (0, 0) ] + sorted(obj["data"])
|
||||||
|
self.timeDiffs = [ (0, 0) ]
|
||||||
|
for i in range(1, len(self.data)):
|
||||||
|
self.timeDiffs.append( ( self.data[i][0] - self.data[i-1][0], self.data[i][1] ) )
|
||||||
|
|
||||||
|
self.currentState = 1
|
||||||
|
self.numStates = len(self.timeDiffs)
|
||||||
|
|
||||||
|
self.running = False
|
||||||
|
self.lastStateChange = 0
|
||||||
|
self.totalTime = self.data[-1][0]
|
||||||
|
self.overtime = 0
|
||||||
|
log.info(str(self.timeDiffs))
|
||||||
|
log.info(str(self.totalTime))
|
||||||
|
|
||||||
|
def finished(self):
|
||||||
|
return not self.running
|
||||||
|
|
||||||
def get_duration(self):
|
def get_duration(self):
|
||||||
return max([t for (t, x) in self.data])
|
return self.totalTime + self.overtime
|
||||||
|
|
||||||
def get_surrounding_points(self, time):
|
|
||||||
if time > self.get_duration():
|
|
||||||
return (None, None)
|
|
||||||
|
|
||||||
|
def get_surrounding_points(self):
|
||||||
prev_point = None
|
prev_point = None
|
||||||
next_point = None
|
next_point = None
|
||||||
|
|
||||||
for i in range(len(self.data)):
|
if self.currentState < self.numStates:
|
||||||
if time < self.data[i][0]:
|
prev_point = self.timeDiffs[self.currentState - 1]
|
||||||
prev_point = self.data[i-1]
|
next_point = self.timeDiffs[self.currentState]
|
||||||
next_point = self.data[i]
|
|
||||||
break
|
|
||||||
|
|
||||||
return (prev_point, next_point)
|
return prev_point, next_point
|
||||||
|
|
||||||
def is_rising(self, time):
|
def is_rising(self):
|
||||||
(prev_point, next_point) = self.get_surrounding_points(time)
|
(prev_point, next_point) = self.get_surrounding_points()
|
||||||
if prev_point and next_point:
|
if prev_point and next_point:
|
||||||
return prev_point[1] < next_point[1]
|
return prev_point[1] < next_point[1]
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_target_temperature(self, time):
|
def get_target_temperature(self, time, temperature):
|
||||||
if time > self.get_duration():
|
relativeTime = time - self.lastStateChange
|
||||||
return 0
|
minimumTime = self.timeDiffs[self.currentState][0]
|
||||||
|
|
||||||
(prev_point, next_point) = self.get_surrounding_points(time)
|
if relativeTime < minimumTime:
|
||||||
|
targetTemp = self.get_intermediate_temperature(relativeTime)
|
||||||
|
else:
|
||||||
|
if self.check_target(temperature):
|
||||||
|
# phase transition
|
||||||
|
self.currentState += 1
|
||||||
|
self.lastStateChange = time
|
||||||
|
|
||||||
incl = float(next_point[1] - prev_point[1]) / float(next_point[0] - prev_point[0])
|
self.totalTime += self.overtime
|
||||||
temp = prev_point[1] + (time - prev_point[0]) * incl
|
self.overtime = 0
|
||||||
return temp
|
|
||||||
|
targetTemp = self.get_intermediate_temperature(0)
|
||||||
|
|
||||||
|
if self.currentState == self.numStates:
|
||||||
|
self.running = False
|
||||||
|
else:
|
||||||
|
targetTemp = self.timeDiffs[self.currentState][1]
|
||||||
|
self.overtime = relativeTime - minimumTime
|
||||||
|
|
||||||
|
return targetTemp
|
||||||
|
|
||||||
|
def get_intermediate_temperature(self, relativeTime):
|
||||||
|
(prev_point, next_point) = self.get_surrounding_points()
|
||||||
|
|
||||||
|
if next_point[0] == 0:
|
||||||
|
targetTemp = next_point[1]
|
||||||
|
else:
|
||||||
|
incl = float(next_point[1] - prev_point[1]) / float(next_point[0])
|
||||||
|
targetTemp = prev_point[1] + (relativeTime * incl)
|
||||||
|
|
||||||
|
return targetTemp
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tests to see if the target temperature has been acquired.
|
||||||
|
"""
|
||||||
|
def check_target(self, temperature):
|
||||||
|
previous, next = self.get_surrounding_points()
|
||||||
|
result = True
|
||||||
|
|
||||||
|
if previous[1] < next[1]:
|
||||||
|
if temperature < next[1]:
|
||||||
|
result = False
|
||||||
|
elif previous[1] > next[1]:
|
||||||
|
if temperature > next[1]:
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class PID():
|
class PID():
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
var state = "IDLE";
|
var state = "IDLE";
|
||||||
var state_last = "";
|
var state_last = "";
|
||||||
var graph = [ 'profile', 'live'];
|
var graph = [ 'profile', 'live', 'movingProfile'];
|
||||||
var points = [];
|
var points = [];
|
||||||
var profiles = [];
|
var profiles = [];
|
||||||
var time_mode = 0;
|
var time_mode = 0;
|
||||||
|
@ -40,7 +40,14 @@ graph.live =
|
||||||
color: "#d8d3c5",
|
color: "#d8d3c5",
|
||||||
draggable: false
|
draggable: false
|
||||||
};
|
};
|
||||||
|
graph.movingProfile =
|
||||||
|
{
|
||||||
|
label: "Live Profile",
|
||||||
|
data: [],
|
||||||
|
points: { show: false },
|
||||||
|
color: "#ffd300",
|
||||||
|
draggable: false
|
||||||
|
};
|
||||||
|
|
||||||
function updateProfile(id)
|
function updateProfile(id)
|
||||||
{
|
{
|
||||||
|
@ -54,7 +61,7 @@ function updateProfile(id)
|
||||||
$('#sel_prof_eta').html(job_time);
|
$('#sel_prof_eta').html(job_time);
|
||||||
$('#sel_prof_cost').html(kwh + ' kWh ('+ currency_type +': '+ cost +')');
|
$('#sel_prof_cost').html(kwh + ' kWh ('+ currency_type +': '+ cost +')');
|
||||||
graph.profile.data = profiles[id].data;
|
graph.profile.data = profiles[id].data;
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ] , getOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteProfile()
|
function deleteProfile()
|
||||||
|
@ -79,7 +86,7 @@ function deleteProfile()
|
||||||
$('#e2').select2('val', 0);
|
$('#e2').select2('val', 0);
|
||||||
graph.profile.points.show = false;
|
graph.profile.points.show = false;
|
||||||
graph.profile.draggable = false;
|
graph.profile.draggable = false;
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ], getOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -143,7 +150,7 @@ function updateProfileTable()
|
||||||
graph.profile.data[row][col] = value;
|
graph.profile.data[row][col] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ], getOptions());
|
||||||
}
|
}
|
||||||
updateProfileTable();
|
updateProfileTable();
|
||||||
|
|
||||||
|
@ -211,7 +218,8 @@ function runTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
graph.live.data = [];
|
graph.live.data = [];
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
|
graph.movingProfile.data = [];
|
||||||
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ] , getOptions());
|
||||||
|
|
||||||
ws_control.send(JSON.stringify(cmd));
|
ws_control.send(JSON.stringify(cmd));
|
||||||
|
|
||||||
|
@ -226,7 +234,8 @@ function runTaskSimulation()
|
||||||
}
|
}
|
||||||
|
|
||||||
graph.live.data = [];
|
graph.live.data = [];
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
|
graph.movingProfile.data = [];
|
||||||
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ] , getOptions());
|
||||||
|
|
||||||
ws_control.send(JSON.stringify(cmd));
|
ws_control.send(JSON.stringify(cmd));
|
||||||
|
|
||||||
|
@ -251,7 +260,7 @@ function enterNewMode()
|
||||||
graph.profile.points.show = true;
|
graph.profile.points.show = true;
|
||||||
graph.profile.draggable = true;
|
graph.profile.draggable = true;
|
||||||
graph.profile.data = [];
|
graph.profile.data = [];
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ], getOptions());
|
||||||
updateProfileTable();
|
updateProfileTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +275,7 @@ function enterEditMode()
|
||||||
$('#form_profile_name').val(profiles[selected_profile].name);
|
$('#form_profile_name').val(profiles[selected_profile].name);
|
||||||
graph.profile.points.show = true;
|
graph.profile.points.show = true;
|
||||||
graph.profile.draggable = true;
|
graph.profile.draggable = true;
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ], getOptions());
|
||||||
updateProfileTable();
|
updateProfileTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +291,7 @@ function leaveEditMode()
|
||||||
$('#profile_table').slideUp();
|
$('#profile_table').slideUp();
|
||||||
graph.profile.points.show = false;
|
graph.profile.points.show = false;
|
||||||
graph.profile.draggable = false;
|
graph.profile.draggable = false;
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ], getOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
function newPoint()
|
function newPoint()
|
||||||
|
@ -296,14 +305,14 @@ function newPoint()
|
||||||
var pointx = 0;
|
var pointx = 0;
|
||||||
}
|
}
|
||||||
graph.profile.data.push([pointx, Math.floor((Math.random()*230)+25)]);
|
graph.profile.data.push([pointx, Math.floor((Math.random()*230)+25)]);
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ], getOptions());
|
||||||
updateProfileTable();
|
updateProfileTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
function delPoint()
|
function delPoint()
|
||||||
{
|
{
|
||||||
graph.profile.data.splice(-1,1)
|
graph.profile.data.splice(-1,1)
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ], getOptions());
|
||||||
updateProfileTable();
|
updateProfileTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,6 +509,7 @@ $(document).ready(function()
|
||||||
|
|
||||||
$.each(x.log, function(i,v) {
|
$.each(x.log, function(i,v) {
|
||||||
graph.live.data.push([v.runtime, v.temperature]);
|
graph.live.data.push([v.runtime, v.temperature]);
|
||||||
|
graph.movingProfile.data.push([v.runtime, v.target]);
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -533,22 +543,25 @@ $(document).ready(function()
|
||||||
$("#nav_stop").show();
|
$("#nav_stop").show();
|
||||||
|
|
||||||
graph.live.data.push([x.runtime, x.temperature]);
|
graph.live.data.push([x.runtime, x.temperature]);
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
|
graph.movingProfile.data.push([x.runtime, x.target]);
|
||||||
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ] , getOptions());
|
||||||
|
|
||||||
|
timeElapsed = new Date(parseInt(x.runtime) * 1000).toISOString().substr(11, 8);
|
||||||
left = parseInt(x.totaltime-x.runtime);
|
left = parseInt(x.totaltime-x.runtime);
|
||||||
|
if (left < 0) { left = 0; }
|
||||||
eta = new Date(left * 1000).toISOString().substr(11, 8);
|
eta = new Date(left * 1000).toISOString().substr(11, 8);
|
||||||
|
|
||||||
updateProgress(parseFloat(x.runtime)/parseFloat(x.totaltime)*100);
|
updateProgress(parseFloat(x.runtime)/parseFloat(x.totaltime)*100);
|
||||||
$('#state').html('<span class="" style="font-size: 22px; font-weight: normal"></span><span style="font-family: Digi; font-size: 40px;">' + eta + '</span>');
|
$('#state').html('<span class="" style="font-size: 22px; font-weight: normal"></span><span style="font-family: Digi; font-size: 40px;">' + eta + '</span>');
|
||||||
|
$('#timeElapsed').html('<span class="" style="font-size: 22px; font-weight: normal"></span><span style="font-family: Digi; font-size: 40px;">' + timeElapsed + '</span>');
|
||||||
$('#target_temp').html(parseInt(x.target));
|
$('#target_temp').html(parseInt(x.target));
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$("#nav_start").show();
|
$("#nav_start").show();
|
||||||
$("#nav_stop").hide();
|
$("#nav_stop").hide();
|
||||||
$('#state').html('<p class="ds-text">'+state+'</p>');
|
$('#state').html('<p class="ds-text">'+state+'</p>');
|
||||||
|
$('#timeElapsed').html('<p class="ds-text">'+state+'</p>');
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#act_temp').html(parseInt(x.temperature));
|
$('#act_temp').html(parseInt(x.temperature));
|
||||||
|
@ -616,7 +629,7 @@ $(document).ready(function()
|
||||||
console.log (e.data);
|
console.log (e.data);
|
||||||
x = JSON.parse(e.data);
|
x = JSON.parse(e.data);
|
||||||
graph.live.data.push([x.runtime, x.temperature]);
|
graph.live.data.push([x.runtime, x.temperature]);
|
||||||
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
|
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ] , getOptions());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,12 +34,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="clearfix visible-xs-inline"></div> -->
|
<!-- <div class="clearfix visible-xs-inline"></div> -->
|
||||||
<div class="col-xs-8 col-md-5">
|
<div class="col-xs-8 col-md-3">
|
||||||
<div class="ds-title">Time Remaining</div><br>
|
<div class="ds-title">Time Remaining</div><br>
|
||||||
<div class="display ds-num ds-text" id="state">
|
<div class="display ds-num ds-text" id="state">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-4 col-md-3">
|
<div class="col-xs-8 col-md-3">
|
||||||
|
<div class="ds-title">Time Elapsed</div><br>
|
||||||
|
<div class="display ds-num ds-text" id="timeElapsed">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4 col-md-2">
|
||||||
<div class="ds-state ds-title"> </div><br>
|
<div class="ds-state ds-title"> </div><br>
|
||||||
<div class="display pull-right ds-state" style="padding-right:0">
|
<div class="display pull-right ds-state" style="padding-right:0">
|
||||||
<span class="ds-led" id="heat">\</span>
|
<span class="ds-led" id="heat">\</span>
|
||||||
|
|
Ładowanie…
Reference in New Issue