From b0988ade107916317ed952ac000548655bfb6796 Mon Sep 17 00:00:00 2001 From: marktilles <82381104+marktilles@users.noreply.github.com> Date: Mon, 5 Jul 2021 22:13:41 +0200 Subject: [PATCH] Marks Patches --- .DS_Store | Bin 0 -> 6148 bytes config.py | 38 +- docs/logs 2.md | 25 + kiln-controller.py | 27 +- lib/.DS_Store | Bin 0 -> 6148 bytes lib/max31855.py | 24 +- lib/max31856.py | 45 +- lib/oven.py | 157 ++--- public/.DS_Store | Bin 0 -> 6148 bytes public/assets/.DS_Store | Bin 0 -> 6148 bytes public/assets/js/picoreflow 2.js | 740 ++++++++++++++++++++++ public/assets/js/picoreflow.js | 32 +- public/control.html | 161 +++++ public/index.html | 27 +- storage/profiles/Unni Stengodsglasyr.json | 1 + storage/profiles/cone-05-long-bisque.json | 1 - storage/profiles/cone-6-long-glaze.json | 1 - storage/profiles/test-200-250.json | 1 - 18 files changed, 1070 insertions(+), 210 deletions(-) create mode 100644 .DS_Store create mode 100644 docs/logs 2.md create mode 100644 lib/.DS_Store create mode 100644 public/.DS_Store create mode 100644 public/assets/.DS_Store create mode 100644 public/assets/js/picoreflow 2.js create mode 100644 public/control.html create mode 100644 storage/profiles/Unni Stengodsglasyr.json delete mode 100644 storage/profiles/cone-05-long-bisque.json delete mode 100644 storage/profiles/cone-6-long-glaze.json delete mode 100644 storage/profiles/test-200-250.json diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a9211d2295a9c7dd5bf246bdd3793fb5f1c56103 GIT binary patch literal 6148 zcmeHK%}&EG40g5woe*LN1RQhWw!`=pJ0Mj94&0E6-~b33IzgqDmI@8jgyW^$$2V>g67CND$xpi z&#E&P&5qy506n`hBoKpA{rAr=iL<=XcoC&?Wnyy5tKx6QI}NUMKgh$pm$ky~G4=Mt z{7O0dKwso3^aoX;hjFtKP!W^48w@_3ho@e)#Zc85Q{Q8JTrBf+sZG=5B&+n&|Wr zJrq3&9q55F8^4IMi$vjHr?e@^AO{Uy2;maiqoL6X6vDy`FaylMpJ%`wYTopp-wAJk z8DIv!n*ll>Br2iru&`*34s1*bfJmQ_M$o2Of^wuo-(g`9M^J=KMYO5Hl^DXNqhGo> z-(g|Vrh{K-%h`z(ZB6?8xL%`6$4Kwhg G47>uVZ(Dr; literal 0 HcmV?d00001 diff --git a/config.py b/config.py index f2c51c1..e01f508 100644 --- a/config.py +++ b/config.py @@ -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 diff --git a/docs/logs 2.md b/docs/logs 2.md new file mode 100644 index 0000000..eda32ab --- /dev/null +++ b/docs/logs 2.md @@ -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 diff --git a/kiln-controller.py b/kiln-controller.py index 4482ba8..51659a0 100755 --- a/kiln-controller.py +++ b/kiln-controller.py @@ -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(): diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 (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 diff --git a/public/.DS_Store b/public/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..eba7a31ae9f14d044b5485778d28c257b6f7c216 GIT binary patch literal 6148 zcmeHK%}T>S5Z-NTO%bsNQIC7^)Wnc96tPr;<`cnj)CDOS4kE;i8om#iP zk!8(>*g4pnHbmAw+;528c6&OlS)1Ffqto8q!(;X|eSXora`*(5tQnlaD;R|>-n-*y zD5G2O&vIt5gv0-&E(iF(8UG4Q7t;FXTsabQcPwk~WAYpnq7fudksu5q3MhAPDn fi=}uAR0;S68i2OJTqAfu=tn@&Kn*eQqYQiiE_zZp literal 0 HcmV?d00001 diff --git a/public/assets/.DS_Store b/public/assets/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..89e61e29b7273379212ec8114df1ab437b03fde0 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8rihq>sK>o{>!I}zdk{jc2X8_|4=Qa!ijBmiG^s&jByXW_cV4O178aL`I{a3QYkL@_cH$(HxZ{j3(K>aKp{wC zAO?s5Vqgv#(1(GsJcqSY$;1FL@G}FrKL}`umc~M%+&ZAc>ofXmh$x`rTLMvNv@{k9 z!2`loDxgZ``ia3+I*bb)XK5@Hs&vNX$}o>!xqQ5ExjKvs8P2$+ka}W(7?@_Ds!S8l z|6};e%zWfer;tSq5Ci{=0bc9c-3}C`&(?3{;aMx7^`W6)T!9J*=u?*fFmNB)luqRr bs6(8ku~3M!;J8c&q>F$gggRp27Z~^eCCW~W literal 0 HcmV?d00001 diff --git a/public/assets/js/picoreflow 2.js b/public/assets/js/picoreflow 2.js new file mode 100644 index 0000000..6ea7abe --- /dev/null +++ b/public/assets/js/picoreflow 2.js @@ -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 = '

