diff --git a/kiln-controller.py b/kiln-controller.py index 4482ba86..828c36f5 100755 --- a/kiln-controller.py +++ b/kiln-controller.py @@ -4,6 +4,7 @@ import sys import logging import json +from datetime import datetime import bottle import gevent @@ -60,7 +61,7 @@ def handle_api(): # start at a specific minute in the schedule # for restarting and skipping over early parts of a schedule - startat = 0; + startat = 0 if 'startat' in bottle.request.json: startat = bottle.request.json['startat'] @@ -72,8 +73,7 @@ def handle_api(): # FIXME juggling of json should happen in the Profile class profile_json = json.dumps(profile) profile = Profile(profile_json) - oven.run_profile(profile,startat=startat) - ovenWatcher.record(profile) + run_profile(profile,startat=startat) if bottle.request.json['cmd'] == 'stop': log.info("api stop command received") @@ -96,6 +96,11 @@ def find_profile(wanted): return profile return None +def run_profile(profile, startat=0): + oven.run_profile(profile, startat) + ovenWatcher.record(profile) + + @app.route('/picoreflow/:filename#.*#') def send_static(filename): log.debug("serving %s" % filename) @@ -126,8 +131,26 @@ def handle_control(): if profile_obj: profile_json = json.dumps(profile_obj) profile = Profile(profile_json) - oven.run_profile(profile) - ovenWatcher.record(profile) + + run_profile(profile) + + elif msgdict.get("cmd") == "SCHEDULED_RUN": + log.info("SCHEDULED_RUN command received") + scheduled_start_time = msgdict.get('scheduledStartTime') + profile_obj = msgdict.get('profile') + if profile_obj: + profile_json = json.dumps(profile_obj) + profile = Profile(profile_json) + + start_datetime = datetime.fromisoformat( + scheduled_start_time, + ) + oven.scheduled_run( + start_datetime, + profile, + lambda: ovenWatcher.record(profile), + ) + elif msgdict.get("cmd") == "SIMULATE": log.info("SIMULATE command received") #profile_obj = msgdict.get('profile') @@ -260,7 +283,7 @@ def get_config(): "time_scale_slope": config.time_scale_slope, "time_scale_profile": config.time_scale_profile, "kwh_rate": config.kwh_rate, - "currency_type": config.currency_type}) + "currency_type": config.currency_type}) def main(): diff --git a/lib/oven.py b/lib/oven.py index af2b42f6..0ca2d9d5 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -6,6 +6,8 @@ import json import config +from threading import Timer + log = logging.getLogger(__name__) @@ -169,10 +171,16 @@ def __init__(self): self.daemon = True self.temperature = 0 self.time_step = config.sensor_time_wait + self.scheduled_run_timer = None + self.start_datetime = None self.reset() def reset(self): self.state = "IDLE" + if self.scheduled_run_timer and self.scheduled_run_timer.is_alive(): + log.info("Cancelling previously scheduled run") + self.scheduled_run_timer.cancel() + self.start_datetime = None self.profile = None self.start_time = 0 self.runtime = 0 @@ -205,6 +213,32 @@ def run_profile(self, profile, startat=0): self.startat = startat * 60 log.info("Starting") + def scheduled_run(self, start_datetime, profile, run_trigger, startat=0): + self.reset() + seconds_until_start = ( + start_datetime - datetime.datetime.now() + ).total_seconds() + if seconds_until_start <= 0: + return + + self.state = "SCHEDULED" + self.start_datetime = start_datetime + self.scheduled_run_timer = Timer( + seconds_until_start, + self._timeout, + args=[profile, run_trigger, startat], + ) + self.scheduled_run_timer.start() + log.info( + "Scheduled to run the kiln at %s", + self.start_datetime, + ) + + def _timeout(self, profile, run_trigger, startat): + self.run_profile(profile, startat) + if run_trigger: + run_trigger() + def abort_run(self): self.reset() @@ -263,6 +297,9 @@ def reset_if_schedule_ended(self): self.reset() def get_state(self): + scheduled_start = None + if self.start_datetime: + scheduled_start = self.start_datetime.strftime("%Y-%m-%d %H:%M") state = { 'runtime': self.runtime, 'temperature': self.board.temp_sensor.temperature + config.thermocouple_offset, @@ -274,6 +311,7 @@ def get_state(self): 'currency_type': config.currency_type, 'profile': self.profile.name if self.profile else None, 'pidstats': self.pid.pidstats, + 'scheduled_start': scheduled_start, } return state @@ -294,7 +332,8 @@ def run(self): class SimulatedOven(Oven): def __init__(self): - self.reset() + # call parent init + Oven.__init__(self) self.board = BoardSimulated() self.t_env = config.sim_t_env @@ -309,9 +348,6 @@ def __init__(self): self.t = self.t_env # deg C temp of oven self.t_h = self.t_env #deg C temp of heating element - # call parent init - Oven.__init__(self) - # start thread self.start() log.info("SimulatedOven started") @@ -380,11 +416,11 @@ class RealOven(Oven): def __init__(self): self.board = Board() self.output = Output() - self.reset() - # call parent init Oven.__init__(self) + self.reset() + # start thread self.start() diff --git a/public/assets/css/picoreflow.css b/public/assets/css/picoreflow.css index 8e77db73..df7fded8 100644 --- a/public/assets/css/picoreflow.css +++ b/public/assets/css/picoreflow.css @@ -32,6 +32,11 @@ body { box-shadow: 0 0 1.1em 0 rgba(0,0,0,0.55); } +#schedule-status { + width: auto; + padding-right: 4px; +} + .display { display: inline-block; text-align: right; @@ -175,6 +180,23 @@ body { background: radial-gradient(ellipse at center, rgba(221,221,221,1) 0%,rgba(221,221,221,0.26) 100%); /* W3C */ } +.ds-led-timer-active { + color: rgb(74, 159, 255); + animation: blinker 1s linear infinite; + background: -moz-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%, rgba(48,144,209,0.26) 100%); /* FF3.6+ */ + 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+ */ + background: -webkit-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* Opera 12+ */ + background: -ms-radial-gradient(center, ellipse cover, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* IE10+ */ + background: radial-gradient(ellipse at center, rgba(124,197,239,1) 0%,rgba(48,144,209,0.26) 100%); /* W3C */ +} + +@keyframes blinker { + 50% { + opacity: 0; + } +} + .ds-trend { top: 0; text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.25), -1px -1px 0 rgba(0, 0, 0, 0.4); @@ -352,6 +374,17 @@ body { top: 10%; } +.schedule-group { + display: flex; + justify-content: flex-end; + margin-top: 10px; +} + +.schedule-group > input { + margin-right: 5px; + text-align: right; +} + .alert { background-image: -webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8)); background-image: -webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%); diff --git a/public/assets/js/picoreflow.js b/public/assets/js/picoreflow.js index 479b7b76..da1f6538 100644 --- a/public/assets/js/picoreflow.js +++ b/public/assets/js/picoreflow.js @@ -222,6 +222,24 @@ function runTask() } +function scheduleTask() +{ + const startTime = document.getElementById('scheduled-run-time').value; + console.log(startTime); + + var cmd = + { + "cmd": "SCHEDULED_RUN", + "profile": profiles[selected_profile], + "scheduledStartTime": startTime, + } + + graph.live.data = []; + graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions()); + + ws_control.send(JSON.stringify(cmd)); +} + function runTaskSimulation() { var cmd = @@ -440,10 +458,37 @@ function getOptions() } +function formatDateInput(date) +{ + var dd = date.getDate(); + var mm = date.getMonth() + 1; //January is 0! + var yyyy = date.getFullYear(); + var hh = date.getHours(); + var mins = date.getMinutes(); + + if (dd < 10) { + dd = '0' + dd; + } + + if (mm < 10) { + mm = '0' + mm; + } + const formattedDate = yyyy + '-' + mm + '-' + dd + 'T' + hh + ':' + mins; + return formattedDate; +} + +function initDatetimePicker() { + const now = new Date(); + const inThirtyMinutes = new Date(); + inThirtyMinutes.setMinutes(inThirtyMinutes.getMinutes() + 10); + $('#scheduled-run-time').attr('value', formatDateInput(inThirtyMinutes)); + $('#scheduled-run-time').attr('min', formatDateInput(now)); +} $(document).ready(function() { + initDatetimePicker(); if(!("WebSocket" in window)) { @@ -538,6 +583,8 @@ $(document).ready(function() { $("#nav_start").hide(); $("#nav_stop").show(); + $("#timer").removeClass("ds-led-timer-active"); + $('#schedule-status').hide() graph.live.data.push([x.runtime, x.temperature]); graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions()); @@ -550,12 +597,22 @@ $(document).ready(function() $('#target_temp').html(parseInt(x.target)); + } + else if (state === "SCHEDULED") { + $("#nav_start").hide(); + $("#nav_stop").show(); + $('#timer').addClass("ds-led-timer-active"); // Start blinking timer symbol + $('#state').html('
'+state+'
'); + $('#schedule-status').html('Start at: ' + x.scheduled_start); + $('#schedule-status').show() } else { $("#nav_start").show(); $("#nav_stop").hide(); + $("#timer").removeClass("ds-led-timer-active"); $('#state').html(''+state+'
'); + $('#schedule-status').hide() } $('#act_temp').html(parseInt(x.temperature)); diff --git a/public/index.html b/public/index.html index 08b2d69e..a619c9df 100644 --- a/public/index.html +++ b/public/index.html @@ -29,6 +29,7 @@