commit
afcb7d5abf
13
config.py
13
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
|
||||
|
||||
@ -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
|
||||
'''
|
||||
|
||||
@ -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()
|
||||
|
||||
107
lib/oven.py
107
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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user