Marks Patches

This commit is contained in:
marktilles 2021-07-05 22:13:41 +02:00
parent 1e2c73f810
commit b0988ade10
18 changed files with 1070 additions and 210 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,3 +1,7 @@
##### CHEMATEX *****
##### CHEMATEX *****
##### CHEMATEX *****
import logging
from lib.max31856 import MAX31856
@ -14,8 +18,10 @@ listening_ip = "0.0.0.0"
listening_port = 8081
### Cost Estimate
kwh_rate = 0.1319 # Rate in currency_type to calculate cost to run job
currency_type = "$" # Currency Symbol to show when calculating cost to run job
# Mark's cost June 2021 Vattenfall all taxes and fees SEK 42,64 => 42,64 /kWt
kwh_rate = 1.0380 # Rate in currency_type to calculate cost to run job
currency_type = "Kr" # Currency Symbol to show when calculating cost to run job
oven_kw = 10000 # kW capacity of oven
########################################################################
#
@ -32,15 +38,15 @@ gpio_heat = 23 # Switches zero-cross solid-state-relay
### Thermocouple Adapter selection:
# max31855 - bitbang SPI interface
# max31856 - bitbang SPI interface. must specify thermocouple_type.
max31855 = 1
max31856 = 0
max31855 = 0
max31856 = 1
# see lib/max31856.py for other thermocouple_type, only applies to max31856
thermocouple_type = MAX31856.MAX31856_S_TYPE
### Thermocouple Connection (using bitbang interfaces)
gpio_sensor_cs = 27
gpio_sensor_clock = 22
gpio_sensor_data = 17
### Thermocouple Connection
gpio_sensor_cs = 5
gpio_sensor_clock = 11
gpio_sensor_data = 9
gpio_sensor_di = 10 # only used with max31856
########################################################################
@ -61,9 +67,9 @@ sensor_time_wait = 2
# well with the simulated oven. You must tune them to work well with
# your specific kiln. Note that the integral pid_ki is
# inverted so that a smaller number means more integral action.
pid_kp = 25 # Proportional
pid_ki = 200 # Integral
pid_kd = 200 # Derivative
pid_kp = 100 # Proportional
pid_ki = 400 # Integral
pid_kd = 800 # Derivative
########################################################################
@ -80,7 +86,7 @@ stop_integral_windup = True
########################################################################
#
# Simulation parameters
simulate = True
simulate = False
sim_t_env = 60.0 # deg C
sim_c_heat = 100.0 # J/K heat capacity of heat element
sim_c_oven = 5000.0 # J/K heat capacity of oven
@ -98,7 +104,7 @@ sim_R_ho_air = 0.05 # K/W " with internal air circulation
# If you change the temp_scale, all settings in this file are assumed to
# be in that scale.
temp_scale = "f" # c = Celsius | f = Fahrenheit - Unit to display
temp_scale = "c" # c = Celsius | f = Fahrenheit - Unit to display
time_scale_slope = "h" # s = Seconds | m = Minutes | h = Hours - Slope displayed in temp_scale per time_scale_slope
time_scale_profile = "m" # s = Seconds | m = Minutes | h = Hours - Enter and view target time in time_scale_profile
@ -107,7 +113,7 @@ time_scale_profile = "m" # s = Seconds | m = Minutes | h = Hours - Enter and vi
# naturally cool off. If your SSR has failed/shorted/closed circuit, this
# means your kiln receives full power until your house burns down.
# this should not replace you watching your kiln or use of a kiln-sitter
emergency_shutoff_temp = 2264 #cone 7
emergency_shutoff_temp = 1270 #cone 7
# If the kiln cannot heat or cool fast enough and is off by more than
# kiln_must_catch_up_max_error the entire schedule is shifted until
@ -115,7 +121,7 @@ emergency_shutoff_temp = 2264 #cone 7
# wanted temperature, the schedule will run forever. This is often used
# for heating as fast as possible in a section of a kiln schedule/profile.
kiln_must_catch_up = True
kiln_must_catch_up_max_error = 10 #degrees
kiln_must_catch_up_max_error = 5 #degrees
# thermocouple offset
# If you put your thermocouple in ice water and it reads 36F, you can
@ -136,4 +142,4 @@ honour_theromocouple_short_errors = False
temperature_average_samples = 40
# Thermocouple AC frequency filtering - set to True if in a 50Hz locale, else leave at False for 60Hz locale
ac_freq_50hz = False
ac_freq_50hz = True

25
docs/logs 2.md Normal file
View File

@ -0,0 +1,25 @@
Logs for a Kiln Run
===================
Logs from the app on the pi go to **/var/log/daemon.log** and look like this...
Jan 21 06:25:40 raspberrypi python[286]: 2019-01-21 06:25:40,390 INFO oven: temp =1092.4, target=1093.2, pid=1.000, heat_on=2.00, heat_off=0.00, run_time=15993, total_time=48780, time_left=32786
| log variable | meaning |
| ------------ | ------- |
|temp | temperature read by thermocouple |
|target | target temperature |
|pid | pid value for that 2s |
|heat_on | number of seconds the elements were on |
|heat_off | number of seconds the elements were off |
|run_time | seconds since start of schedule|
|total_time | total seconds for schedule |
|time_left | seconds left till the end of schedule|
It's trivial to convert the data to csv...
$ grep "INFO oven" daemon.log|sed 's/temp=//'|sed 's/target=//'|sed 's/heat_on=//'|sed 's/heat_off=//'|sed 's/run_time=//'|sed 's/total_time=//'|sed 's/time_left=//'|sed 's/pid=//'|sed 's/.*: //' >out.csv
Here is a slow glaze firing imported into google docs. Make sure to check out the report tab.
https://docs.google.com/spreadsheets/d/1Lcp88P1cNNzYWgKDfnd5UaPVqLuBdAT3lkpfcZMKEBM/edit#gid=2116406322

View File