Schedule Points

'; + html += ''; + + for(var i=0; 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 += ''; + html += ''; + html += ''; + html += ''; + html += ''; + } + + html += '
#Target Time in ' + time_scale_long+ 'Target Temperature in °'+temp_scale_display+'Slope in °'+temp_scale_display+'/'+time_scale_slope+'

' + (i+1) + '

 
'; + + $('#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 last) + { + data.push([rawdata[i][0], rawdata[i][1]]); + } + else + { + $.bootstrapGrowl(" ERROR 88:
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"); + $('

Oh no, you need a browser that supports WebSockets. How about Google Chrome?

').appendTo('#container'); + } + else + { + + // Status Socket //////////////////////////////// + + ws_status.onopen = function() + { + console.log("Status Socket has been opened"); + + $.bootstrapGrowl("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(" ERROR 1:
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(" Run completed", { + 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('' + eta + ''); + $('#target_temp').html(parseInt(x.target)); + + + } + else + { + $("#nav_start").show(); + $("#nav_stop").hide(); + $('#state').html('

'+state+'

'); + } + + $('#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'+profile.name+''); + + 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); + }); + + } +}); diff --git a/public/assets/js/picoreflow.js b/public/assets/js/picoreflow.js index 479b7b7..27873ee 100644 --- a/public/assets/js/picoreflow.js +++ b/public/assets/js/picoreflow.js @@ -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() diff --git a/public/control.html b/public/control.html new file mode 100644 index 0000000..0e020d0 --- /dev/null +++ b/public/control.html @@ -0,0 +1,161 @@ + + + + + Chematex Kiln Controller + + + + + + + + + + + + + + + + + + + + +
CHEMATEX Ugnen - Drejstuga Fun!
+
Ensure switches and power cord are in proper positions
+
Byt till Rhode
+ +
+
+
+
Sensor Temp
+
Target Temp
+
   Heat        Cool        Air        Hazard     Door   
+
+
+
+
25°C
+
---°C
+
+
\l[I
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+ + +
+ +
+
+
+
+ +
+
+ + + + + + + + + diff --git a/public/index.html b/public/index.html index 08b2d69..f208651 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - Kiln Controller + Drejstuga Fun! @@ -23,13 +23,14 @@ +
Drejstuga Fun!
Sensor Temp
Target Temp
-
Status
+
   Heat        Cool        Air        Hazard     Door   
@@ -50,36 +51,23 @@
- - -
-
@@ -94,7 +82,6 @@