Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to schedule when the kiln starts #59

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions kiln-controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys
import logging
import json
from datetime import datetime

import bottle
import gevent
Expand Down Expand Up @@ -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']

Expand All @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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():
Expand Down
48 changes: 42 additions & 6 deletions lib/oven.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import json
import config

from threading import Timer

log = logging.getLogger(__name__)


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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()

Expand Down
33 changes: 33 additions & 0 deletions public/assets/css/picoreflow.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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%);
Expand Down
57 changes: 57 additions & 0 deletions public/assets/js/picoreflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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());
Expand All @@ -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('<p class="ds-text">'+state+'</p>');
$('#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('<p class="ds-text">'+state+'</p>');
$('#schedule-status').hide()
}

$('#act_temp').html(parseInt(x.temperature));
Expand Down
9 changes: 7 additions & 2 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@
<div class="ds-title-panel">
<div class="ds-title">Sensor Temp</div>
<div class="ds-title">Target Temp</div>
<div id="schedule-status" class="ds-title"></div>
<div class="ds-title ds-state pull-right" style="border-left: 1px solid #ccc;">Status</div>
</div>
<div class="clearfix"></div>
<div class="ds-panel">
<div class="display ds-num"><span id="act_temp">25</span><span class="ds-unit" id="act_temp_scale" >&deg;C</span></div>
<div class="display ds-num ds-target"><span id="target_temp">---</span><span class="ds-unit" id="target_temp_scale">&deg;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">&#92;</span><span class="ds-led" id="cool">&#108;</span><span class="ds-led" id="air">&#91;</span><span class="ds-led" id="hazard">&#73;</span><span class="ds-led" id="door">&#9832;</span></div>
<div class="display pull-right ds-state" style="padding-right:0"><span class="ds-led" id="heat">&#92;</span><span class="ds-led" id="cool">&#108;</span><span class="ds-led" id="air">&#91;</span><span class="ds-led" id="hazard">&#73;</span><span class="ds-led" id="timer">&#x29D6;</span></div>
</div>
<div class="clearfix"></div>
<div>
Expand Down Expand Up @@ -107,7 +108,11 @@ <h3 class="modal-title" id="jobSummaryModalLabel">Task Overview</h3>
<div class="modal-footer">
<div class="btn-group" style="width: 100%">
<button type="button" class="btn btn-danger" style="width: 50%" data-dismiss="modal">No, take me back</button>
<button type="button" class="btn btn-success" style="width: 50%" data-dismiss="modal" onclick="runTask()">Yes, start the Run</button>
<button type="button" class="btn btn-success" style="width: 50%" data-dismiss="modal" onclick="runTask()">Yes, start the Run now</button>
</div>
<div class="schedule-group">
<input type="datetime-local" id="scheduled-run-time">
<button type="button" class="btn btn-primary" style="width: 50%" data-dismiss="modal" onclick="scheduleTask()">Schedule start for later</button>
</div>
</div>
</div>
Expand Down