@ -1,5 +1,15 @@
#!/usr/bin/env python
#### MARK TILLES START BLINKING GREEN LED WHEN SERVICE IS RUNNING
from gpiozero import Button, LEDBoard
from signal import pause
import warnings, os, sys
green_ledGPIO = 6
green_led=LEDBoard(green_ledGPIO)
green_led.blink(on_time=1, off_time=1)
#### END - MARK TILLES START BLINKING GREEN LED WHEN SERVICE IS RUNNING
import os
import sys
import logging
@ -60,13 +70,25 @@ 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']
# get the wanted profile/kiln schedule
profile = find_profile(wanted)
if profile is None:
elif msgdict.get("cmd") == "MARK_SWITCH_TO_CHEMATEX":
log.info("Switching to Chematex kiln")
oven.abort_run()
os.system ("/home/pi/mark_scripts/chematex &")
# TODO: add system call to actually switch
# Added by Henrik for Mark Tilles
elif msgdict.get("cmd") == "MARK_SWITCH_TO_RHODE":
log.info("Switching to Rhode kiln")
oven.abort_run()
os.system ("/home/pi/mark_scripts/rhode &")
# TODO: add system call to actually switch
return { "success" : False, "error" : "profile %s not found" % wanted }
# FIXME juggling of json should happen in the Profile class
@ -260,7 +282,8 @@ 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})
"oven_kw": config.oven_kw,
"currency_type": config.currency_type})
def main():

BIN
lib/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -13,7 +13,7 @@ class MAX31855(object):
'''Initialize Soft (Bitbang) SPI bus
Parameters:
- cs_pin: Chip Select (CS) / Slave Select (SS) pin (Any GPIO)
- cs_pin: Chip Select (CS) / Slave Select (SS) pin (Any GPIO)
- clock_pin: Clock (SCLK / SCK) pin (Any GPIO)
- data_pin: Data input (SO / MOSI) pin (Any GPIO)
- units: (optional) unit of measurement to return. ("c" (default) | "k" | "f")
@ -26,7 +26,6 @@ class MAX31855(object):
self.units = units
self.data = None
self.board = board
self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False
# Initialize needed GPIO
GPIO.setmode(self.board)
@ -71,13 +70,20 @@ class MAX31855(object):
if data_32 is None:
data_32 = self.data
anyErrors = (data_32 & 0x10000) != 0 # Fault bit, D16
noConnection = (data_32 & 0x00000001) != 0 # OC bit, D0
shortToGround = (data_32 & 0x00000002) != 0 # SCG bit, D1
shortToVCC = (data_32 & 0x00000004) != 0 # SCV bit, D2
if anyErrors:
self.noConnection = (data_32 & 0x00000001) != 0 # OC bit, D0
self.shortToGround = (data_32 & 0x00000002) != 0 # SCG bit, D1
self.shortToVCC = (data_32 & 0x00000004) != 0 # SCV bit, D2
self.unknownError = not (self.noConnection | self.shortToGround | self.shortToVCC) # Errk!
else:
self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False
if noConnection:
raise MAX31855Error("No Connection")
elif shortToGround:
raise MAX31855Error("Thermocouple short to ground")
elif shortToVCC:
raise MAX31855Error("Thermocouple short to VCC")
else:
# Perhaps another SPI device is trying to send data?
# Did you remember to initialize all other SPI devices?
raise MAX31855Error("Unknown Error")
def data_to_tc_temperature(self, data_32 = None):
'''Takes an integer and returns a thermocouple temperature in celsius.'''
@ -132,7 +138,7 @@ class MAX31855(object):
GPIO.setup(self.clock_pin, GPIO.IN)
def data_to_LinearizedTempC(self, data_32 = None):
'''Return the NIST-linearized thermocouple temperature value in degrees
'''Return the NIST-linearized thermocouple temperature value in degrees
celsius. See https://learn.adafruit.com/calibrating-sensors/maxim-31855-linearization for more infoo.
This code came from https://github.com/nightmechanic/FuzzypicoReflow/blob/master/lib/max31855.py
'''

View File

