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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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 6ab6dcd70164ca7d0360b5208a0a873652193927 Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Sun, 16 May 2021 11:40:02 +0100 Subject: [PATCH 14/20] 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 15/20] 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 16/20] 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 17/20] 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 18/20] 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 97bab2a248bd77100cfb0f300311d712bf5538cd Mon Sep 17 00:00:00 2001 From: Andrew de Quincey Date: Wed, 26 May 2021 17:31:01 +0100 Subject: [PATCH 19/20] 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 20/20] 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))