diff --git a/config.py b/config.py index 132e849..fed2ddd 100644 --- a/config.py +++ b/config.py @@ -117,3 +117,16 @@ 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 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 diff --git a/lib/max31855.py b/lib/max31855.py index a2f249c..60475a7 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 = self.unknownError = False # Initialize needed GPIO GPIO.setmode(self.board) @@ -70,20 +71,13 @@ 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 # SCV bit, D2 + self.unknownError = not (self.noConnection | self.shortToGround | self.shortToVCC) # Errk! + else: + 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.''' @@ -138,7 +132,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/max31856.py b/lib/max31856.py index f0fd4ef..4a3841b 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, 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 @@ -100,6 +100,8 @@ class MAX31856(object): MAX31856.MAX31856_X_TYPE. avgsel (1-byte Hex): Type of Averaging. Choose from values in CR0 table of datasheet. Default is single sample. + ac_freq_50hz: Set to True if your AC frequency is 50Hz, Set to False for 60Hz, + ocdetect: Detect open circuit errors (ie broken thermocouple). Choose from values in CR1 table of datasheet software_spi (dict): Contains the pin assignments for software SPI, as defined below: clk (integer): Pin number for software SPI clk cs (integer): Pin number for software SPI cs @@ -112,6 +114,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,11 +136,13 @@ class MAX31856(object): self._spi.set_mode(1) self._spi.set_bit_order(SPI.MSBFIRST) - 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, self.MAX31856_CR0_READ_CONT) + self._write_register(self.MAX31856_REG_WRITE_CR0, 0) 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): @@ -297,8 +303,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() diff --git a/lib/oven.py b/lib/oven.py index 2268388..8858c56 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -8,6 +8,7 @@ import config log = logging.getLogger(__name__) + class Output(object): def __init__(self): self.active = False @@ -29,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 @@ -82,7 +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 = self.unknownError = False class TempSensorSimulated(TempSensor): '''not much here, just need to be able to set the temperature''' @@ -94,6 +97,11 @@ class TempSensorReal(TempSensor): during the time_step''' def __init__(self): TempSensor.__init__(self) + self.sleeptime = self.time_step / float(config.temperature_average_samples) + self.bad_count = 0 + self.ok_count = 0 + self.bad_stamp = 0 + if config.max31855: log.info("init MAX31855") from max31855 import MAX31855, MAX31855Error @@ -110,26 +118,48 @@ 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, + ac_freq_50hz = config.ac_freq_50hz, ) def run(self): - '''take 5 measurements over each time period and return the - average''' + '''use a moving average of config.temperature_average_samples 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) - if len(temps) > 0: - self.temperature = sum(temps)/len(temps) + # reset error counter if time is up + if (time.time() - self.bad_stamp) > (self.time_step * 2): + if self.bad_count + self.ok_count: + self.bad_percent = (self.bad_count / (self.bad_count + self.ok_count)) * 100 + else: + self.bad_percent = 0 + self.bad_count = 0 + self.ok_count = 0 + self.bad_stamp = time.time() + + temp = self.thermocouple.get() + self.noConnection = self.thermocouple.noConnection + self.shortToGround = self.thermocouple.shortToGround + self.shortToVCC = self.thermocouple.shortToVCC + self.unknownError = self.thermocouple.unknownError + + is_bad_value = self.noConnection | self.unknownError + if config.honour_theromocouple_short_errors: + is_bad_value |= self.shortToGround | self.shortToVCC + + if not is_bad_value: + temps.append(temp) + if len(temps) > config.temperature_average_samples: + del temps[0] + self.ok_count += 1 + + else: + log.error(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) + time.sleep(self.sleeptime) class Oven(threading.Thread): '''parent oven class. this has all the common code @@ -152,8 +182,22 @@ 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 + 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 self.totaltime = profile.get_duration() self.state = "RUNNING" @@ -183,6 +227,9 @@ 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: @@ -192,12 +239,24 @@ 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") self.reset() + if self.board.temp_sensor.noConnection: + log.info("emergency!!! lost connection to thermocouple, shutting down") + self.reset() + + if self.board.temp_sensor.unknownError: + log.info("emergency!!! unknown thermocouple error, shutting down") + self.reset() + + if self.board.temp_sensor.bad_percent > 30: + log.info("emergency!!! too many errors in a short period, shutting down") + self.reset() + def reset_if_schedule_ended(self): if self.runtime > self.totaltime: log.info("schedule ended, shutting down") @@ -326,6 +385,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 + @@ -338,8 +401,10 @@ class RealOven(Oven): if heat_on > 0: self.heat = 1.0 - self.output.heat(heat_on) - self.output.cool(heat_off) + if 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,