diff --git a/lib/oven.py b/lib/oven.py index c5097ab..776d22a 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -59,6 +59,11 @@ class Oven (threading.Thread): def __init__(self, simulate=False, time_step=config.sensor_time_wait): 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.simulate = simulate self.time_step = time_step @@ -78,7 +83,6 @@ class Oven (threading.Thread): self.profile = None self.start_time = 0 self.runtime = 0 - self.totaltime = 0 self.target = 0 self.door = self.get_door_state() self.state = Oven.STATE_IDLE @@ -90,7 +94,7 @@ class Oven (threading.Thread): def run_profile(self, profile): log.info("Running profile %s" % profile.name) self.profile = profile - self.totaltime = profile.get_duration() + self.profile.running = True self.state = Oven.STATE_RUNNING self.start_time = datetime.datetime.now() log.info("Starting") @@ -111,8 +115,8 @@ class Oven (threading.Thread): else: runtime_delta = datetime.datetime.now() - self.start_time 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)) - self.target = self.profile.get_target_temperature(self.runtime) + 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.temp_sensor.temperature) pid = self.pid.compute(self.target, self.temp_sensor.temperature) log.info("pid: %.3f" % pid) @@ -134,27 +138,18 @@ class Oven (threading.Thread): else: 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 - 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: self.set_air(False) elif self.temp_sensor.temperature < 180: self.set_air(True) - if self.runtime >= self.totaltime: + if self.profile.finished(): self.reset() - if pid > 0: time.sleep(self.time_step * (1 - pid)) else: @@ -209,7 +204,7 @@ class Oven (threading.Thread): 'heat': self.heat, 'cool': self.cool, 'air': self.air, - 'totaltime': self.totaltime, + 'totaltime': self.profile.get_duration() if self.profile else 0, 'door': self.door } return state @@ -310,46 +305,99 @@ class TempSensorSimulate(TempSensor): time.sleep(self.sleep_time) -class Profile(): +class Profile: def __init__(self, json_data): obj = json.loads(json_data) 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): - return max([t for (t, x) in self.data]) - - def get_surrounding_points(self, time): - if time > self.get_duration(): - return (None, None) + return self.totalTime + self.overtime + def get_surrounding_points(self): prev_point = None next_point = None - for i in range(len(self.data)): - if time < self.data[i][0]: - prev_point = self.data[i-1] - next_point = self.data[i] - break + if self.currentState < self.numStates: + prev_point = self.timeDiffs[self.currentState - 1] + next_point = self.timeDiffs[self.currentState] - return (prev_point, next_point) + return prev_point, next_point - def is_rising(self, time): - (prev_point, next_point) = self.get_surrounding_points(time) + def is_rising(self): + (prev_point, next_point) = self.get_surrounding_points() if prev_point and next_point: return prev_point[1] < next_point[1] else: return False - def get_target_temperature(self, time): - if time > self.get_duration(): - return 0 + def get_target_temperature(self, time, temperature): + relativeTime = time - self.lastStateChange + 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]) - temp = prev_point[1] + (time - prev_point[0]) * incl - return temp + self.totalTime += self.overtime + self.overtime = 0 + + 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(): diff --git a/public/assets/js/kilncontroller.js b/public/assets/js/kilncontroller.js index 8584add..bd945c9 100644 --- a/public/assets/js/kilncontroller.js +++ b/public/assets/js/kilncontroller.js @@ -1,6 +1,6 @@ var state = "IDLE"; var state_last = ""; -var graph = [ 'profile', 'live']; +var graph = [ 'profile', 'live', 'movingProfile']; var points = []; var profiles = []; var time_mode = 0; @@ -40,7 +40,14 @@ graph.live = color: "#d8d3c5", draggable: false }; - +graph.movingProfile = +{ + label: "Live Profile", + data: [], + points: { show: false }, + color: "#ffd300", + draggable: false +}; function updateProfile(id) { @@ -54,7 +61,7 @@ function updateProfile(id) $('#sel_prof_eta').html(job_time); $('#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()); + graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ] , getOptions()); } function deleteProfile() @@ -79,7 +86,7 @@ function deleteProfile() $('#e2').select2('val', 0); graph.profile.points.show = 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.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions()); + graph.plot = $.plot("#graph_container", [ graph.profile, graph.live, graph.movingProfile ], getOptions()); } updateProfileTable(); @@ -211,7 +218,8 @@ function runTask() } 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)); @@ -226,7 +234,8 @@ function runTaskSimulation() } 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)); @@ -251,7 +260,7 @@ function enterNewMode() graph.profile.points.show = true; graph.profile.draggable = true; 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(); } @@ -266,7 +275,7 @@ function enterEditMode() $('#form_profile_name').val(profiles[selected_profile].name); graph.profile.points.show = 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(); } @@ -282,7 +291,7 @@ function leaveEditMode() $('#profile_table').slideUp(); graph.profile.points.show = 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() @@ -296,14 +305,14 @@ function newPoint() var pointx = 0; } 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(); } function delPoint() { 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(); } @@ -500,6 +509,7 @@ $(document).ready(function() $.each(x.log, function(i,v) { 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()); }); } @@ -533,22 +543,25 @@ $(document).ready(function() $("#nav_stop").show(); 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); + if (left < 0) { left = 0; } eta = new Date(left * 1000).toISOString().substr(11, 8); updateProgress(parseFloat(x.runtime)/parseFloat(x.totaltime)*100); $('#state').html('' + eta + ''); + $('#timeElapsed').html('' + timeElapsed + ''); $('#target_temp').html(parseInt(x.target)); - - } else { $("#nav_start").show(); $("#nav_stop").hide(); $('#state').html('

'+state+'

'); + $('#timeElapsed').html('

'+state+'

'); } $('#act_temp').html(parseInt(x.temperature)); @@ -616,7 +629,7 @@ $(document).ready(function() console.log (e.data); x = JSON.parse(e.data); 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()); } diff --git a/public/index.html b/public/index.html index 5565c88..143e985 100644 --- a/public/index.html +++ b/public/index.html @@ -34,12 +34,17 @@ -
+
Time Remaining

-
+
+
Time Elapsed

+
+
+
+
 

\