@ -89,7 +89,7 @@ class MAX31856(object):
MAX31856_S_TYPE = 0x6 # Read S Type Thermocouple
MAX31856_T_TYPE = 0x7 # Read T Type Thermocouple
def __init__(self, tc_type=MAX31856_S_TYPE, units="c", avgsel=0x0, ac_freq_50hz=False, ocdetect=0x1, software_spi=None, hardware_spi=None, gpio=None):
def __init__(self, tc_type=MAX31856_S_TYPE, units="c", avgsel=0x0, software_spi=None, hardware_spi=None, gpio=None):
"""
Initialize MAX31856 device with software SPI on the specified CLK,
CS, and DO pins. Alternatively can specify hardware SPI by sending an
@ -100,8 +100,6 @@ class MAX31856(object):
MAX31856.MAX31856_X_TYPE.
avgsel (1-byte Hex): Type of Averaging. Choose from values in CR0 table of datasheet.
Default is single sample.
ac_freq_50hz: Set to True if your AC frequency is 50Hz, Set to False for 60Hz,
ocdetect: Detect open circuit errors (ie broken thermocouple). Choose from values in CR1 table of datasheet
software_spi (dict): Contains the pin assignments for software SPI, as defined below:
clk (integer): Pin number for software SPI clk
cs (integer): Pin number for software SPI cs
@ -114,8 +112,6 @@ class MAX31856(object):
self.tc_type = tc_type
self.avgsel = avgsel
self.units = units
self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False
# Handle hardware SPI
if hardware_spi is not None:
self._logger.debug('Using hardware SPI')
@ -136,13 +132,11 @@ class MAX31856(object):
self._spi.set_mode(1)
self._spi.set_bit_order(SPI.MSBFIRST)
self.cr0 = self.MAX31856_CR0_READ_CONT | ((ocdetect & 3) << 4) | (1 if ac_freq_50hz else 0)
self.cr1 = (((self.avgsel & 7) << 4) + (self.tc_type & 0x0f))
self.cr1 = ((self.avgsel << 4) + self.tc_type)
# Setup for reading continuously with T-Type thermocouple
self._write_register(self.MAX31856_REG_WRITE_CR0, 0)
self._write_register(self.MAX31856_REG_WRITE_CR0, self.MAX31856_CR0_READ_CONT)
self._write_register(self.MAX31856_REG_WRITE_CR1, self.cr1)
self._write_register(self.MAX31856_REG_WRITE_CR0, self.cr0)
@staticmethod
def _cj_temp_from_bytes(msb, lsb):
@ -303,39 +297,8 @@ class MAX31856(object):
'''Convert celsius to fahrenheit.'''
return celsius * 9.0/5.0 + 32
def checkErrors(self):
data = self.read_fault_register()
self.noConnection = (data & 0x00000001) != 0
self.unknownError = (data & 0xfe) != 0
def get(self):
self.checkErrors()
celcius = self.read_temp_c()
return getattr(self, "to_" + self.units)(celcius)
if __name__ == "__main__":
# Multi-chip example
import time
cs_pins = [6]
clock_pin = 13
data_pin = 5
di_pin = 26
units = "c"
thermocouples = []
for cs_pin in cs_pins:
thermocouples.append(MAX31856(avgsel=0, ac_freq_50hz=True, tc_type=MAX31856.MAX31856_K_TYPE, software_spi={'clk': clock_pin, 'cs': cs_pin, 'do': data_pin, 'di': di_pin}, units=units))
running = True
while(running):
try:
for thermocouple in thermocouples:
rj = thermocouple.read_internal_temp_c()
tc = thermocouple.get()
print("tc: {} and rj: {}, NC:{} ??:{}".format(tc, rj, thermocouple.noConnection, thermocouple.unknownError))
time.sleep(1)
except KeyboardInterrupt:
running = False
for thermocouple in thermocouples:
thermocouple.cleanup()

View File

@ -8,7 +8,6 @@ import config
log = logging.getLogger(__name__)
class Output(object):
def __init__(self):
self.active = False
@ -27,15 +26,13 @@ class Output(object):
log.warning(msg)
self.active = False
def heat(self,sleepfor, tuning=False):
def heat(self,sleepfor):
self.GPIO.output(config.gpio_heat, self.GPIO.HIGH)
if tuning:
return
time.sleep(sleepfor)
self.GPIO.output(config.gpio_heat, self.GPIO.LOW)
def cool(self,sleepfor):
'''no active cooling, so sleep'''
self.GPIO.output(config.gpio_heat, self.GPIO.LOW)
time.sleep(sleepfor)
# FIX - Board class needs to be completely removed
@ -57,7 +54,7 @@ class Board(object):
self.active = True
log.info("import %s " % (self.name))
except ImportError:
msg = "max31855 config set, but import failed"
msg = "max31855 config set, but import failed"
log.warning(msg)
if config.max31856:
@ -67,7 +64,7 @@ class Board(object):
self.active = True
log.info("import %s " % (self.name))
except ImportError:
msg = "max31856 config set, but import failed"
msg = "max31856 config set, but import failed"
log.warning(msg)
def create_temp_sensor(self):
@ -79,15 +76,13 @@ class Board(object):
class BoardSimulated(object):
def __init__(self):
self.temp_sensor = TempSensorSimulated()
class TempSensor(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
self.temperature = 0
self.bad_percent = 0
self.time_step = config.sensor_time_wait
self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False
class TempSensorSimulated(TempSensor):
'''not much here, just need to be able to set the temperature'''
@ -99,11 +94,6 @@ class TempSensorReal(TempSensor):
during the time_step'''
def __init__(self):
TempSensor.__init__(self)
self.sleeptime = self.time_step / float(config.temperature_average_samples)
self.bad_count = 0
self.ok_count = 0
self.bad_stamp = 0
if config.max31855:
log.info("init MAX31855")
from max31855 import MAX31855, MAX31855Error
@ -118,50 +108,31 @@ class TempSensorReal(TempSensor):
software_spi = { 'cs': config.gpio_sensor_cs,
'clk': config.gpio_sensor_clock,
'do': config.gpio_sensor_data,
'di': config.gpio_sensor_di }
### MARK TILLES ADDED
'di': config.gpio_sensor_di,
#'gpio': config.gpio_heat
}
self.thermocouple = MAX31856(tc_type=config.thermocouple_type,
software_spi = software_spi,
units = config.temp_scale,
ac_freq_50hz = config.ac_freq_50hz,
units = config.temp_scale
)
def run(self):
'''use a moving average of config.temperature_average_samples across the time_step'''
temps = []
'''take 5 measurements over each time period and return the
average'''
while True:
# reset error counter if time is up
if (time.time() - self.bad_stamp) > (self.time_step * 2):
if self.bad_count + self.ok_count:
self.bad_percent = (self.bad_count / (self.bad_count + self.ok_count)) * 100
else:
self.bad_percent = 0
self.bad_count = 0
self.ok_count = 0
self.bad_stamp = time.time()
temp = self.thermocouple.get()
self.noConnection = self.thermocouple.noConnection
self.shortToGround = self.thermocouple.shortToGround
self.shortToVCC = self.thermocouple.shortToVCC
self.unknownError = self.thermocouple.unknownError
is_bad_value = self.noConnection | self.unknownError
if config.honour_theromocouple_short_errors:
is_bad_value |= self.shortToGround | self.shortToVCC
if not is_bad_value:
temps.append(temp)
if len(temps) > config.temperature_average_samples:
del temps[0]
self.ok_count += 1
else:
log.error("Problem reading temp N/C:%s GND:%s VCC:%s ???:%s" % (self.noConnection,self.shortToGround,self.shortToVCC,self.unknownError))
self.bad_count += 1
if len(temps):
self.temperature = sum(temps) / len(temps)
time.sleep(self.sleeptime)
maxtries = 5
sleeptime = self.time_step / float(maxtries)
temps = []
for x in range(0,maxtries):
try:
temp = self.thermocouple.get()
temps.append(temp)
except Exception:
log.exception("problem reading temp")
time.sleep(sleeptime)
self.temperature = sum(temps)/len(temps)
class Oven(threading.Thread):
'''parent oven class. this has all the common code
@ -184,22 +155,8 @@ class Oven(threading.Thread):
self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp)
def run_profile(self, profile, startat=0):
self.reset()
if self.board.temp_sensor.noConnection:
log.info("Refusing to start profile - thermocouple not connected")
return
if self.board.temp_sensor.shortToGround:
log.info("Refusing to start profile - thermocouple short to ground")
return
if self.board.temp_sensor.shortToVCC:
log.info("Refusing to start profile - thermocouple short to VCC")
return
if self.board.temp_sensor.unknownError:
log.info("Refusing to start profile - thermocouple unknown error")
return
log.info("Running schedule %s" % profile.name)
self.reset()
self.profile = profile
self.totaltime = profile.get_duration()
self.state = "RUNNING"
@ -229,9 +186,6 @@ class Oven(threading.Thread):
def update_runtime(self):
runtime_delta = datetime.datetime.now() - self.start_time
if runtime_delta.total_seconds() < 0:
runtime_delta = datetime.timedelta(0)
if self.startat > 0:
self.runtime = self.startat + runtime_delta.total_seconds()
else:
@ -241,24 +195,12 @@ class Oven(threading.Thread):
self.target = self.profile.get_target_temperature(self.runtime)
def reset_if_emergency(self):
'''reset if the temperature is way TOO HOT, or other critical errors detected'''
'''reset if the temperature is way TOO HOT'''
if (self.board.temp_sensor.temperature + config.thermocouple_offset >=
config.emergency_shutoff_temp):
log.info("emergency!!! temperature too high, shutting down")
self.reset()
if self.board.temp_sensor.noConnection:
log.info("emergency!!! lost connection to thermocouple, shutting down")
self.reset()
if self.board.temp_sensor.unknownError:
log.info("emergency!!! unknown thermocouple error, shutting down")
self.reset()
if self.board.temp_sensor.bad_percent > 30:
log.info("emergency!!! too many errors in a short period, shutting down")
self.reset()
def reset_if_schedule_ended(self):
if self.runtime > self.totaltime:
log.info("schedule ended, shutting down")
@ -272,8 +214,6 @@ class Oven(threading.Thread):
'state': self.state,
'heat': self.heat,
'totaltime': self.totaltime,
'kwh_rate': config.kwh_rate,
'currency_type': config.currency_type,
'profile': self.profile.name if self.profile else None,
}
return state
@ -309,10 +249,10 @@ class SimulatedOven(Oven):
# set temps to the temp of the surrounding environment
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")
@ -370,9 +310,9 @@ class SimulatedOven(Oven):
self.runtime,
self.totaltime,
time_left))
# we don't actually spend time heating & cooling during
# a simulation, so sleep.
# a simulation, so sleep.
time.sleep(self.time_step)
@ -389,10 +329,6 @@ class RealOven(Oven):
# start thread
self.start()
def reset(self):
super().reset()
self.output.cool(0)
def heat_then_cool(self):
pid = self.pid.compute(self.target,
self.board.temp_sensor.temperature +
@ -405,10 +341,8 @@ class RealOven(Oven):
if heat_on > 0:
self.heat = 1.0
if heat_on:
self.output.heat(heat_on)
if heat_off:
self.output.cool(heat_off)
self.output.heat(heat_on)
self.output.cool(heat_off)
time_left = self.totaltime - self.runtime
log.info("temp=%.2f, target=%.2f, pid=%.3f, heat_on=%.2f, heat_off=%.2f, run_time=%d, total_time=%d, time_left=%d" %
(self.board.temp_sensor.temperature + config.thermocouple_offset,
@ -466,7 +400,7 @@ class PID():
self.lastErr = 0
# FIX - this was using a really small window where the PID control
# takes effect from -1 to 1. I changed this to various numbers and
# takes effect from -1 to 1. I changed this to various numbers and
# settled on -50 to 50 and then divide by 50 at the end. This results
# in a larger PID control window and much more accurate control...
# instead of what used to be binary on/off control.
@ -479,12 +413,8 @@ class PID():
error = float(setpoint - ispoint)
if self.ki > 0:
if config.stop_integral_windup == True:
if abs(self.kp * error) < window_size:
self.iterm += (error * timeDelta * (1/self.ki))
else:
self.iterm += (error * timeDelta * (1/self.ki))
self.iterm += (error * timeDelta * (1/self.ki))
dErr = (error - self.lastErr) / timeDelta
output = self.kp * error + self.iterm + self.kd * dErr
out4logs = output
@ -496,16 +426,15 @@ class PID():
if output < 0:
output = 0
#if output > 1:
# output = 1
output = float(output / window_size)
if out4logs > 0:
# log.info("pid percents pid=%0.2f p=%0.2f i=%0.2f d=%0.2f" % (out4logs,
# ((self.kp * error)/out4logs)*100,
# (self.iterm/out4logs)*100,
# ((self.kd * dErr)/out4logs)*100))
log.info("pid actuals pid=%0.2f p=%0.2f i=%0.2f d=%0.2f" % (out4logs,
self.kp * error,
self.iterm,
self.kd * dErr))
if out4logs > 0:
log.info("pid percents pid=%0.2f p=%0.2f i=%0.2f d=%0.2f" % (out4logs,
((self.kp * error)/out4logs)*100,
(self.iterm/out4logs)*100,
((self.kd * dErr)/out4logs)*100))
return output

BIN
public/.DS_Store vendored Normal file

Binary file not shown.

BIN
public/assets/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,740 @@
var state = "IDLE";
var state_last = "";
var graph = [ 'profile', 'live'];
var points = [];
var profiles = [];
var time_mode = 0;
var selected_profile = 0;
var selected_profile_name = 'cone-05-long-bisque.json';
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 protocol = 'ws:';
if (window.location.protocol == 'https:') {
protocol = 'wss:';
}
var host = "" + protocol + "//" + 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 =
{
label: "Profile",
data: [],
points: { show: false },
color: "#75890c",
draggable: false
};
graph.live =
{
label: "Live",
data: [],
points: { show: false },
color: "#d8d3c5",
draggable: false
};
function updateProfile(id)
{
selected_profile = id;
selected_profile_name = profiles[id].name;
var job_seconds = profiles[id].data.length === 0 ? 0 : parseInt(profiles[id].data[profiles[id].data.length-1][0]);
var kwh = (oven_kw*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 ('+ 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);
ws_storage.send(delete_cmd);
ws_storage.send('GET');
selected_profile_name = profiles[0].name;
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());
}
function updateProgress(percentage)
{
if(state=="RUNNING")
{
if(percentage > 100) percentage = 100;
$('#progressBar').css('width', percentage+'%');
if(percentage>5) $('#progressBar').html(parseInt(percentage)+'%');
}
else
{
$('#progressBar').css('width', 0+'%');
$('#progressBar').html('');
}
}
function updateProfileTable()
{
var dps = 0;
var slope = "";
var color = "";
var html = '<h3>Schedule Points</h3><div class="table-responsive" style="scroll: none"><table class="table table-striped">';
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 &deg;'+temp_scale_display+'/'+time_scale_slope+'</th><th></th></tr>';
for(var i=0; i<graph.profile.data.length;i++)
{
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="'+ 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="' + formatDPS(dps) + '" style="width: 100px" /></div></td>';
html += '<td>&nbsp;</td></tr>';
}
html += '</table></div>';
$('#profile_table').html(html);
//Link table to graph
$(".form-control").change(function(e)
{
var id = $(this)[0].id; //e.currentTarget.attributes.id
var value = parseInt($(this)[0].value);
var fields = id.split("-");
var col = parseInt(fields[1]);
var row = parseInt(fields[2]);
if (graph.profile.data.length > 0) {
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 (1500 * 9 / 5) + 32
}
else {
return 1500
}
}
function timeTickFormatter(val)
{
if (val < 1800)
{
return val;
}
else
{
var hours = Math.floor(val / (3600));
var div_min = val % (3600);
var minutes = Math.floor(div_min / 60);
if (hours < 10) {hours = "0"+hours;}
if (minutes < 10) {minutes = "0"+minutes;}
return hours+":"+minutes;
}
}
function runTask()
{
var cmd =
{
"cmd": "RUN",
"profile": profiles[selected_profile]
}
graph.live.data = [];
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
ws_control.send(JSON.stringify(cmd));
}
function runTaskSimulation()
{
var cmd =
{
"cmd": "SIMULATE",
"profile": profiles[selected_profile]
}
graph.live.data = [];
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
ws_control.send(JSON.stringify(cmd));
}
function abortTask()
{
var cmd = {"cmd": "STOP"};
ws_control.send(JSON.stringify(cmd));
}
function enterNewMode()
{
state="EDIT"
$('#status').slideUp();
$('#edit').show();
$('#profile_selector').hide();
$('#btn_controls').hide();
$('#form_profile_name').attr('value', '');
$('#form_profile_name').attr('placeholder', 'Please enter a name');
graph.profile.points.show = true;
graph.profile.draggable = true;
graph.profile.data = [];
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
updateProfileTable();
}
function enterEditMode()
{
state="EDIT"
$('#status').slideUp();
$('#edit').show();
$('#profile_selector').hide();
$('#btn_controls').hide();
console.log(profiles);
$('#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());
updateProfileTable();
}
function leaveEditMode()
{
selected_profile_name = $('#form_profile_name').val();
ws_storage.send('GET');
state="IDLE";
$('#edit').hide();
$('#profile_selector').show();
$('#btn_controls').show();
$('#status').slideDown();
$('#profile_table').slideUp();
graph.profile.points.show = false;
graph.profile.draggable = false;
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
}
function newPoint()
{
if(graph.profile.data.length > 0)
{
var pointx = parseInt(graph.profile.data[graph.profile.data.length-1][0])+15;
}
else
{
var pointx = 0;
}
graph.profile.data.push([pointx, Math.floor((Math.random()*230)+25)]);
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
updateProfileTable();
}
function delPoint()
{
graph.profile.data.splice(-1,1)
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ], getOptions());
updateProfileTable();
}
function toggleTable()
{
if($('#profile_table').css('display') == 'none')
{
$('#profile_table').slideDown();
}
else
{
$('#profile_table').slideUp();
}
}
function saveProfile()
{
name = $('#form_profile_name').val();
var rawdata = graph.plot.getData()[0].data
var data = [];
var last = -1;
for(var i=0; i<rawdata.length;i++)
{
if(rawdata[i][0] > last)
{
data.push([rawdata[i][0], rawdata[i][1]]);
}
else
{
$.bootstrapGrowl("<span class=\"glyphicon glyphicon-exclamation-sign\"></span> <b>ERROR 88:</b><br/>An oven is not a time-machine", {
ele: 'body', // which element to append to
type: 'alert', // (null, 'info', 'error', 'success')
offset: {from: 'top', amount: 250}, // 'top', or 'bottom'
align: 'center', // ('left', 'right', or 'center')
width: 385, // (integer, or 'auto')
delay: 5000,
allow_dismiss: true,
stackup_spacing: 10 // spacing between consecutively stacked growls.
});
return false;
}
last = rawdata[i][0];
}
var profile = { "type": "profile", "data": data, "name": name }
var put = { "cmd": "PUT", "profile": profile }
var put_cmd = JSON.stringify(put);
ws_storage.send(put_cmd);
leaveEditMode();
}
function getOptions()
{
var options =
{
series:
{
lines:
{
show: true
},
points:
{
show: true,
radius: 5,
symbol: "circle"
},
shadowSize: 3
},
xaxis:
{
min: 0,
tickColor: 'rgba(216, 211, 197, 0.2)',
tickFormatter: timeTickFormatter,
font:
{
size: 14,
lineHeight: 14, weight: "normal",
family: "Digi",
variant: "small-caps",
color: "rgba(216, 211, 197, 0.85)"
}
},
yaxis:
{
min: 0,
tickDecimals: 0,
draggable: false,
tickColor: 'rgba(216, 211, 197, 0.2)',
font:
{
size: 14,
lineHeight: 14,
weight: "normal",
family: "Digi",
variant: "small-caps",
color: "rgba(216, 211, 197, 0.85)"
}
},
grid:
{
color: 'rgba(216, 211, 197, 0.55)',
borderWidth: 1,
labelMargin: 10,
mouseActiveRadius: 50
},
legend:
{
show: false
}
}
return options;
}
function MARK_SWITCH_TO_CHEMATEX() {
// I want to run the script /home/pi/mark_scripts/chematex then alert:
if (confirm('Switching to CHEMATEX kiln.\nCAUTION! This will cancel any current kiln firing!\nClick OK then wait 10 seconds before refreshing this page.')){
var cmd =
{
"cmd": "MARK_SWITCH_TO_CHEMATEX",
}
ws_control.send(JSON.stringify(cmd));
}
else {
alert ("Oven Switchover Canceled")
}
}
function MARK_SWITCH_TO_RHODE() {
// I want to run the script /home/pi/mark_scripts/rhode then alert:
if (confirm('Switching to RHODE kiln.\nCAUTION! This will cancel any current kiln firing!\nClick OK then wait 10 seconds before refreshing this page.')){
var cmd =
{
"cmd": "MARK_SWITCH_TO_RHODE",
}
ws_control.send(JSON.stringify(cmd));
}
else {
alert ("Oven Switchover Canceled")
}
}
$(document).ready(function()
{
if(!("WebSocket" in window))
{
$('#chatLog, input, button, #examples').fadeOut("fast");
$('<p>Oh no, you need a browser that supports WebSockets. How about <a href="http://www.google.com/chrome">Google Chrome</a>?</p>').appendTo('#container');
}
else
{
// Status Socket ////////////////////////////////
ws_status.onopen = function()
{
console.log("Status Socket has been opened");
$.bootstrapGrowl("<span class=\"glyphicon glyphicon-exclamation-sign\"></span>Getting data from server",
{
ele: 'body', // which element to append to
type: 'success', // (null, 'info', 'error', 'success')
offset: {from: 'top', amount: 250}, // 'top', or 'bottom'
align: 'center', // ('left', 'right', or 'center')
width: 385, // (integer, or 'auto')
delay: 2500,
allow_dismiss: true,
stackup_spacing: 10 // spacing between consecutively stacked growls.
});
};
ws_status.onclose = function()
{
$.bootstrapGrowl("<span class=\"glyphicon glyphicon-exclamation-sign\"></span> <b>ERROR 1:</b><br/>Status Websocket not available", {
ele: 'body', // which element to append to
type: 'error', // (null, 'info', 'error', 'success')
offset: {from: 'top', amount: 250}, // 'top', or 'bottom'
align: 'center', // ('left', 'right', or 'center')
width: 385, // (integer, or 'auto')
delay: 5000,
allow_dismiss: true,
stackup_spacing: 10 // spacing between consecutively stacked growls.
});
};
ws_status.onmessage = function(e)
{
console.log("received status data")
console.log(e.data);
x = JSON.parse(e.data);
if (x.type == "backlog")
{
if (x.profile)
{
selected_profile_name = x.profile.name;
$.each(profiles, function(i,v) {
if(v.name == x.profile.name) {
updateProfile(i);
$('#e2').select2('val', i);
}
});
}
$.each(x.log, function(i,v) {
graph.live.data.push([v.runtime, v.temperature]);
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
});
}
if(state!="EDIT")
{
state = x.state;
if (state!=state_last)
{
if(state_last == "RUNNING")
{
$('#target_temp').html('---');
updateProgress(0);
$.bootstrapGrowl("<span class=\"glyphicon glyphicon-exclamation-sign\"></span> <b>Run completed</b>", {
ele: 'body', // which element to append to
type: 'success', // (null, 'info', 'error', 'success')
offset: {from: 'top', amount: 250}, // 'top', or 'bottom'
align: 'center', // ('left', 'right', or 'center')
width: 385, // (integer, or 'auto')
delay: 0,
allow_dismiss: true,
stackup_spacing: 10 // spacing between consecutively stacked growls.
});
}
}
if(state=="RUNNING")
{
$("#nav_start").hide();
$("#nav_stop").show();
graph.live.data.push([x.runtime, x.temperature]);
graph.plot = $.plot("#graph_container", [ graph.profile, graph.live ] , getOptions());
left = parseInt(x.totaltime-x.runtime);
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
{
$("#nav_start").show();
$("#nav_stop").hide();
$('#state').html('<p class="ds-text">'+state+'</p>');
}
$('#act_temp').html(parseInt(x.temperature));
if (x.heat > 0.0) {
setTimeout(function() { $('#heat').addClass("ds-led-heat-active") }, 0 )
setTimeout(function() { $('#heat').removeClass("ds-led-heat-active") }, (x.heat*1000.0)-5)
}
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 > 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;
oven_kw = x.oven_kw;
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()
{
};
ws_control.onmessage = function(e)
{
//Data from Simulation
console.log ("control socket has been opened")
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());
}
// Storage Socket ///////////////////////////////
ws_storage.onopen = function()
{
ws_storage.send('GET');
};
ws_storage.onmessage = function(e)
{
message = JSON.parse(e.data);
if(message.resp)
{
if(message.resp == "FAIL")
{
if (confirm('Overwrite?'))
{
message.force=true;
console.log("Sending: " + JSON.stringify(message));
ws_storage.send(JSON.stringify(message));
}
else
{
//do nothing
}
}
return;
}
//the message is an array of profiles
//FIXME: this should be better, maybe a {"profiles": ...} container?
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++)
{
var profile = profiles[i];
//console.log(profile.name);
$('#e2').append('<option value="'+i+'">'+profile.name+'</option>');
if (profile.name == selected_profile_name)
{
selected_profile = i;
$('#e2').select2('val', i);
updateProfile(i);
}
}
};
$("#e2").select2(
{
placeholder: "Select Profile",
allowClear: true,
minimumResultsForSearch: -1
});
$("#e2").on("change", function(e)
{
updateProfile(e.val);
});
}
});

View File

@ -1,3 +1,4 @@
oven_kw = x.oven_kw;
var state = "IDLE";
var state_last = "";
var graph = [ 'profile', 'live'];
@ -51,7 +52,7 @@ function updateProfile(id)
selected_profile = id;
selected_profile_name = profiles[id].name;
var job_seconds = profiles[id].data.length === 0 ? 0 : parseInt(profiles[id].data[profiles[id].data.length-1][0]);
var kwh = (3850*job_seconds/3600/1000).toFixed(2);
var kwh = (oven_kw*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);
@ -440,6 +441,35 @@ function getOptions()
}
function MARK_SWITCH_TO_CHEMATEX() {
// I want to run the script /home/pi/mark_scripts/chematex then alert:
if (confirm('Switching to CHEMATEX kiln.\nCAUTION! This will cancel any current kiln firing!\nClick OK then wait 10 seconds before refreshing this page.')){
var cmd =
{
"cmd": "MARK_SWITCH_TO_CHEMATEX",
}
ws_control.send(JSON.stringify(cmd));
}
else {
alert ("Oven Switchover Canceled")
}
}
function MARK_SWITCH_TO_RHODE() {
// I want to run the script /home/pi/mark_scripts/rhode then alert:
if (confirm('Switching to RHODE kiln.\nCAUTION! This will cancel any current kiln firing!\nClick OK then wait 10 seconds before refreshing this page.')){
var cmd =
{
"cmd": "MARK_SWITCH_TO_RHODE",
}
ws_control.send(JSON.stringify(cmd));
}
else {
alert ("Oven Switchover Canceled")
}
}
$(document).ready(function()

161
public/control.html Normal file
View File

@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Chematex Kiln Controller</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="assets/js/jquery-1.10.2.min.js"></script>
<script src="assets/js/jquery.event.drag-2.2.js"></script>
<script src="assets/js/jquery.flot.js"></script>
<script src="assets/js/jquery.flot.resize.js"></script>
<script src="assets/js/jquery.flot.draggable.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/jquery.bootstrap-growl.min.js"></script>
<script src="assets/js/select2.min.js"></script>
<script src="assets/js/picoreflow.js"></script>
<link rel="stylesheet" href="assets/css/bootstrap.min.css"/>
<link rel="stylesheet" href="assets/css/bootstrap-theme.min.css"/>
<link rel="stylesheet" href="assets/css/bootstrap-modal.css"/>
<link rel="stylesheet" href="assets/css/select2.css"/>
<link rel="stylesheet" href="assets/css/picoreflow.css"/>
</head>
<body>
<br><center><font size = "+3" color = "green">CHEMATEX Ugnen - Drejstuga Fun!</font></center>
<center><font color = "blue" size = "+2">Ensure switches and power cord are in proper positions</font></center>
<center><a href="http://192.168.0.12:8081/picoreflow/control.html" onClick="MARK_SWITCH_TO_RHODE();">Byt till Rhode</a></center>
<div class="container">
<div id="status">
<div class="ds-title-panel">
<div class="ds-title">Sensor Temp</div>
<div class="ds-title">Target Temp</div>
<div class="ds-title ds-state pull-right" style="border-left: 1px solid #ccc;">&nbsp;&nbsp;&nbsp;Heat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cool&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Air&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Hazard&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Door&nbsp;&nbsp;&nbsp;</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>
<div class="clearfix"></div>
<div>
<div class="progress progress-striped active">
<div id="progressBar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
<span class="sr-only"></span>
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<div id="profile_selector" class="pull-left">
<select id="e2" class="select2" style="margin-top: 4px"></select>
<button id="btn_edit" type="button" class="btn btn-default" onclick="enterEditMode()"><span class="glyphicon glyphicon-edit"></span></button>
<button id="btn_new" type="button" class="btn btn-default" onclick="enterNewMode(selected_profile)"><span class="glyphicon glyphicon-plus"></span></button>
</div>
<div id="btn_controls" class="pull-right" style="margin-top: 3px">
<div id="nav_start" class="btn-group" style="display:none">
<button type="button" class="btn btn-default" style="visibility: hidden;" onclick="runTaskSimulation();">Simulate</button>
<button type="button" class="btn btn-success" data-toggle="modal" data-target="#jobSummaryModal"><span class="glyphicon glyphicon-play"></span> Start</button>
</div>
<button id="nav_stop" type="button" class="btn btn-danger" onclick="abortTask()" style="display:none" ><span class="glyphicon glyphicon-stop"></span> Stop</button>
</div>
<div id="edit" style="display:none;">
<div class="input-group">
<span class="input-group-addon">Schedule Name</span>
<input id="form_profile_name" type="text" class="form-control" />
<span class="input-group-btn">
<button class="btn btn-success" type="button" onclick="saveProfile();">Save</button>
<button id="btn_exit" type="button" class="btn btn-default" onclick="leaveEditMode()"><span class="glyphicon glyphicon-remove"></span></button>
</span>
</div>
<div class="btn-group btn-group-sm" style="margin-top: 10px">
<button id="btn_newPoint" type="button" class="btn btn-default" onclick="newPoint()"><span class="glyphicon glyphicon-plus"></span></button>
<button id="btn_delPoint" type="button" class="btn btn-default" onclick="delPoint()"><span class="glyphicon glyphicon-minus"></span></button>
</div>
<div class="btn-group btn-group-sm" style="margin-top: 10px">
<button id="btn_table" type="button" class="btn btn-default" onclick="toggleTable()"><span class="glyphicon glyphicon-list"></span></button>
<button id="btn_live" type="button" class="btn btn-default" onclick="toggleLive()"><span class="glyphicon glyphicon-eye-open"></span></button>
</div>
<div class="btn-group btn-group-sm" style="margin-top: 10px">
<button id="btn_delProfile" type="button" class="btn btn-danger" data-toggle="modal" data-target="#delProfileModal"><span class="glyphicon glyphicon-trash"></span></button>
</div>
</div>
</div>
<div class="panel-body">
<div id="graph_container" class="graph"></div>
</div>
<div id="profile_table" class="panel-footer" style="display:none;"></div>
</div>
</div>
<div id="jobSummaryModal" class="modal fade" tabindex="-1" aria-hidden="true" style="display: none;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 class="modal-title" id="jobSummaryModalLabel">Task Overview</h3>
</div>
<div class="modal-body">
<table class="table table-bordered">
<tr><td>Selected Profile</td><td><b><span id="sel_prof"></span></b></td></tr>
<tr><td>Estimated Runtime</td><td><b><span id="sel_prof_eta"></span></b></td></tr>
<tr><td>Estimated Power consumption</td><td><b><span id="sel_prof_cost"></span></b></td></tr>
</table>
</div>
<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>
</div>
</div>
</div>
</div>
</div>
<div id="delProfileModal" class="modal fade" tabindex="-1" aria-hidden="true" style="display: none;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 class="modal-title" id="delProfileModalLabel">Delete this profile?</h3>
</div>
<div class="modal-body">
Do your really want to delete this profile?
</div>
<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="deleteProfile()">Yes, delete the profile</button>
</div>
</div>
</div>
</div>
</div>
<div id="overwriteProfileModal" class="modal fade" tabindex="-1" aria-hidden="true" style="display: none;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 class="modal-title" id="overwriteProfileModalLabel">Overwrite this profile?</h3>
</div>
<div class="modal-body">
Do your really want to overwrite this profile?
</div>
<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="deleteProfile()">Yes, delete the profile</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>Kiln Controller</title>
<title>Drejstuga Fun!</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="assets/js/jquery-1.10.2.min.js"></script>
@ -23,13 +23,14 @@
</head>
<body>
<br><center><font size = "+3" color = "green">Drejstuga Fun!</font></center>
<div class="container">
<div id="status">
<div class="ds-title-panel">
<div class="ds-title">Sensor Temp</div>
<div class="ds-title">Target Temp</div>
<div class="ds-title ds-state pull-right" style="border-left: 1px solid #ccc;">Status</div>
<div class="ds-title ds-state pull-right" style="border-left: 1px solid #ccc;">&nbsp;&nbsp;&nbsp;Heat&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cool&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Air&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Hazard&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Door&nbsp;&nbsp;&nbsp;</div>
</div>
<div class="clearfix"></div>
<div class="ds-panel">
@ -50,36 +51,23 @@
<div class="panel panel-default">
<div class="panel-heading">
<div id="profile_selector" class="pull-left">
<select id="e2" class="select2" style="margin-top: 4px"></select>
<button id="btn_edit" type="button" class="btn btn-default" onclick="enterEditMode()"><span class="glyphicon glyphicon-edit"></span></button>
<button id="btn_new" type="button" class="btn btn-default" onclick="enterNewMode(selected_profile)"><span class="glyphicon glyphicon-plus"></span></button>
</div>
<div id="btn_controls" class="pull-right" style="margin-top: 3px">
<div id="nav_start" class="btn-group" style="display:none">
<button type="button" class="btn btn-default" style="visibility: hidden;" onclick="runTaskSimulation();">Simulate</button>
<button type="button" class="btn btn-success" data-toggle="modal" data-target="#jobSummaryModal"><span class="glyphicon glyphicon-play"></span> Start</button>
</div>
<button id="nav_stop" type="button" class="btn btn-danger" onclick="abortTask()" style="display:none" ><span class="glyphicon glyphicon-stop"></span> Stop</button>
</div>
<div id="edit" style="display:none;">
<div class="input-group">
<span class="input-group-addon">Schedule Name</span>
<input id="form_profile_name" type="text" class="form-control" />
<span class="input-group-btn">
<button class="btn btn-success" type="button" onclick="saveProfile();">Save</button>
<button id="btn_exit" type="button" class="btn btn-default" onclick="leaveEditMode()"><span class="glyphicon glyphicon-remove"></span></button>
</span>
</div>
<div class="btn-group btn-group-sm" style="margin-top: 10px">
<button id="btn_newPoint" type="button" class="btn btn-default" onclick="newPoint()"><span class="glyphicon glyphicon-plus"></span></button>
<button id="btn_delPoint" type="button" class="btn btn-default" onclick="delPoint()"><span class="glyphicon glyphicon-minus"></span></button>
</div>
<div class="btn-group btn-group-sm" style="margin-top: 10px">
<button id="btn_table" type="button" class="btn btn-default" onclick="toggleTable()"><span class="glyphicon glyphicon-list"></span></button>
<button id="btn_live" type="button" class="btn btn-default" onclick="toggleLive()"><span class="glyphicon glyphicon-eye-open"></span></button>
</div>
<div class="btn-group btn-group-sm" style="margin-top: 10px">
<button id="btn_delProfile" type="button" class="btn btn-danger" data-toggle="modal" data-target="#delProfileModal"><span class="glyphicon glyphicon-trash"></span></button>
</div>
</div>
</div>
@ -94,7 +82,6 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 class="modal-title" id="jobSummaryModalLabel">Task Overview</h3>
</div>
<div class="modal-body">
@ -106,8 +93,6 @@
</div>
<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>
</div>
</div>
</div>
@ -118,7 +103,6 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 class="modal-title" id="delProfileModalLabel">Delete this profile?</h3>
</div>
<div class="modal-body">
@ -126,8 +110,6 @@
</div>
<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="deleteProfile()">Yes, delete the profile</button>
</div>
</div>
</div>
@ -138,7 +120,6 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 class="modal-title" id="overwriteProfileModalLabel">Overwrite this profile?</h3>
</div>
<div class="modal-body">
@ -146,8 +127,6 @@
</div>
<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="deleteProfile()">Yes, delete the profile</button>
</div>
</div>
</div>

View File

@ -0,0 +1 @@
{"type": "profile", "data": [[0, 20], [1800, 90], [12000, 650], [19200, 1260], [20400, 1260], [21600, 593], [63000, 25]], "name": "Unni Stengodsglasyr"}

View File

@ -1 +0,0 @@
{"data": [[0, 65], [600, 200], [7500, 250], [14340, 600], [24840, 1300], [45840, 1650], [46800, 1708], [52800, 1888], [54600, 1888]], "type": "profile", "name": "cone-05-long-bisque"}

View File

@ -1 +0,0 @@
{"data": [[0, 65], [600, 200], [7200, 250], [25200, 1976], [32880, 2232], [33480, 2232], [36780, 1832], [48780, 1400]], "type": "profile", "name": "cone-6-long-glaze"}

View File

@ -1 +0,0 @@
{"data": [[0, 200], [1740, 200], [1800, 250], [3600, 250]], "type": "profile", "name": "test-200-250"}