From 2fa18589a50dfd1e66452bb63dfecc4aba9749d9 Mon Sep 17 00:00:00 2001 From: jbruce Date: Wed, 2 Nov 2022 09:31:48 -0900 Subject: [PATCH] exception handling completed for 31855 and 31856 --- config.py | 48 ++++--- lib/max31855.py | 265 ----------------------------------- lib/max31855spi.py | 36 ----- lib/max31856.py | 341 --------------------------------------------- lib/oven.py | 223 ++++++++++++++++++----------- requirements.txt | 13 +- 6 files changed, 177 insertions(+), 749 deletions(-) delete mode 100644 lib/max31855.py delete mode 100644 lib/max31855spi.py delete mode 100644 lib/max31856.py diff --git a/config.py b/config.py index 25ced07..1015521 100644 --- a/config.py +++ b/config.py @@ -66,17 +66,20 @@ gpio_heat = board.D23 #output that controls relay ### Thermocouple Adapter selection: # max31855 - bitbang SPI interface # max31856 - bitbang SPI interface. must specify thermocouple_type. -#max31855 = 1 -#max31856 = 0 -# see lib/max31856.py for other thermocouple_type, only applies to max31856 +max31855 = 1 +max31856 = 0 # uncomment this if using MAX-31856 -#thermocouple_type = MAX31856.MAX31856_S_TYPE +#thermocouple_type = ThermocoupleType.S -### Thermocouple Connection (using bitbang interfaces) -#gpio_sensor_cs = 27 -#gpio_sensor_clock = 22 -#gpio_sensor_data = 17 -#gpio_sensor_di = 10 # only used with max31856 +# here are the possible max-31856 thermocouple types +# ThermocoupleType.B +# ThermocoupleType.E +# ThermocoupleType.J +# ThermocoupleType.K +# ThermocoupleType.N +# ThermocoupleType.R +# ThermocoupleType.S +# ThermocoupleType.T ######################################################################## # @@ -112,7 +115,7 @@ stop_integral_windup = True ######################################################################## # # Simulation parameters -simulate = True +simulate = False sim_t_env = 60.0 # deg C sim_c_heat = 500.0 # J/K heat capacity of heat element sim_c_oven = 5000.0 # J/K heat capacity of oven @@ -179,16 +182,25 @@ ac_freq_50hz = False # - unknown error with thermocouple # - too many errors in a short period from thermocouple # but in some cases, you might want to ignore a specific error, log it, -# and continue running your profile. +# and continue running your profile instead of having the process die. +# +# You should only set these to True if you experience a problem +# and WANT to ignore it to complete a firing. ignore_temp_too_high = False -ignore_lost_connection_tc = False -ignore_unknown_tc_error = False -ignore_too_many_tc_errors = False -# some kilns/thermocouples start erroneously reporting "short" -# errors at higher temperatures due to plasma forming in the kiln. -# Set this to True to ignore these errors and assume the temperature -# reading was correct anyway +ignore_tc_lost_connection = False +ignore_tc_cold_junction_range_error = False +ignore_tc_range_error = False +ignore_tc_cold_junction_temp_high = False +ignore_tc_cold_junction_temp_low = False +ignore_tc_temp_high = False +ignore_tc_temp_low = False +ignore_tc_voltage_error = False ignore_tc_short_errors = False +ignore_tc_unknown_error = False + +# This overrides all possible thermocouple errors and prevents the +# process from exiting. +ignore_tc_too_many_errors = False ######################################################################## # automatic restarts - if you have a power brown-out and the raspberry pi diff --git a/lib/max31855.py b/lib/max31855.py deleted file mode 100644 index 60475a7..0000000 --- a/lib/max31855.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/python -import RPi.GPIO as GPIO -import math - -class MAX31855(object): - '''Python driver for [MAX38155 Cold-Junction Compensated Thermocouple-to-Digital Converter](http://www.maximintegrated.com/datasheet/index.mvp/id/7273) - Requires: - - The [GPIO Library](https://code.google.com/p/raspberry-gpio-python/) (Already on most Raspberry Pi OS builds) - - A [Raspberry Pi](http://www.raspberrypi.org/) - - ''' - def __init__(self, cs_pin, clock_pin, data_pin, units = "c", board = GPIO.BCM): - '''Initialize Soft (Bitbang) SPI bus - - Parameters: - - 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") - - board: (optional) pin numbering method as per RPi.GPIO library (GPIO.BCM (default) | GPIO.BOARD) - - ''' - self.cs_pin = cs_pin - self.clock_pin = clock_pin - self.data_pin = data_pin - 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) - GPIO.setup(self.cs_pin, GPIO.OUT) - GPIO.setup(self.clock_pin, GPIO.OUT) - GPIO.setup(self.data_pin, GPIO.IN) - - # Pull chip select high to make chip inactive - GPIO.output(self.cs_pin, GPIO.HIGH) - - def get(self): - '''Reads SPI bus and returns current value of thermocouple.''' - self.read() - self.checkErrors() - #return getattr(self, "to_" + self.units)(self.data_to_tc_temperature()) - return getattr(self, "to_" + self.units)(self.data_to_LinearizedTempC()) - - def get_rj(self): - '''Reads SPI bus and returns current value of reference junction.''' - self.read() - return getattr(self, "to_" + self.units)(self.data_to_rj_temperature()) - - def read(self): - '''Reads 32 bits of the SPI bus & stores as an integer in self.data.''' - bytesin = 0 - # Select the chip - GPIO.output(self.cs_pin, GPIO.LOW) - # Read in 32 bits - for i in range(32): - GPIO.output(self.clock_pin, GPIO.LOW) - bytesin = bytesin << 1 - if (GPIO.input(self.data_pin)): - bytesin = bytesin | 1 - GPIO.output(self.clock_pin, GPIO.HIGH) - # Unselect the chip - GPIO.output(self.cs_pin, GPIO.HIGH) - # Save data - self.data = bytesin - - def checkErrors(self, data_32 = None): - '''Checks error bits to see if there are any SCV, SCG, or OC faults''' - if data_32 is None: - data_32 = self.data - anyErrors = (data_32 & 0x10000) != 0 # Fault bit, D16 - if anyErrors: - self.noConnection = (data_32 & 0x00000001) != 0 # OC bit, D0 - self.shortToGround = (data_32 & 0x00000002) != 0 # SCG bit, D1 - self.shortToVCC = (data_32 & 0x00000004) != 0 # SCV bit, D2 - self.unknownError = not (self.noConnection | self.shortToGround | self.shortToVCC) # Errk! - else: - self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False - - def data_to_tc_temperature(self, data_32 = None): - '''Takes an integer and returns a thermocouple temperature in celsius.''' - if data_32 is None: - data_32 = self.data - tc_data = ((data_32 >> 18) & 0x3FFF) - return self.convert_tc_data(tc_data) - - def data_to_rj_temperature(self, data_32 = None): - '''Takes an integer and returns a reference junction temperature in celsius.''' - if data_32 is None: - data_32 = self.data - rj_data = ((data_32 >> 4) & 0xFFF) - return self.convert_rj_data(rj_data) - - def convert_tc_data(self, tc_data): - '''Convert thermocouple data to a useful number (celsius).''' - if tc_data & 0x2000: - # two's compliment - without_resolution = ~tc_data & 0x1FFF - without_resolution += 1 - without_resolution *= -1 - else: - without_resolution = tc_data & 0x1FFF - return without_resolution * 0.25 - - def convert_rj_data(self, rj_data): - '''Convert reference junction data to a useful number (celsius).''' - if rj_data & 0x800: - without_resolution = ~rj_data & 0x7FF - without_resolution += 1 - without_resolution *= -1 - else: - without_resolution = rj_data & 0x7FF - return without_resolution * 0.0625 - - def to_c(self, celsius): - '''Celsius passthrough for generic to_* method.''' - return celsius - - def to_k(self, celsius): - '''Convert celsius to kelvin.''' - return celsius + 273.15 - - def to_f(self, celsius): - '''Convert celsius to fahrenheit.''' - return celsius * 9.0/5.0 + 32 - - def cleanup(self): - '''Selective GPIO cleanup''' - GPIO.setup(self.cs_pin, GPIO.IN) - GPIO.setup(self.clock_pin, GPIO.IN) - - def data_to_LinearizedTempC(self, data_32 = None): - '''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 -''' - if data_32 is None: - data_32 = self.data - # extract TC temp - # Check if signed bit is set. - if data_32 & 0x80000000: - # Negative value, take 2's compliment. Compute this with subtraction - # because python is a little odd about handling signed/unsigned. - data_32 >>= 18 - data_32 -= 16384 - else: - # Positive value, just shift the bits to get the value. - data_32 >>= 18 - # Scale by 0.25 degrees C per bit and return value. - TC_temp = data_32 * 0.25 - # Extract Internal Temp - data_32 = self.data - # Ignore bottom 4 bits of thermocouple data. - data_32 >>= 4 - # Grab bottom 11 bits as internal temperature data. - Internal_Temp= data_32 & 0x7FF - if data_32 & 0x800: - # Negative value, take 2's compliment. Compute this with subtraction - # because python is a little odd about handling signed/unsigned. - Internal_Temp -= 4096 - # Scale by 0.0625 degrees C per bit and return value. - Internal_Temp = Internal_Temp * 0.0625 - - # MAX31855 thermocouple voltage reading in mV - thermocoupleVoltage = (TC_temp - Internal_Temp) * 0.041276 - # MAX31855 cold junction voltage reading in mV - coldJunctionTemperature = Internal_Temp - coldJunctionVoltage = (-0.176004136860E-01 + - 0.389212049750E-01 * coldJunctionTemperature + - 0.185587700320E-04 * math.pow(coldJunctionTemperature, 2.0) + - -0.994575928740E-07 * math.pow(coldJunctionTemperature, 3.0) + - 0.318409457190E-09 * math.pow(coldJunctionTemperature, 4.0) + - -0.560728448890E-12 * math.pow(coldJunctionTemperature, 5.0) + - 0.560750590590E-15 * math.pow(coldJunctionTemperature, 6.0) + - -0.320207200030E-18 * math.pow(coldJunctionTemperature, 7.0) + - 0.971511471520E-22 * math.pow(coldJunctionTemperature, 8.0) + - -0.121047212750E-25 * math.pow(coldJunctionTemperature, 9.0) + - 0.118597600000E+00 * math.exp(-0.118343200000E-03 * math.pow((coldJunctionTemperature-0.126968600000E+03), 2.0))) - # cold junction voltage + thermocouple voltage - voltageSum = thermocoupleVoltage + coldJunctionVoltage - # calculate corrected temperature reading based on coefficients for 3 different ranges - # float b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10; - if voltageSum < 0: - b0 = 0.0000000E+00 - b1 = 2.5173462E+01 - b2 = -1.1662878E+00 - b3 = -1.0833638E+00 - b4 = -8.9773540E-01 - b5 = -3.7342377E-01 - b6 = -8.6632643E-02 - b7 = -1.0450598E-02 - b8 = -5.1920577E-04 - b9 = 0.0000000E+00 - elif voltageSum < 20.644: - b0 = 0.000000E+00 - b1 = 2.508355E+01 - b2 = 7.860106E-02 - b3 = -2.503131E-01 - b4 = 8.315270E-02 - b5 = -1.228034E-02 - b6 = 9.804036E-04 - b7 = -4.413030E-05 - b8 = 1.057734E-06 - b9 = -1.052755E-08 - elif voltageSum < 54.886: - b0 = -1.318058E+02 - b1 = 4.830222E+01 - b2 = -1.646031E+00 - b3 = 5.464731E-02 - b4 = -9.650715E-04 - b5 = 8.802193E-06 - b6 = -3.110810E-08 - b7 = 0.000000E+00 - b8 = 0.000000E+00 - b9 = 0.000000E+00 - else: - # TODO: handle error - out of range - return 0 - return (b0 + - b1 * voltageSum + - b2 * pow(voltageSum, 2.0) + - b3 * pow(voltageSum, 3.0) + - b4 * pow(voltageSum, 4.0) + - b5 * pow(voltageSum, 5.0) + - b6 * pow(voltageSum, 6.0) + - b7 * pow(voltageSum, 7.0) + - b8 * pow(voltageSum, 8.0) + - b9 * pow(voltageSum, 9.0)) - - -class MAX31855Error(Exception): - def __init__(self, value): - self.value = value - def __str__(self): - return repr(self.value) - -if __name__ == "__main__": - - # Multi-chip example - import time - cs_pins = [4, 17, 18, 24] - clock_pin = 23 - data_pin = 22 - units = "f" - thermocouples = [] - for cs_pin in cs_pins: - thermocouples.append(MAX31855(cs_pin, clock_pin, data_pin, units)) - running = True - while(running): - try: - for thermocouple in thermocouples: - rj = thermocouple.get_rj() - try: - tc = thermocouple.get() - except MAX31855Error as e: - tc = "Error: "+ e.value - running = False - print("tc: {} and rj: {}".format(tc, rj)) - time.sleep(1) - except KeyboardInterrupt: - running = False - for thermocouple in thermocouples: - thermocouple.cleanup() diff --git a/lib/max31855spi.py b/lib/max31855spi.py deleted file mode 100644 index b3eb362..0000000 --- a/lib/max31855spi.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/python -import logging - -from Adafruit_MAX31855 import MAX31855 - -class MAX31855SPI(object): - '''Python driver for [MAX38155 Cold-Junction Compensated Thermocouple-to-Digital Converter](http://www.maximintegrated.com/datasheet/index.mvp/id/7273) - Requires: - - adafruit's MAX31855 SPI-only device library - - ''' - def __init__(self, spi_dev): - self.max31855 = MAX31855.MAX31855(spi=spi_dev) - self.log = logging.getLogger(__name__) - - def get(self): - '''Reads SPI bus and returns current value of thermocouple.''' - state = self.max31855.readState() - self.log.debug("status %s" % state) - if state['openCircuit']: - raise MAX31855Error('Not Connected') - elif state['shortGND']: - raise MAX31855Error('Short to Ground') - elif state['shortVCC']: - raise MAX31855Error('Short to VCC') - elif state['fault']: - raise MAX31855Error('Unknown Error') - return self.max31855.readLinearizedTempC() - - -class MAX31855SPIError(Exception): - def __init__(self, value): - self.value = value - - def __str__(self): - return repr(self.value) diff --git a/lib/max31856.py b/lib/max31856.py deleted file mode 100644 index 4a3841b..0000000 --- a/lib/max31856.py +++ /dev/null @@ -1,341 +0,0 @@ -""" -max31856.py - -Class which defines interaction with the MAX31856 sensor. - -Copyright (c) 2019 John Robinson -Author: John Robinson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -import logging -import warnings - -import Adafruit_GPIO as Adafruit_GPIO -import Adafruit_GPIO.SPI as SPI - - -class MAX31856(object): - """Class to represent an Adafruit MAX31856 thermocouple temperature - measurement board. - """ - - # Board Specific Constants - MAX31856_CONST_THERM_LSB = 2**-7 - MAX31856_CONST_THERM_BITS = 19 - MAX31856_CONST_CJ_LSB = 2**-6 - MAX31856_CONST_CJ_BITS = 14 - - ### Register constants, see data sheet Table 6 (in Rev. 0) for info. - # Read Addresses - MAX31856_REG_READ_CR0 = 0x00 - MAX31856_REG_READ_CR1 = 0x01 - MAX31856_REG_READ_MASK = 0x02 - MAX31856_REG_READ_CJHF = 0x03 - MAX31856_REG_READ_CJLF = 0x04 - MAX31856_REG_READ_LTHFTH = 0x05 - MAX31856_REG_READ_LTHFTL = 0x06 - MAX31856_REG_READ_LTLFTH = 0x07 - MAX31856_REG_READ_LTLFTL = 0x08 - MAX31856_REG_READ_CJTO = 0x09 - MAX31856_REG_READ_CJTH = 0x0A # Cold-Junction Temperature Register, MSB - MAX31856_REG_READ_CJTL = 0x0B # Cold-Junction Temperature Register, LSB - MAX31856_REG_READ_LTCBH = 0x0C # Linearized TC Temperature, Byte 2 - MAX31856_REG_READ_LTCBM = 0x0D # Linearized TC Temperature, Byte 1 - MAX31856_REG_READ_LTCBL = 0x0E # Linearized TC Temperature, Byte 0 - MAX31856_REG_READ_FAULT = 0x0F # Fault status register - - # Write Addresses - MAX31856_REG_WRITE_CR0 = 0x80 - MAX31856_REG_WRITE_CR1 = 0x81 - MAX31856_REG_WRITE_MASK = 0x82 - MAX31856_REG_WRITE_CJHF = 0x83 - MAX31856_REG_WRITE_CJLF = 0x84 - MAX31856_REG_WRITE_LTHFTH = 0x85 - MAX31856_REG_WRITE_LTHFTL = 0x86 - MAX31856_REG_WRITE_LTLFTH = 0x87 - MAX31856_REG_WRITE_LTLFTL = 0x88 - MAX31856_REG_WRITE_CJTO = 0x89 - MAX31856_REG_WRITE_CJTH = 0x8A # Cold-Junction Temperature Register, MSB - MAX31856_REG_WRITE_CJTL = 0x8B # Cold-Junction Temperature Register, LSB - - # Pre-config Register Options - MAX31856_CR0_READ_ONE = 0x40 # One shot reading, delay approx. 200ms then read temp registers - MAX31856_CR0_READ_CONT = 0x80 # Continuous reading, delay approx. 100ms between readings - - # Thermocouple Types - MAX31856_B_TYPE = 0x0 # Read B Type Thermocouple - MAX31856_E_TYPE = 0x1 # Read E Type Thermocouple - MAX31856_J_TYPE = 0x2 # Read J Type Thermocouple - MAX31856_K_TYPE = 0x3 # Read K Type Thermocouple - MAX31856_N_TYPE = 0x4 # Read N Type Thermocouple - MAX31856_R_TYPE = 0x5 # Read R Type Thermocouple - MAX31856_S_TYPE = 0x6 # Read S Type Thermocouple - MAX31856_T_TYPE = 0x7 # Read T Type Thermocouple - - def __init__(self, tc_type=MAX31856_S_TYPE, units="c", avgsel=0x0, ac_freq_50hz=False, ocdetect=0x1, software_spi=None, hardware_spi=None, gpio=None): - """ - Initialize MAX31856 device with software SPI on the specified CLK, - CS, and DO pins. Alternatively can specify hardware SPI by sending an - SPI.SpiDev device in the spi parameter. - - Args: - tc_type (1-byte Hex): Type of Thermocouple. Choose from class variables of the form - 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 - do (integer): Pin number for software SPI MISO - di (integer): Pin number for software SPI MOSI - hardware_spi (SPI.SpiDev): If using hardware SPI, define the connection - """ - self._logger = logging.getLogger('Adafruit_MAX31856.MAX31856') - self._spi = None - 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') - self._spi = hardware_spi - elif software_spi is not None: - self._logger.debug('Using software SPI') - # Default to platform GPIO if not provided. - if gpio is None: - gpio = Adafruit_GPIO.get_platform_gpio() - self._spi = SPI.BitBang(gpio, software_spi['clk'], software_spi['di'], - software_spi['do'], software_spi['cs']) - else: - raise ValueError( - 'Must specify either spi for for hardware SPI or clk, cs, and do for softwrare SPI!') - self._spi.set_clock_hz(5000000) - # According to Wikipedia (on SPI) and MAX31856 Datasheet: - # SPI mode 1 corresponds with correct timing, CPOL = 0, CPHA = 1 - self._spi.set_mode(1) - self._spi.set_bit_order(SPI.MSBFIRST) - - self.cr0 = self.MAX31856_CR0_READ_CONT | ((ocdetect & 3) << 4) | (1 if ac_freq_50hz else 0) - self.cr1 = (((self.avgsel & 7) << 4) + (self.tc_type & 0x0f)) - - # Setup for reading continuously with T-Type thermocouple - 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): - """ - Takes in the msb and lsb from a Cold Junction (CJ) temperature reading and converts it - into a decimal value. - - This function was removed from readInternalTempC() and moved to its own method to allow for - easier testing with standard values. - - Args: - msb (hex): Most significant byte of CJ temperature - lsb (hex): Least significant byte of a CJ temperature - - """ - # (((msb w/o +/-) shifted by number of 1 byte above lsb) - # + val_low_byte) - # >> shifted back by # of dead bits - temp_bytes = (((msb & 0x7F) << 8) + lsb) >> 2 - - if msb & 0x80: - # Negative Value. Scale back by number of bits - temp_bytes -= 2**(MAX31856.MAX31856_CONST_CJ_BITS -1) - - # temp_bytes*value of lsb - temp_c = temp_bytes*MAX31856.MAX31856_CONST_CJ_LSB - - return temp_c - - @staticmethod - def _thermocouple_temp_from_bytes(byte0, byte1, byte2): - """ - Converts the thermocouple byte values to a decimal value. - - This function was removed from readInternalTempC() and moved to its own method to allow for - easier testing with standard values. - - Args: - byte2 (hex): Most significant byte of thermocouple temperature - byte1 (hex): Middle byte of thermocouple temperature - byte0 (hex): Least significant byte of a thermocouple temperature - - Returns: - temp_c (float): Temperature in degrees celsius - """ - # (((val_high_byte w/o +/-) shifted by 2 bytes above LSB) - # + (val_mid_byte shifted by number 1 byte above LSB) - # + val_low_byte ) - # >> back shift by number of dead bits - temp_bytes = (((byte2 & 0x7F) << 16) + (byte1 << 8) + byte0) - temp_bytes = temp_bytes >> 5 - - if byte2 & 0x80: - temp_bytes -= 2**(MAX31856.MAX31856_CONST_THERM_BITS -1) - - # temp_bytes*value of LSB - temp_c = temp_bytes*MAX31856.MAX31856_CONST_THERM_LSB - - return temp_c - - def read_internal_temp_c(self): - """ - Return internal temperature value in degrees celsius. - """ - val_low_byte = self._read_register(self.MAX31856_REG_READ_CJTL) - val_high_byte = self._read_register(self.MAX31856_REG_READ_CJTH) - - temp_c = MAX31856._cj_temp_from_bytes(val_high_byte, val_low_byte) - self._logger.debug("Cold Junction Temperature {0} deg. C".format(temp_c)) - - return temp_c - - def read_temp_c(self): - """ - Return the thermocouple temperature value in degrees celsius. - """ - val_low_byte = self._read_register(self.MAX31856_REG_READ_LTCBL) - val_mid_byte = self._read_register(self.MAX31856_REG_READ_LTCBM) - val_high_byte = self._read_register(self.MAX31856_REG_READ_LTCBH) - - temp_c = MAX31856._thermocouple_temp_from_bytes(val_low_byte, val_mid_byte, val_high_byte) - - self._logger.debug("Thermocouple Temperature {0} deg. C".format(temp_c)) - - return temp_c - - def read_fault_register(self): - """Return bytes containing fault codes and hardware problems. - - TODO: Could update in the future to return human readable values - """ - reg = self._read_register(self.MAX31856_REG_READ_FAULT) - return reg - - def _read_register(self, address): - """ - Reads a register at address from the MAX31856 - - Args: - address (8-bit Hex): Address for read register. Format 0Xh. Constants listed in class - as MAX31856_REG_READ_* - - Note: - SPI transfer method is used. The address is written in as the first byte, and then a - dummy value as the second byte. The data from the sensor is contained in the second - byte, the dummy byte is only used to keep the SPI clock ticking as we read in the - value. The first returned byte is discarded because no data is transmitted while - specifying the register address. - """ - raw = self._spi.transfer([address, 0x00]) - if raw is None or len(raw) != 2: - raise RuntimeError('Did not read expected number of bytes from device!') - - value = raw[1] - self._logger.debug('Read Register: 0x{0:02X}, Raw Value: 0x{1:02X}'.format( - (address & 0xFFFF), (value & 0xFFFF))) - return value - - def _write_register(self, address, write_value): - """ - Writes to a register at address from the MAX31856 - - Args: - address (8-bit Hex): Address for read register. Format 0Xh. Constants listed in class - as MAX31856_REG_WRITE_* - write_value (8-bit Hex): Value to write to the register - """ - self._spi.transfer([address, write_value]) - self._logger.debug('Wrote Register: 0x{0:02X}, Value 0x{1:02X}'.format((address & 0xFF), - (write_value & 0xFF))) - - # If we've gotten this far without an exception, the transmission must've gone through - return True - - # Deprecated Methods - def readTempC(self): #pylint: disable-msg=invalid-name - """Depreciated due to Python naming convention, use read_temp_c instead - """ - warnings.warn("Depreciated due to Python naming convention, use read_temp_c() instead", DeprecationWarning) - return read_temp_c(self) - - def readInternalTempC(self): #pylint: disable-msg=invalid-name - """Depreciated due to Python naming convention, use read_internal_temp_c instead - """ - warnings.warn("Depreciated due to Python naming convention, use read_internal_temp_c() instead", DeprecationWarning) - return read_internal_temp_c(self) - - # added by jbruce to mimic MAX31855 lib - def to_c(self, celsius): - '''Celsius passthrough for generic to_* method.''' - return celsius - - def to_k(self, celsius): - '''Convert celsius to kelvin.''' - return celsius + 273.15 - - def to_f(self, celsius): - '''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 540f44e..7c74417 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -1,6 +1,5 @@ import threading import time -import random import datetime import logging import json @@ -30,8 +29,12 @@ class Duplogger(): duplog = Duplogger().logref() - class Output(object): + '''This represents a GPIO output that controls a solid + state relay to turn the kiln elements on and off. + inputs + config.gpio_heat + ''' def __init__(self): self.active = False self.heater = digitalio.DigitalInOut(config.gpio_heat) @@ -48,11 +51,18 @@ class Output(object): # wrapper for blinka board class Board(object): + '''This represents a blinka board where this code + runs. + ''' def __init__(self): log.info("board: %s" % (self.name)) self.temp_sensor.start() class RealBoard(Board): + '''Each board has a thermocouple board attached to it. + Any blinka board that supports SPI can be used. The + board is automatically detected by blinka. + ''' def __init__(self): self.name = None self.load_libs() @@ -70,12 +80,18 @@ class RealBoard(Board): return Max31856() class SimulatedBoard(Board): + '''Simulated board used during simulations. + See config.simulate + ''' def __init__(self): self.name = "simulated" self.temp_sensor = TempSensorSimulated() Board.__init__(self) class TempSensor(threading.Thread): + '''Used by the Board class. Each Board must have + a TempSensor. + ''' def __init__(self): threading.Thread.__init__(self) self.daemon = True @@ -83,7 +99,7 @@ class TempSensor(threading.Thread): self.status = ThermocoupleTracker() class TempSensorSimulated(TempSensor): - '''not much here, just need to be able to set the temperature''' + '''Simulates a temperature sensor ''' def __init__(self): TempSensor.__init__(self) self.simulated_temperature = 0 @@ -91,8 +107,11 @@ class TempSensorSimulated(TempSensor): return self.simulated_temperature class TempSensorReal(TempSensor): - '''real temperature sensor thread that takes N measurements - during the time_step''' + '''real temperature sensor that takes many measurements + during the time_step + inputs + config.temperature_average_samples + ''' def __init__(self): TempSensor.__init__(self) self.sleeptime = self.time_step / float(config.temperature_average_samples) @@ -102,35 +121,20 @@ class TempSensorReal(TempSensor): def get_temperature(self): '''read temp from tc and convert if needed''' - temp = self.raw_temp() # raw_temp provided by subclasses - if config.temp_scale.lower() == "f": - temp = (temp*9/5)+32 - return temp - - #def get_temperature(self): - # try: - # temp = self.raw_temp - # if config.temp_scale.lower() == "f": - # temp = (temp*9/5)+32 - # log.info("temp = %0.2f" % temp) - # return temp - # except RuntimeError as rte: - # if rte.args and rte.args[0] == "thermocouple not connected": - # self.bad_count = self.bad_count + 1 - # if rte.args and rte.args[0] == "short circuit to ground": - # if not config.ignore_tc_short_errors: - # self.bad_count = self.bad_count + 1 - # if rte.args and rte.args[0] == "short circuit to power": - # if not config.ignore_tc_short_errors: - # self.bad_count = self.bad_count + 1 - # if rte.args and rte.args[0] == "faulty reading": - # self.bad_count = self.bad_count + 1 - # - # log.error("Problem reading temp %s" % (rte.args[0])) - # # fix still need to include max-31856 errors by calling fault - # # and checking a dict of possible faults. what a shitty way to handle - # # errors. - # return None + try: + temp = self.raw_temp() # raw_temp provided by subclasses + if config.temp_scale.lower() == "f": + temp = (temp*9/5)+32 + self.status.good() + return temp + except ThermocoupleError as tce: + if tce.ignore: + log.error("Problem reading temp (ignored) %s" % (tce.message)) + self.status.good() + else: + log.error("Problem reading temp %s" % (tce.message)) + self.status.bad() + return None def temperature(self): '''average temp over a duty cycle''' @@ -138,20 +142,14 @@ class TempSensorReal(TempSensor): def run(self): while True: - # not at all sure this try/except should be here - # might be better getting the temp - try: - temp = self.get_temperature() - self.status.good() + temp = self.get_temperature() + if temp: self.temptracker.add(temp) - except RuntimeError as rte: - self.status.bad() - log.error("Problem reading temp %s" % (rte.args[0])) - time.sleep(self.sleeptime) class TempTracker(object): - '''creates a sliding window of temperatures + '''creates a sliding window of N temperatures per + config.sensor_time_wait ''' def __init__(self): self.size = config.temperature_average_samples @@ -204,8 +202,6 @@ class ThermocoupleTracker(object): class Max31855(TempSensorReal): '''each subclass expected to handle errors and get temperature''' - # FIX I need unified errors from these classes since the underlying - # implementations are different def __init__(self): TempSensorReal.__init__(self) log.info("thermocouple MAX31855") @@ -213,7 +209,83 @@ class Max31855(TempSensorReal): self.thermocouple = adafruit_max31855.MAX31855(self.spi, self.cs) def raw_temp(self): - return self.thermocouple.temperature_NIST + try: + return self.thermocouple.temperature_NIST + except RuntimeError as rte: + if rte.args and rte.args[0]: + raise Max31855_Error(rte.args[0]) + raise Max31855_Error('unknown') + +class ThermocoupleError(Exception): + ''' + thermocouple exception parent class to handle mapping of error messages + and make them consistent across adafruit libraries. Also set whether + each exception should be ignored based on settings in config.py. + ''' + def __init__(self, message): + self.ignore = False + self.message = message + self.map_message() + self.set_ignore() + super().__init__(self.message) + + def set_ignore(self): + if self.message == "not connected" and config.ignore_tc_lost_connection == True: + self.ignore = True + if self.message == "short circuit" and config.ignore_tc_short_errors == True: + self.ignore = True + if self.message == "unknown" and config.ignore_tc_unknown_error == True: + self.ignore = True + if self.message == "cold junction range fault" and config.ignore_tc_cold_junction_range_error == True: + self.ignore = True + if self.message == "thermocouple range fault" and config.ignore_tc_range_error == True: + self.ignore = True + if self.message == "cold junction temp too high" and config.ignore_tc_cold_junction_temp_high == True: + self.ignore = True + if self.message == "cold junction temp too low" and config.ignore_tc_cold_junction_temp_low == True: + self.ignore = True + if self.message == "thermocouple temp too high" and config.ignore_tc_temp_high == True: + self.ignore = True + if self.message == "thermocouple temp too low" and config.ignore_tc_temp_low == True: + self.ignore = True + if self.message == "voltage too high or low" and config.ignore_tc_voltage_error == True: + self.ignore = True + + def map_message(self): + try: + self.message = self.map[self.orig_message] + except KeyError: + self.message = "unknown" + +class Max31855_Error(ThermocoupleError): + ''' + All children must set self.orig_message and self.map + ''' + def __init__(self, message): + self.orig_message = message + # this purposefully makes "fault reading" and + # "Total thermoelectric voltage out of range..." unknown errors + self.map = { + "thermocouple not connected" : "not connected", + "short circuit to ground" : "short circuit", + "short circuit to power" : "short circuit", + } + super().__init__(self.message) + +class Max31856_Error(ThermocoupleError): + def __init__(self, message): + self.orig_message = message + self.map = { + "cj_range" : "cold junction range fault", + "tc_range" : "thermocouple range fault", + "cj_high" : "cold junction temp too high", + "cj_low" : "cold junction temp too low", + "tc_high" : "thermocouple temp too high", + "tc_low" : "thermocouple temp too low", + "voltage" : "voltage too high or low", + "open_tc" : "not connected" + } + super().__init__(self.message) class Max31856(TempSensorReal): '''each subclass expected to handle errors and get temperature''' @@ -229,7 +301,16 @@ class Max31856(TempSensorReal): self.thermocouple.noise_rejection(60) def raw_temp(self): - return self.thermocouple.temperature + # The underlying adafruit library does not throw exceptions + # for thermocouple errors. Instead, they are stored in + # dict named self.thermocouple.fault. Here we check that + # dict for errors and raise an exception. + # and raise Max31856_Error(message) + temp = self.thermocouple.temperature + for k,v in self.thermocouple.fault: + if v: + raise Max31856_Error(k) + return temp class Oven(threading.Thread): '''parent oven class. this has all the common code @@ -254,21 +335,6 @@ class Oven(threading.Thread): def run_profile(self, profile, startat=0): self.reset() - - # FIX, these need to be moved - #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 - self.startat = startat * 60 self.runtime = self.startat self.start_time = datetime.datetime.now() - datetime.timedelta(seconds=self.startat) @@ -310,27 +376,16 @@ class Oven(threading.Thread): def reset_if_emergency(self): '''reset if the temperature is way TOO HOT, or other critical errors detected''' - # FIX - need to fix this whole thing... - #if (self.board.temp_sensor.temperature() + config.thermocouple_offset >= - # config.emergency_shutoff_temp): - # log.info("emergency!!! temperature too high") - # if config.ignore_temp_too_high == False: - # self.abort_run() - - #if self.board.temp_sensor.noConnection: - # log.info("emergency!!! lost connection to thermocouple") - # if config.ignore_lost_connection_tc == False: - # self.abort_run() - - #if self.board.temp_sensor.unknownError: - # log.info("emergency!!! unknown thermocouple error") - # if config.ignore_unknown_tc_error == False: - # self.abort_run() - - #if self.board.temp_sensor.status.over_error_limit(): - # log.info("emergency!!! too many errors in a short period") - # if config.ignore_too_many_tc_errors == False: - # self.abort_run() + if (self.board.temp_sensor.temperature() + config.thermocouple_offset >= + config.emergency_shutoff_temp): + log.info("emergency!!! temperature too high") + if config.ignore_temp_too_high == False: + self.abort_run() + + if self.board.temp_sensor.status.over_error_limit(): + log.info("emergency!!! too many errors in a short period") + if config.ignore_too_many_tc_errors == False: + self.abort_run() def reset_if_schedule_ended(self): if self.runtime > self.totaltime: diff --git a/requirements.txt b/requirements.txt index fbf159f..f3808e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,9 +6,12 @@ gevent-websocket websocket-client -#RPi.GPIO -#Adafruit-MAX31855 -#Adafruit-GPIO - -adafruit-circuitpython-max31856 +# List of all supported adafruit modules for thermocouples adafruit-circuitpython-max31855 +adafruit-circuitpython-max31856 + +# untested - for PT100 platinum thermocouples +#adafruit-circuitpython-max31865 + +# untested - for mcp9600 and mcp9601 +#adafruit-circuitpython-mcp9600