From 9b08342038b8335787b5e7acf6d78386c25d6bae Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Wed, 28 Apr 2021 23:10:48 +0100 Subject: [PATCH 01/28] rebuild temperature error handling and smoothing --- lib/max31855.py | 23 ++++++---------- lib/oven.py | 72 ++++++++++++++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/lib/max31855.py b/lib/max31855.py index a2f249c..1cd77aa 100644 --- a/lib/max31855.py +++ b/lib/max31855.py @@ -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,6 +26,7 @@ class MAX31855(object): self.units = units self.data = None self.board = board + self.noConnection = self.shortToGround = self.shortToVCC = False # Initialize needed GPIO GPIO.setmode(self.board) @@ -70,20 +71,12 @@ 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: - 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") + self.noConnection = (data_32 & 0x00000001) != 0 # OC bit, D0 + self.shortToGround = (data_32 & 0x00000002) != 0 # SCG bit, D1 + self.shortToVCC = (data_32 & 0x00000004) != 0 + else: + self.noConnection = self.shortToGround = self.shortToVCC = False def data_to_tc_temperature(self, data_32 = None): '''Takes an integer and returns a thermocouple temperature in celsius.''' @@ -138,7 +131,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 ''' diff --git a/lib/oven.py b/lib/oven.py index 17ea994..07ff563 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -8,6 +8,8 @@ import config log = logging.getLogger(__name__) +TEMPERATURE_MOVING_AVERAGE_SAMPLES = 40 + class Output(object): def __init__(self): self.active = False @@ -54,7 +56,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: @@ -64,7 +66,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): @@ -76,13 +78,14 @@ 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.time_step = config.sensor_time_wait + self.noConnection = self.shortToGround = self.shortToVCC = False class TempSensorSimulated(TempSensor): '''not much here, just need to be able to set the temperature''' @@ -94,6 +97,8 @@ class TempSensorReal(TempSensor): during the time_step''' def __init__(self): TempSensor.__init__(self) + self.sleeptime = self.time_step / float(TEMPERATURE_MOVING_AVERAGE_SAMPLES) + if config.max31855: log.info("init MAX31855") from max31855 import MAX31855, MAX31855Error @@ -114,20 +119,20 @@ class TempSensorReal(TempSensor): ) def run(self): - '''take 5 measurements over each time period and return the - average''' + '''use a moving average of TEMPERATURE_AVERAGE_WINDOW across the time_step''' + temps = [] while True: - 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) + temp = self.thermocouple.get() + temps.append(temp) + if len(temps) > TEMPERATURE_MOVING_AVERAGE_SAMPLES: + del temps[0] + + if len(temps): + self.temperature = sum(temps) / len(temps) + self.noConnection = self.thermocouple.noConnection + self.shortToGround = self.thermocouple.shortToGround + self.shortToVCC = self.thermocouple.shortToVCC + time.sleep(self.sleeptime) class Oven(threading.Thread): '''parent oven class. this has all the common code @@ -150,8 +155,19 @@ 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): - log.info("Running schedule %s" % profile.name) 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 + + log.info("Running schedule %s" % profile.name) self.profile = profile self.totaltime = profile.get_duration() self.state = "RUNNING" @@ -196,6 +212,10 @@ class Oven(threading.Thread): 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() + def reset_if_schedule_ended(self): if self.runtime > self.totaltime: log.info("schedule ended, shutting down") @@ -244,10 +264,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") @@ -305,9 +325,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) @@ -395,7 +415,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. @@ -409,7 +429,7 @@ class PID(): if self.ki > 0: self.iterm += (error * timeDelta * (1/self.ki)) - + dErr = (error - self.lastErr) / timeDelta output = self.kp * error + self.iterm + self.kd * dErr out4logs = output @@ -426,10 +446,6 @@ class PID(): 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 temp==%0.2f set=%0.2f err=%0.2f p=%0.2f i=%0.2f d=%0.2f pid=%0.2f out=%0.2f" % (ispoint, setpoint, error, self.kp * error, self.iterm, self.kd * dErr, out4logs, output)) return output From e1be00e2e3d0a623a72556e87cddd3af12fbf795 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Wed, 28 Apr 2021 23:38:36 +0100 Subject: [PATCH 02/28] fix comment --- lib/oven.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/oven.py b/lib/oven.py index f9990e1..141997d 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -120,7 +120,7 @@ class TempSensorReal(TempSensor): ) def run(self): - '''use a moving average of TEMPERATURE_AVERAGE_WINDOW across the time_step''' + '''use a moving average of TEMPERATURE_MOVING_AVERAGE_SAMPLES across the time_step''' temps = [] while True: temp = self.thermocouple.get() From 9c77d1ab302239bcae121896b0431b3b15bc64ec Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 1 May 2021 15:02:51 +0100 Subject: [PATCH 03/28] add config parameters for controlling temperature --- config.py | 26 ++++++++++++++++++-------- lib/max31855.py | 7 ++++--- lib/oven.py | 29 ++++++++++++++++++++++------- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/config.py b/config.py index 10d3a24..be68ded 100644 --- a/config.py +++ b/config.py @@ -57,7 +57,7 @@ sensor_time_wait = 2 # These parameters work 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_kp = 25 # Proportional pid_ki = 200 # Integral pid_kd = 200 # Derivative @@ -66,11 +66,11 @@ pid_kd = 200 # Derivative # # Initial heating and Integral Windup # -# During initial heating, if the temperature is constantly under the +# During initial heating, if the temperature is constantly under the # setpoint,large amounts of Integral can accumulate. This accumulation # causes the kiln to run above the setpoint for potentially a long # period of time. These settings allow integral accumulation only when -# the temperature is within stop_integral_windup_margin percent below +# the temperature is within stop_integral_windup_margin percent below # or above the setpoint. This applies only to the integral. stop_integral_windup = True stop_integral_windup_margin = 10 @@ -96,20 +96,20 @@ 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 = "f" # 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 # emergency shutoff the profile if this temp is reached or exceeded. # This just shuts off the profile. If your SSR is working, your kiln will -# naturally cool off. If your SSR has failed/shorted/closed circuit, this +# 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 = 2264 #cone 7 -# If the kiln cannot heat or cool fast enough and is off by more than +# 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 -# the desired temperature is reached. If your kiln cannot attain the +# the desired temperature is reached. If your kiln cannot attain the # wanted temperature, the schedule will run forever. kiln_must_catch_up = True kiln_must_catch_up_max_error = 10 #degrees @@ -119,3 +119,13 @@ kiln_must_catch_up_max_error = 10 #degrees # set set this offset to -4 to compensate. This probably means you have a # cheap thermocouple. Invest in a better thermocouple. thermocouple_offset=0 + +# some kilns/thermocouples start erroneously reporting "short" errors at higher temperatures +# due to plasma forming in the kiln. +# Set this to False to ignore these errors and assume the temperature reading was correct anyway +honour_theromocouple_short_errors = True + +# number of samples of temperature to average. +# If you suffer from ghe high temperature kiln issue and have set honour_theromocouple_short_errors to False, +# you will likely need to increase this (eg I use 40) +temperature_average_samples = 5 diff --git a/lib/max31855.py b/lib/max31855.py index 1cd77aa..60475a7 100644 --- a/lib/max31855.py +++ b/lib/max31855.py @@ -26,7 +26,7 @@ class MAX31855(object): self.units = units self.data = None self.board = board - self.noConnection = self.shortToGround = self.shortToVCC = False + self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False # Initialize needed GPIO GPIO.setmode(self.board) @@ -74,9 +74,10 @@ class MAX31855(object): 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 + 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 = False + self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False def data_to_tc_temperature(self, data_32 = None): '''Takes an integer and returns a thermocouple temperature in celsius.''' diff --git a/lib/oven.py b/lib/oven.py index e8c0c30..03fbae1 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -8,7 +8,6 @@ import config log = logging.getLogger(__name__) -TEMPERATURE_MOVING_AVERAGE_SAMPLES = 40 class Output(object): def __init__(self): @@ -124,15 +123,24 @@ class TempSensorReal(TempSensor): temps = [] while True: temp = self.thermocouple.get() - temps.append(temp) - if len(temps) > TEMPERATURE_MOVING_AVERAGE_SAMPLES: - del temps[0] - - if len(temps): - self.temperature = sum(temps) / len(temps) 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] + else: + log.error(f"Problem reading temp N/C:{self.noConnection} GND:{self.shortToGround} VCC:{self.shortToVCC} ???:{self.unknownError}") + + if len(temps): + self.temperature = sum(temps) / len(temps) time.sleep(self.sleeptime) class Oven(threading.Thread): @@ -167,6 +175,9 @@ class Oven(threading.Thread): 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.profile = profile @@ -217,6 +228,10 @@ class Oven(threading.Thread): 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() + def reset_if_schedule_ended(self): if self.runtime > self.totaltime: log.info("schedule ended, shutting down") From 0f2eb392c908ad7748d2b739bed0ba02d919cb13 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 1 May 2021 15:03:38 +0100 Subject: [PATCH 04/28] typo --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index be68ded..059d6dd 100644 --- a/config.py +++ b/config.py @@ -126,6 +126,6 @@ thermocouple_offset=0 honour_theromocouple_short_errors = True # number of samples of temperature to average. -# If you suffer from ghe high temperature kiln issue and have set honour_theromocouple_short_errors to False, +# If you suffer from the high temperature kiln issue and have set honour_theromocouple_short_errors to False, # you will likely need to increase this (eg I use 40) temperature_average_samples = 5 From 74a4aa3770083a70791a4fe6419f5511a039b2b7 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 1 May 2021 15:16:03 +0100 Subject: [PATCH 05/28] fix config.temperature_average_samples --- lib/oven.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/oven.py b/lib/oven.py index 03fbae1..c26e46c 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -96,7 +96,7 @@ class TempSensorReal(TempSensor): during the time_step''' def __init__(self): TempSensor.__init__(self) - self.sleeptime = self.time_step / float(TEMPERATURE_MOVING_AVERAGE_SAMPLES) + self.sleeptime = self.time_step / float(config.temperature_average_samples) if config.max31855: log.info("init MAX31855") @@ -119,7 +119,7 @@ class TempSensorReal(TempSensor): ) def run(self): - '''use a moving average of TEMPERATURE_MOVING_AVERAGE_SAMPLES across the time_step''' + '''use a moving average of config.temperature_average_samples across the time_step''' temps = [] while True: temp = self.thermocouple.get() From c955c1f595e4b0ec07fdfd6758b0ec3c1b3b3eda Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 1 May 2021 15:37:30 +0100 Subject: [PATCH 06/28] detect too many errors and shutdown --- lib/oven.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/oven.py b/lib/oven.py index c26e46c..526d474 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -97,6 +97,9 @@ class TempSensorReal(TempSensor): 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") @@ -122,6 +125,12 @@ class TempSensorReal(TempSensor): '''use a moving average of config.temperature_average_samples across the time_step''' temps = [] while True: + # reset error counter if time is up + if (time.time() - self.bad_stamp) > (self.time_step * 4): + 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 @@ -136,8 +145,11 @@ class TempSensorReal(TempSensor): temps.append(temp) if len(temps) > config.temperature_average_samples: del temps[0] + self.ok_count += 1 + else: log.error(f"Problem reading temp N/C:{self.noConnection} GND:{self.shortToGround} VCC:{self.shortToVCC} ???:{self.unknownError}") + self.bad_count += 1 if len(temps): self.temperature = sum(temps) / len(temps) @@ -218,7 +230,7 @@ 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''' + '''reset if the temperature is way TOO HOT, or other critical errors detected''' if (self.board.temp_sensor.temperature + config.thermocouple_offset >= config.emergency_shutoff_temp): log.info("emergency!!! temperature too high, shutting down") @@ -232,6 +244,10 @@ class Oven(threading.Thread): log.info("emergency!!! unknown thermocouple error, shutting down") self.reset() + if self.board.temp_sensor.bad_count / (self.board.temp_sensor.bad_count + self.board.temp_sensor.ok_count) > 0.3: + 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") From 610ad4cdcdb582d1de99e17594c3af25b797502f Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 1 May 2021 17:32:21 +0100 Subject: [PATCH 07/28] calculate bad percent in temperature thread to prevent threading/boundary issues --- lib/oven.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/oven.py b/lib/oven.py index 526d474..adfe692 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -100,6 +100,7 @@ class TempSensorReal(TempSensor): self.bad_count = 0 self.ok_count = 0 self.bad_stamp = 0 + self.bad_percent = 0 if config.max31855: log.info("init MAX31855") @@ -126,7 +127,8 @@ class TempSensorReal(TempSensor): temps = [] while True: # reset error counter if time is up - if (time.time() - self.bad_stamp) > (self.time_step * 4): + if (time.time() - self.bad_stamp) > (self.time_step * 2): + self.bad_percent = (self.bad_count / (self.bad_count + self.ok_count)) * 100 self.bad_count = 0 self.ok_count = 0 self.bad_stamp = time.time() @@ -244,7 +246,7 @@ class Oven(threading.Thread): log.info("emergency!!! unknown thermocouple error, shutting down") self.reset() - if self.board.temp_sensor.bad_count / (self.board.temp_sensor.bad_count + self.board.temp_sensor.ok_count) > 0.3: + if self.board.temp_sensor.bad_percent > 30: log.info("emergency!!! too many errors in a short period, shutting down") self.reset() From 83512b1faf882fda380ed233f62d895461c3832b Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 1 May 2021 17:34:13 +0100 Subject: [PATCH 08/28] fix simulated sensor --- lib/oven.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/oven.py b/lib/oven.py index adfe692..2d1d795 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -83,8 +83,9 @@ class TempSensor(threading.Thread): 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 = False + self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False class TempSensorSimulated(TempSensor): '''not much here, just need to be able to set the temperature''' @@ -100,7 +101,6 @@ class TempSensorReal(TempSensor): self.bad_count = 0 self.ok_count = 0 self.bad_stamp = 0 - self.bad_percent = 0 if config.max31855: log.info("init MAX31855") From ec3c825e8e29d400ab0043083ebfcc1351a259a8 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 1 May 2021 20:27:48 +0100 Subject: [PATCH 09/28] fix percent calculation --- lib/oven.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/oven.py b/lib/oven.py index 2d1d795..4d790b7 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -128,7 +128,10 @@ class TempSensorReal(TempSensor): while True: # reset error counter if time is up if (time.time() - self.bad_stamp) > (self.time_step * 2): - self.bad_percent = (self.bad_count / (self.bad_count + self.ok_count)) * 100 + 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() From 6f92021f09b57fe788113dbf52540be2b1c95c32 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 15 May 2021 18:27:15 +0100 Subject: [PATCH 10/28] add line frequency setting and expose avgsel value --- config.py | 11 +++++++++++ lib/max31856.py | 11 +++++++---- lib/oven.py | 6 ++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/config.py b/config.py index 059d6dd..7e76785 100644 --- a/config.py +++ b/config.py @@ -129,3 +129,14 @@ honour_theromocouple_short_errors = True # If you suffer from the high temperature kiln issue and have set honour_theromocouple_short_errors to False, # you will likely need to increase this (eg I use 40) temperature_average_samples = 5 + +# Thermocouple AC frequency filtering - set to True if in a 50Hz locale, else leave at False for 60Hz locale +ac_freq_50hz = False + +# MAX31856 avgsel -- number of samples averaged on-chip prior to returning result. Values: +# 0: 1 sample +# 1: 2 samples +# 2: 4 samples +# 3: 8 samples +# 4: 16 samples +max31856_avgsel = 0 diff --git a/lib/max31856.py b/lib/max31856.py index f0fd4ef..d0bc2be 100644 --- a/lib/max31856.py +++ b/lib/max31856.py @@ -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, software_spi=None, hardware_spi=None, gpio=None): + def __init__(self, tc_type=MAX31856_S_TYPE, units="c", avgsel=0x0, ac_freq_50hz=False, 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,6 +100,7 @@ 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, 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 @@ -112,6 +113,8 @@ 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') @@ -132,10 +135,12 @@ class MAX31856(object): self._spi.set_mode(1) self._spi.set_bit_order(SPI.MSBFIRST) + self.cr0 = self.MAX31856_CR0_READ_CONT | (1 if ac_freq_50hz else 0) self.cr1 = ((self.avgsel << 4) + self.tc_type) # Setup for reading continuously with T-Type thermocouple - self._write_register(self.MAX31856_REG_WRITE_CR0, self.MAX31856_CR0_READ_CONT) + self._write_register(self.MAX31856_REG_WRITE_CR0, 0) + self._write_register(self.MAX31856_REG_WRITE_CR0, self.cr0) self._write_register(self.MAX31856_REG_WRITE_CR1, self.cr1) @staticmethod @@ -300,5 +305,3 @@ class MAX31856(object): def get(self): celcius = self.read_temp_c() return getattr(self, "to_" + self.units)(celcius) - - diff --git a/lib/oven.py b/lib/oven.py index 4d790b7..9011330 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -118,8 +118,10 @@ class TempSensorReal(TempSensor): 'do': config.gpio_sensor_data, 'di': config.gpio_sensor_di } self.thermocouple = MAX31856(tc_type=config.thermocouple_type, - software_spi = sofware_spi, - units = config.temp_scale + software_spi = software_spi, + units = config.temp_scale, + avgsel = config.max31856_avgsel, + ac_freq_50hz = config.ac_freq_50hz, ) def run(self): From 225e591f3525be8b12e0191c38577d45ae40c2b7 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 15 May 2021 18:44:56 +0100 Subject: [PATCH 11/28] don't change the GPIO unless we actually need to --- lib/oven.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/oven.py b/lib/oven.py index 9011330..6aa4c68 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -30,10 +30,10 @@ class Output(object): def heat(self,sleepfor): self.GPIO.output(config.gpio_heat, self.GPIO.HIGH) 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 @@ -383,6 +383,10 @@ 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 + @@ -395,8 +399,10 @@ class RealOven(Oven): if heat_on > 0: self.heat = 1.0 - self.output.heat(heat_on) - self.output.cool(heat_off) + if self.output.heat_on: + self.output.heat(heat_on) + if heat_off: + 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, From c1ca132697433b9e302906afc4a419e9b5dd830a Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 15 May 2021 18:45:20 +0100 Subject: [PATCH 12/28] bugfix --- lib/oven.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/oven.py b/lib/oven.py index 6aa4c68..6a49609 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -399,7 +399,7 @@ class RealOven(Oven): if heat_on > 0: self.heat = 1.0 - if self.output.heat_on: + if heat_on: self.output.heat(heat_on) if heat_off: self.output.cool(heat_off) From 82d7cab0a4b7e361205834b15cd8bb9bf3491eca Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sat, 15 May 2021 18:47:50 +0100 Subject: [PATCH 13/28] reorder register setup -- setup avgsel first, THEN enable continuous mode --- lib/max31856.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/max31856.py b/lib/max31856.py index d0bc2be..558f2b6 100644 --- a/lib/max31856.py +++ b/lib/max31856.py @@ -140,8 +140,8 @@ class MAX31856(object): # 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.cr0) 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): From cd7b3cd26bf24a93c8236e7893afa544af02531e Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sun, 16 May 2021 00:43:34 +0100 Subject: [PATCH 14/28] tweak windup so it isn't temperature dependant --- config.py | 18 ++++++++---------- lib/oven.py | 33 ++++++++++++++++----------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/config.py b/config.py index 10d3a24..132e849 100644 --- a/config.py +++ b/config.py @@ -57,7 +57,7 @@ sensor_time_wait = 2 # These parameters work 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_kp = 25 # Proportional pid_ki = 200 # Integral pid_kd = 200 # Derivative @@ -66,14 +66,12 @@ pid_kd = 200 # Derivative # # Initial heating and Integral Windup # -# During initial heating, if the temperature is constantly under the +# During initial heating, if the temperature is constantly under the # setpoint,large amounts of Integral can accumulate. This accumulation # causes the kiln to run above the setpoint for potentially a long # period of time. These settings allow integral accumulation only when -# the temperature is within stop_integral_windup_margin percent below -# or above the setpoint. This applies only to the integral. +# the temperature is close to the setpoint. This applies only to the integral. stop_integral_windup = True -stop_integral_windup_margin = 10 ######################################################################## # @@ -96,20 +94,20 @@ 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 = "f" # 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 # emergency shutoff the profile if this temp is reached or exceeded. # This just shuts off the profile. If your SSR is working, your kiln will -# naturally cool off. If your SSR has failed/shorted/closed circuit, this +# 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 = 2264 #cone 7 -# If the kiln cannot heat or cool fast enough and is off by more than +# 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 -# the desired temperature is reached. If your kiln cannot attain the +# the desired temperature is reached. If your kiln cannot attain the # wanted temperature, the schedule will run forever. kiln_must_catch_up = True kiln_must_catch_up_max_error = 10 #degrees diff --git a/lib/oven.py b/lib/oven.py index da6f2d4..1183e34 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -54,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: @@ -64,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): @@ -76,7 +76,7 @@ class Board(object): class BoardSimulated(object): def __init__(self): self.temp_sensor = TempSensorSimulated() - + class TempSensor(threading.Thread): def __init__(self): threading.Thread.__init__(self) @@ -118,7 +118,7 @@ class TempSensorReal(TempSensor): '''take 5 measurements over each time period and return the average''' while True: - maxtries = 5 + maxtries = 5 sleeptime = self.time_step / float(maxtries) temps = [] for x in range(0,maxtries): @@ -246,10 +246,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") @@ -307,9 +307,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) @@ -397,7 +397,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. @@ -411,12 +411,11 @@ class PID(): if self.ki > 0: if config.stop_integral_windup == True: - margin = setpoint * config.stop_integral_windup_margin/100 - if (abs(error) <= abs(margin)): + if self.kp * error < window_size: self.iterm += (error * timeDelta * (1/self.ki)) else: self.iterm += (error * timeDelta * (1/self.ki)) - + dErr = (error - self.lastErr) / timeDelta output = self.kp * error + self.iterm + self.kd * dErr out4logs = output @@ -430,14 +429,14 @@ class PID(): output = float(output / window_size) - if out4logs > 0: + 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.kp * error)/out4logs)*100, # (self.iterm/out4logs)*100, -# ((self.kd * dErr)/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.kp * error, self.iterm, - self.kd * dErr)) + self.kd * dErr)) return output From 6ab6dcd70164ca7d0360b5208a0a873652193927 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sun, 16 May 2021 11:40:02 +0100 Subject: [PATCH 15/28] add open circuit detection code --- lib/max31856.py | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/lib/max31856.py b/lib/max31856.py index 558f2b6..0a3b2ad 100644 --- a/lib/max31856.py +++ b/lib/max31856.py @@ -89,7 +89,12 @@ 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, software_spi=None, hardware_spi=None, gpio=None): + MAX31856_OCDETECT_OFF = 0x00 # open circuit detection disabled + MAX31856_OCDETECT_1 = 0x01 # enabled every 16 conversions (Rs < 5kOhm) + MAX31856_OCDETECT_2 = 0x02 # enabled every 16 conversions (40kOhm < Rs > 5kOhm, time < 2ms) + MAX31856_OCDETECT_3 = 0x03 # enabled every 16 conversions (40kOhm < Rs > 5kOhm, time > 2ms) + + def __init__(self, tc_type=MAX31856_S_TYPE, units="c", avgsel=0x0, ac_freq_50hz=False, ocdetect=0, 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 @@ -135,8 +140,8 @@ class MAX31856(object): self._spi.set_mode(1) self._spi.set_bit_order(SPI.MSBFIRST) - self.cr0 = self.MAX31856_CR0_READ_CONT | (1 if ac_freq_50hz else 0) - self.cr1 = ((self.avgsel << 4) + self.tc_type) + 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)) # Setup for reading continuously with T-Type thermocouple self._write_register(self.MAX31856_REG_WRITE_CR0, 0) @@ -302,6 +307,39 @@ 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() From be4228da37f030a455dec25684f11bbbb36876e7 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sun, 16 May 2021 11:59:22 +0100 Subject: [PATCH 16/28] implement ocdetect and remove avgsel --- config.py | 8 -------- lib/max31856.py | 8 ++------ lib/oven.py | 1 - 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/config.py b/config.py index 7e76785..c4bf853 100644 --- a/config.py +++ b/config.py @@ -132,11 +132,3 @@ temperature_average_samples = 5 # Thermocouple AC frequency filtering - set to True if in a 50Hz locale, else leave at False for 60Hz locale ac_freq_50hz = False - -# MAX31856 avgsel -- number of samples averaged on-chip prior to returning result. Values: -# 0: 1 sample -# 1: 2 samples -# 2: 4 samples -# 3: 8 samples -# 4: 16 samples -max31856_avgsel = 0 diff --git a/lib/max31856.py b/lib/max31856.py index 0a3b2ad..4a3841b 100644 --- a/lib/max31856.py +++ b/lib/max31856.py @@ -89,12 +89,7 @@ class MAX31856(object): MAX31856_S_TYPE = 0x6 # Read S Type Thermocouple MAX31856_T_TYPE = 0x7 # Read T Type Thermocouple - MAX31856_OCDETECT_OFF = 0x00 # open circuit detection disabled - MAX31856_OCDETECT_1 = 0x01 # enabled every 16 conversions (Rs < 5kOhm) - MAX31856_OCDETECT_2 = 0x02 # enabled every 16 conversions (40kOhm < Rs > 5kOhm, time < 2ms) - MAX31856_OCDETECT_3 = 0x03 # enabled every 16 conversions (40kOhm < Rs > 5kOhm, time > 2ms) - - def __init__(self, tc_type=MAX31856_S_TYPE, units="c", avgsel=0x0, ac_freq_50hz=False, ocdetect=0, software_spi=None, hardware_spi=None, gpio=None): + 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): """ Initialize MAX31856 device with software SPI on the specified CLK, CS, and DO pins. Alternatively can specify hardware SPI by sending an @@ -106,6 +101,7 @@ class MAX31856(object): 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 diff --git a/lib/oven.py b/lib/oven.py index 6a49609..565a9bc 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -120,7 +120,6 @@ class TempSensorReal(TempSensor): self.thermocouple = MAX31856(tc_type=config.thermocouple_type, software_spi = software_spi, units = config.temp_scale, - avgsel = config.max31856_avgsel, ac_freq_50hz = config.ac_freq_50hz, ) From 665863d3102e75d8b6eb6d5a60cad988b0966d4d Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Tue, 18 May 2021 01:21:12 +0100 Subject: [PATCH 17/28] prevent runtime going negative at start of run in catchup mode --- lib/oven.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/oven.py b/lib/oven.py index 565a9bc..df049d6 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -231,6 +231,8 @@ class Oven(threading.Thread): self.runtime = self.startat + runtime_delta.total_seconds() else: self.runtime = runtime_delta.total_seconds() + if self.runtime < 0: + self.runtime = 0 def update_target_temp(self): self.target = self.profile.get_target_temperature(self.runtime) From 1c8f5843f025fcdacb83ce2ff1cc0fab26267a60 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Wed, 26 May 2021 00:52:49 +0100 Subject: [PATCH 18/28] disable item update during kiln catchup --- lib/oven.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/oven.py b/lib/oven.py index df049d6..3b80c1a 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -219,11 +219,15 @@ class Oven(threading.Thread): log.info("kiln must catch up, too cold, shifting schedule") self.start_time = self.start_time + \ datetime.timedelta(seconds=self.time_step) + return True # kiln too hot, wait for it to cool down if temp - self.target > config.kiln_must_catch_up_max_error: log.info("kiln must catch up, too hot, shifting schedule") self.start_time = self.start_time + \ datetime.timedelta(seconds=self.time_step) + return True + + return False def update_runtime(self): runtime_delta = datetime.datetime.now() - self.start_time @@ -279,10 +283,10 @@ class Oven(threading.Thread): time.sleep(1) continue if self.state == "RUNNING": - self.kiln_must_catch_up() + catching_up = self.kiln_must_catch_up() self.update_runtime() self.update_target_temp() - self.heat_then_cool() + self.heat_then_cool(catching_up) self.reset_if_emergency() self.reset_if_schedule_ended() @@ -334,10 +338,11 @@ class SimulatedOven(Oven): self.temperature = self.t self.board.temp_sensor.temperature = self.t - def heat_then_cool(self): + def heat_then_cool(self, catching_up): pid = self.pid.compute(self.target, self.board.temp_sensor.temperature + - config.thermocouple_offset) + config.thermocouple_offset, + catching_up) heat_on = float(self.time_step * pid) heat_off = float(self.time_step * (1 - pid)) @@ -388,10 +393,11 @@ class RealOven(Oven): super().reset() self.output.cool(0) - def heat_then_cool(self): + def heat_then_cool(self, catching_up): pid = self.pid.compute(self.target, self.board.temp_sensor.temperature + - config.thermocouple_offset) + config.thermocouple_offset, + catching_up) heat_on = float(self.time_step * pid) heat_off = float(self.time_step * (1 - pid)) @@ -465,7 +471,7 @@ class PID(): # 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. - def compute(self, setpoint, ispoint): + def compute(self, setpoint, ispoint, catching_up): now = datetime.datetime.now() timeDelta = (now - self.lastNow).total_seconds() @@ -476,7 +482,7 @@ class PID(): if self.ki > 0: if config.stop_integral_windup == True: margin = setpoint * config.stop_integral_windup_margin/100 - if (abs(error) <= abs(margin)): + if (abs(error) <= abs(margin)) and not catching_up: self.iterm += (error * timeDelta * (1/self.ki)) else: self.iterm += (error * timeDelta * (1/self.ki)) From 499f029ce5e9f70d94c96768eae47f6f4d4a0acf Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Wed, 26 May 2021 17:23:43 +0100 Subject: [PATCH 19/28] prevent self.runtime from going negative during profile run --- lib/oven.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/oven.py b/lib/oven.py index 3b80c1a..b9567a1 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -231,12 +231,13 @@ 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: self.runtime = runtime_delta.total_seconds() - if self.runtime < 0: - self.runtime = 0 def update_target_temp(self): self.target = self.profile.get_target_temperature(self.runtime) @@ -283,10 +284,10 @@ class Oven(threading.Thread): time.sleep(1) continue if self.state == "RUNNING": - catching_up = self.kiln_must_catch_up() + self.kiln_must_catch_up() self.update_runtime() self.update_target_temp() - self.heat_then_cool(catching_up) + self.heat_then_cool() self.reset_if_emergency() self.reset_if_schedule_ended() @@ -338,11 +339,10 @@ class SimulatedOven(Oven): self.temperature = self.t self.board.temp_sensor.temperature = self.t - def heat_then_cool(self, catching_up): + def heat_then_cool(self): pid = self.pid.compute(self.target, self.board.temp_sensor.temperature + - config.thermocouple_offset, - catching_up) + config.thermocouple_offset) heat_on = float(self.time_step * pid) heat_off = float(self.time_step * (1 - pid)) @@ -471,7 +471,7 @@ class PID(): # 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. - def compute(self, setpoint, ispoint, catching_up): + def compute(self, setpoint, ispoint): now = datetime.datetime.now() timeDelta = (now - self.lastNow).total_seconds() @@ -482,7 +482,7 @@ class PID(): if self.ki > 0: if config.stop_integral_windup == True: margin = setpoint * config.stop_integral_windup_margin/100 - if (abs(error) <= abs(margin)) and not catching_up: + if (abs(error) <= abs(margin)): self.iterm += (error * timeDelta * (1/self.ki)) else: self.iterm += (error * timeDelta * (1/self.ki)) From 941a158295a7ae944a28237f4b810dcfdab53b50 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Wed, 26 May 2021 17:24:19 +0100 Subject: [PATCH 20/28] should be abs() --- lib/oven.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/oven.py b/lib/oven.py index 1183e34..2268388 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -411,7 +411,7 @@ class PID(): if self.ki > 0: if config.stop_integral_windup == True: - if self.kp * error < window_size: + if abs(self.kp * error) < window_size: self.iterm += (error * timeDelta * (1/self.ki)) else: self.iterm += (error * timeDelta * (1/self.ki)) From 97bab2a248bd77100cfb0f300311d712bf5538cd Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Wed, 26 May 2021 17:31:01 +0100 Subject: [PATCH 21/28] remove unwanted return --- lib/oven.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/oven.py b/lib/oven.py index b9567a1..d7938f8 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -219,15 +219,11 @@ class Oven(threading.Thread): log.info("kiln must catch up, too cold, shifting schedule") self.start_time = self.start_time + \ datetime.timedelta(seconds=self.time_step) - return True # kiln too hot, wait for it to cool down if temp - self.target > config.kiln_must_catch_up_max_error: log.info("kiln must catch up, too hot, shifting schedule") self.start_time = self.start_time + \ datetime.timedelta(seconds=self.time_step) - return True - - return False def update_runtime(self): runtime_delta = datetime.datetime.now() - self.start_time From bee27bed29a6a02ade2ead117d6059dc2cffa601 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Wed, 26 May 2021 18:30:42 +0100 Subject: [PATCH 22/28] remove more dead code --- lib/oven.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/oven.py b/lib/oven.py index d7938f8..e38be34 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -389,11 +389,10 @@ class RealOven(Oven): super().reset() self.output.cool(0) - def heat_then_cool(self, catching_up): + def heat_then_cool(self): pid = self.pid.compute(self.target, self.board.temp_sensor.temperature + - config.thermocouple_offset, - catching_up) + config.thermocouple_offset) heat_on = float(self.time_step * pid) heat_off = float(self.time_step * (1 - pid)) From 7bd5c969e4eb787c0b5b78b64c5503855c1ccf83 Mon Sep 17 00:00:00 2001 From: Jason Bruce Date: Tue, 1 Jun 2021 15:23:45 -0400 Subject: [PATCH 23/28] - updated the installation docs --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0f5b306..a023e07 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,14 @@ My controller plugs into the wall, and the kiln plugs into the controller. ## Software -### Raspbian +### Raspberry PI OS -Download [NOOBs](https://www.raspberrypi.org/downloads/noobs/). Copy files to an SD card. Install raspbian on RPi using NOOBs. +Download [Raspberry PI OS](https://www.raspberrypi.org/software/). Use Rasberry PI Imaging tool to install the OS on an SD card. Boot the OS, open a terminal and... - $ sudo apt-get install python3-pip python3-virtualenv libevent-dev git virtualenv - $ git clone https://github.com/jbruce12000/kiln-controller.git + $ sudo apt-get update + $ sudo apt-get dist-upgrade + $ sudo apt-get install python3-virtualenv libevent-dev virtualenv + $ git clone https://github.com/jbruce12000/kiln-controller $ cd kiln-controller $ virtualenv -p python3 venv $ source venv/bin/activate From 937228cc5fa0efaf3143c014ad30e3884aef6117 Mon Sep 17 00:00:00 2001 From: Jason Bruce Date: Fri, 4 Jun 2021 09:08:57 -0400 Subject: [PATCH 24/28] - changed the pid tuning page to a manual, pracitical approach --- docs/pid_tuning.md | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/docs/pid_tuning.md b/docs/pid_tuning.md index c90ad9f..995ee36 100644 --- a/docs/pid_tuning.md +++ b/docs/pid_tuning.md @@ -10,45 +10,41 @@ A controller with properly tuned PID values reacts quickly to changes in the set ## Try the Existing Values -My kiln is Skutt KS-1018 with a kiln vent. Try the current settings for pid_kp, pid_ki, and pid_kd and if they work for you, you're done. Otherwise, you have some experimentation ahead of you. The following exercise took me about 2 hours of testing. +My kiln is Skutt KS-1018 with a kiln vent. Try the current settings for pid_kp, pid_ki, and pid_kd and if they work for you, you're done. Otherwise, you have some experimentation ahead of you. The following exercise took me about an hour of testing. ## The Tuning Process -in config.py set the PID values like so... +I'm a big fan of manual tuning. Let's start with some reasonable values for PID settings in config.py... - pid_kp = 1.0 - pid_ki = 0.0 - pid_kd = 0.0 + pid_kp = 20 + pid_ki = 50 + pid_kd = 100 -run a test schedule. I used a schedule that switches between 200 and 250 F every 30 minutes. +When you change values, change only one at a time and watch the impact. Change values by either doubling or halving. -What you are looking for is overshoot (in my case 25F) past 200F to 225F. The next thing the controller should do is undershoot by just a little below the set point of 200F. If these two things happen, great. If not, you will need to change pid_kp to a higher or lower value. +Run a test schedule. I used a schedule that switches between 200 and 250 F every 30 minutes. The kiln will likely shoot past 200. This is normal. We'll eventually get rid of most of the overshoot, but probably not all. -Once you get the overshoot and minimal undershoot, you need to record some values. First grab the overshoot... in my case 25F. +Let's balance pid_ki first (the integral). The lower the pid_ki, the greater the impact it will have on the system. If a system is consistently low or high, the integral is used to help bring the system closer to the set point. The integral accumulates over time and has [potentially] a bigger and bigger impact. - pid_kp = 25 +* If you have a steady state (no oscillations), but the temperature is always above the set point, increase pid_ki. +* If you have a steady state (no oscillations), but the temperature is always above the set point, decrease pid_ki. +* If you have an oscillation but the temperature is mostly above the setpoint, increase pid_ki. +* If you have an oscillation but the temperature is mostly below the setpoint, decrease pid_ki. -Measure the time in seconds from high peak to low peak. In my case this was 725 seconds. Multiply that number by 1.5 to get the Integral. So 725 * 1.5 = 1088. +Let's set pid_kp next (proportional). Think of pid_kp as a dimmable light switch that turns on the heat when below the set point and turns it off when above. The brightness of the dimmable light is defined by pid_kp. Be careful reducing pid_kp too much. It can result in strange behavior. - pid_ki = 1088 +* If you have oscillations that don't stop or increase in size, reduce pid_kp +* If you have too much overshoot (after adjusting pid_kd), reduce pid_kp +* If you approach the set point wayyy tooo sloooowly, increase pid_kp + +Now set pid_kd (derivative). pid_kd makes an impact when there is a change in temperature. It's used to reduce oscillations. -Now set the derivative at 1/5 of the Integral. So 1088/5 = 217 +* If you have oscillations that take too long to settle, increase pid_kp +* If you have crazy, unpredictable changes in temperature, reduce pid_kp - pid_kd = 217 - -in essence these values mean... - -| setting | Value | Action | -| ------- | ----- | ------ | -| pid_kp | 25 | react pretty slowly | -| pid_ki | 1088 | predict really far forward in time and make changes early | -| pid_kd | 217 | heavily dampen oscillations | - -Now, run the test schedule again and see how well it works. Expect some overshoot as the kiln reaches the set temperature the first time, but no oscillation. Any holds or ramps after that should have a smooth transition and should remain really close to the set point [1 or 2 degrees F]. +Expect some overshoot as the kiln reaches the set temperature the first time, but no oscillation. Any holds or ramps after that should have a smooth transition and should remain really close to the set point [1 or 2 degrees F]. ## Troubleshooting * only change one value at a time, then test it. -* If there is too much overshoot, decrease pid_kp. -* If the temp is always below the set point, decrease pid_ki (which increases the integral action). * if the above does not work, try the Ziegler / Nichols method https://blog.opticontrols.com/archives/477 From 5574c467b2339b005d65818ea1fa8e94def70ffe Mon Sep 17 00:00:00 2001 From: Jason Bruce Date: Fri, 4 Jun 2021 09:12:49 -0400 Subject: [PATCH 25/28] fixed bug in instructions --- docs/pid_tuning.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pid_tuning.md b/docs/pid_tuning.md index 995ee36..ac4ed18 100644 --- a/docs/pid_tuning.md +++ b/docs/pid_tuning.md @@ -27,7 +27,7 @@ Run a test schedule. I used a schedule that switches between 200 and 250 F every Let's balance pid_ki first (the integral). The lower the pid_ki, the greater the impact it will have on the system. If a system is consistently low or high, the integral is used to help bring the system closer to the set point. The integral accumulates over time and has [potentially] a bigger and bigger impact. * If you have a steady state (no oscillations), but the temperature is always above the set point, increase pid_ki. -* If you have a steady state (no oscillations), but the temperature is always above the set point, decrease pid_ki. +* If you have a steady state (no oscillations), but the temperature is always below the set point, decrease pid_ki. * If you have an oscillation but the temperature is mostly above the setpoint, increase pid_ki. * If you have an oscillation but the temperature is mostly below the setpoint, decrease pid_ki. @@ -40,7 +40,7 @@ Let's set pid_kp next (proportional). Think of pid_kp as a dimmable light switch Now set pid_kd (derivative). pid_kd makes an impact when there is a change in temperature. It's used to reduce oscillations. * If you have oscillations that take too long to settle, increase pid_kp -* If you have crazy, unpredictable changes in temperature, reduce pid_kp +* If you have crazy, unpredictable behavior from the controller, reduce pid_kp Expect some overshoot as the kiln reaches the set temperature the first time, but no oscillation. Any holds or ramps after that should have a smooth transition and should remain really close to the set point [1 or 2 degrees F]. From 09ee64403fc6c97d6625cb00042304ab89408a37 Mon Sep 17 00:00:00 2001 From: Jason Bruce Date: Mon, 7 Jun 2021 08:22:21 -0400 Subject: [PATCH 26/28] adding a list of thermocouple types and feature about integral wind-up --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a023e07..9202c35 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,12 @@ Turns a Raspberry Pi into an inexpensive, web-enabled kiln controller. * supports PID parameters you tune to your kiln * monitors temperature in kiln after schedule has ended * api for starting and stopping at any point in a schedule - * support of MAX31856 + * supports MAX31856 and MAX31855 thermocouple boards + * support for K, J, N, R, S, T, E, or B type thermocouples * accurate simulation - * support for shifting schedule when kiln cannot heat quickly enough + * support for shifting schedule when kiln cannot heat quickly enough + * support for preventing initial integral wind-up + **Run Kiln Schedule** From c5e5acc725475e7fcc1b4566eb76b2fbb4e984e8 Mon Sep 17 00:00:00 2001 From: Jason Bruce Date: Mon, 7 Jun 2021 10:32:17 -0400 Subject: [PATCH 27/28] setting sane defaults for new features --- config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.py b/config.py index fed2ddd..966658e 100644 --- a/config.py +++ b/config.py @@ -121,12 +121,12 @@ thermocouple_offset=0 # some kilns/thermocouples start erroneously reporting "short" errors at higher temperatures # due to plasma forming in the kiln. # Set this to False to ignore these errors and assume the temperature reading was correct anyway -honour_theromocouple_short_errors = True +honour_theromocouple_short_errors = False # number of samples of temperature to average. # If you suffer from the high temperature kiln issue and have set honour_theromocouple_short_errors to False, # you will likely need to increase this (eg I use 40) -temperature_average_samples = 5 +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 From 4a399aa1034c2934e188c73338964f0e99a0c142 Mon Sep 17 00:00:00 2001 From: jbruce12000 Date: Mon, 7 Jun 2021 16:40:09 -0400 Subject: [PATCH 28/28] changing print statement to work on python 3.5 --- lib/oven.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/oven.py b/lib/oven.py index 8858c56..974c5a0 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -154,7 +154,7 @@ class TempSensorReal(TempSensor): self.ok_count += 1 else: - log.error(f"Problem reading temp N/C:{self.noConnection} GND:{self.shortToGround} VCC:{self.shortToVCC} ???:{self.unknownError}") + 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):