- complete rewrite of oven class

- simulation more accurate
- added kiln_must_catch_up functionality
- added max31856 support
pull/15/head
Jason Bruce 2021-03-14 15:16:03 -04:00
rodzic 5bd8c28388
commit c5b7f21044
8 zmienionych plików z 613 dodań i 378 usunięć

Wyświetl plik

@ -1,4 +1,5 @@
import logging
from lib.max31856 import MAX31856
########################################################################
#
@ -13,7 +14,7 @@ listening_ip = "0.0.0.0"
listening_port = 8081
### Cost Estimate
kwh_rate = 0.18 # Rate in currency_type to calculate cost to run job
kwh_rate = 0.1319 # Rate in currency_type to calculate cost to run job
currency_type = "$" # Currency Symbol to show when calculating cost to run job
########################################################################
@ -27,35 +28,30 @@ currency_type = "$" # Currency Symbol to show when calculating cost to run j
### Outputs
gpio_heat = 23 # Switches zero-cross solid-state-relay
heater_invert = 0 # switches the polarity of the heater control
### Thermocouple Adapter selection:
# max31855 - bitbang SPI interface
# max31855spi - kernel SPI interface
# max6675 - bitbang SPI interface
# max31856 - bitbang SPI interface. must specify thermocouple_type.
max31855 = 1
max6675 = 0
max31855spi = 0 # if you use this one, you MUST reassign the default GPIO pins
max31856 = 0
# see lib/max31856.py for other thermocouple_type, only applies to max31856
thermocouple_type = MAX31856.MAX31856_S_TYPE
### Thermocouple Connection (using bitbang interfaces)
gpio_sensor_cs = 27
gpio_sensor_clock = 22
gpio_sensor_data = 17
### Thermocouple SPI Connection (using adafrut drivers + kernel SPI interface)
spi_sensor_chip_id = 0
### duty cycle of the entire system in seconds. Every N seconds a decision
### is made about switching the relay[s] on & off and for how long.
### The thermocouple is read five times during this period and the highest
### value is used.
sensor_time_wait = 2
sensor_time_wait = 1
########################################################################
#
# PID parameters
pid_kp = 25 # Proportional
pid_ki = 1088 # Integration
pid_kd = 217 # Derivative was 217
@ -64,7 +60,7 @@ pid_kd = 217 # Derivative was 217
########################################################################
#
# Simulation parameters
simulate = True
sim_t_env = 25.0 # deg C
sim_c_heat = 100.0 # J/K heat capacity of heat element
sim_c_oven = 5000.0 # J/K heat capacity of oven
@ -87,15 +83,14 @@ time_scale_profile = "m" # s = Seconds | m = Minutes | h = Hours - Enter and vi
# when solid state relays fail, they usually fail closed. 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 = 2250
emergency_shutoff_temp = 2264 #cone 7
# not used yet
# if measured value is N degrees below set point
warning_temp_low = 5
# not used yet
# if measured value is N degrees above set point
warning_temp_high = 5
# If the kiln cannot heat 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
# wanted temperature, the schedule will run forever.
kiln_must_catch_up = True
kiln_must_catch_up_max_error = 10 #degrees
# thermocouple offset
# If you put your thermocouple in ice water and it reads 36F, you can

Wyświetl plik

@ -25,17 +25,23 @@ except:
logging.basicConfig(level=config.log_level, format=config.log_format)
log = logging.getLogger("kiln-controller")
log.info("Starting kill controller")
log.info("Starting kiln controller")
script_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, script_dir + '/lib/')
profile_path = os.path.join(script_dir, "storage", "profiles")
from oven import Oven, Profile
from oven import SimulatedOven, RealOven, Profile
from ovenWatcher import OvenWatcher
app = bottle.Bottle()
oven = Oven()
if config.simulate == True:
log.info("this is a simulation")
oven = SimulatedOven()
else:
log.info("this is a real kiln")
oven = RealOven()
ovenWatcher = OvenWatcher(oven)
@app.route('/')

0
lib/__init__.py 100644
Wyświetl plik

304
lib/max31856.py 100644
Wyświetl plik

@ -0,0 +1,304 @@
"""
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, 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.
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
# 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.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_CR1, self.cr1)
@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 get(self):
celcius = self.read_temp_c()
return getattr(self, "to_" + self.units)(celcius)

Wyświetl plik

@ -1,125 +0,0 @@
#!/usr/bin/python
import RPi.GPIO as GPIO
import time
class MAX6675(object):
'''Python driver for [MAX6675 Cold-Junction Compensated Thermocouple-to-Digital Converter](http://www.adafruit.com/datasheets/MAX6675.pdf)
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
# 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())
def read(self):
'''Reads 16 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 16 bits
for i in range(16):
GPIO.output(self.clock_pin, GPIO.LOW)
time.sleep(0.001)
bytesin = bytesin << 1
if (GPIO.input(self.data_pin)):
bytesin = bytesin | 1
GPIO.output(self.clock_pin, GPIO.HIGH)
time.sleep(0.001)
# Unselect the chip
GPIO.output(self.cs_pin, GPIO.HIGH)
# Save data
self.data = bytesin
def checkErrors(self, data_16 = None):
'''Checks errors on bit D2'''
if data_16 is None:
data_16 = self.data
noConnection = (data_16 & 0x4) != 0 # tc input bit, D2
if noConnection:
raise MAX6675Error("No Connection") # open thermocouple
def data_to_tc_temperature(self, data_16 = None):
'''Takes an integer and returns a thermocouple temperature in celsius.'''
if data_16 is None:
data_16 = self.data
# Remove bits D0-3
tc_data = ((data_16 >> 3) & 0xFFF)
# 12-bit resolution
return (tc_data * 0.25)
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)
class MAX6675Error(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
if __name__ == "__main__":
# default example
cs_pin = 24
clock_pin = 23
data_pin = 22
units = "c"
thermocouple = MAX6675(cs_pin, clock_pin, data_pin, units)
running = True
while(running):
try:
try:
tc = thermocouple.get()
except MAX6675Error as e:
tc = "Error: "+ e.value
running = False
print("tc: {}".format(tc))
time.sleep(1)
except KeyboardInterrupt:
running = False
thermocouple.cleanup()

Wyświetl plik

@ -4,213 +4,95 @@ import random
import datetime
import logging
import json
import config
log = logging.getLogger(__name__)
try:
if config.max31855 + config.max6675 + config.max31855spi > 1:
log.error("choose (only) one converter IC")
exit()
if config.max31855:
from max31855 import MAX31855, MAX31855Error
log.info("import MAX31855")
if config.max31855spi:
import Adafruit_GPIO.SPI as SPI
from max31855spi import MAX31855SPI, MAX31855SPIError
log.info("import MAX31855SPI")
spi_reserved_gpio = [7, 8, 9, 10, 11]
if config.gpio_heat in spi_reserved_gpio:
raise Exception("gpio_heat pin %s collides with SPI pins %s" % (config.gpio_heat, spi_reserved_gpio))
if config.max6675:
from max6675 import MAX6675, MAX6675Error
log.info("import MAX6675")
sensor_available = True
except ImportError:
log.exception("Could not initialize temperature sensor, using dummy values!")
sensor_available = False
class Output(object):
def __init__(self):
self.active = False
self.load_libs()
try:
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(config.gpio_heat, GPIO.OUT)
# GPIO.setup(config.gpio_cool, GPIO.OUT)
# GPIO.setup(config.gpio_air, GPIO.OUT)
# GPIO.setup(config.gpio_door, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def load_libs(self):
try:
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(config.gpio_heat, GPIO.OUT)
self.active = True
except:
msg = "Could not initialize GPIOs, oven operation will only be simulated!"
log.warning(msg)
self.active = False
gpio_available = True
except ImportError:
msg = "Could not initialize GPIOs, oven operation will only be simulated!"
log.warning(msg)
gpio_available = False
def heat(self,time):
GPIO.output(config.gpio_heat, GPIO.HIGH)
time.sleep(time)
GPIO.output(config.gpio_heat, GPIO.LOW)
def cool(self,time):
'''no active cooling, so sleep'''
time.sleep(time)
class Oven (threading.Thread):
STATE_IDLE = "IDLE"
STATE_RUNNING = "RUNNING"
def __init__(self, simulate=False, time_step=config.sensor_time_wait):
threading.Thread.__init__(self)
self.daemon = True
self.simulate = simulate
self.time_step = time_step
self.reset()
if simulate:
self.temp_sensor = TempSensorSimulate(self,
self.time_step,
self.time_step)
if sensor_available:
self.temp_sensor = TempSensorReal(self.time_step)
else:
self.temp_sensor = TempSensorSimulate(self,
self.time_step,
self.time_step)
class Board(object):
def __init__(self):
self.name = None
self.active = False
self.temp_sensor = None
self.gpio_active = False
self.load_gpio_libs()
self.load_libs()
self.create_temp_sensor()
self.temp_sensor.start()
self.start()
def reset(self):
self.profile = None
self.start_time = 0
self.runtime = 0
self.totaltime = 0
self.target = 0
self.state = Oven.STATE_IDLE
self.set_heat(False)
self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp)
def load_libs(self):
if config.max31855:
try:
from max31855 import MAX31855, MAX31855Error
self.name='MAX31855'
self.active = True
log.info("import %s " % (self.name))
except ImportError:
msg = "max31855 config set, but import failed"
log.warning(msg)
def run_profile(self, profile, startat=0):
log.info("Running schedule %s" % profile.name)
self.profile = profile
self.totaltime = profile.get_duration()
self.state = Oven.STATE_RUNNING
self.start_time = datetime.datetime.now()
self.startat = startat * 60
log.info("Starting")
if config.max31856:
try:
from max31856 import MAX31856, MAX31856Error
self.name='MAX31856'
self.active = True
log.info("import %s " % (self.name))
except ImportError:
msg = "max31856 config set, but import failed"
log.warning(msg)
def abort_run(self):
self.reset()
def run(self):
temperature_count = 0
last_temp = 0
pid = 0
while True:
if self.state == Oven.STATE_IDLE:
time.sleep(1)
elif self.state == Oven.STATE_RUNNING:
if self.simulate:
self.runtime += 0.5
else:
runtime_delta = datetime.datetime.now() - self.start_time
if self.startat > 0:
self.runtime = self.startat + runtime_delta.total_seconds();
else:
self.runtime = runtime_delta.total_seconds()
self.target = self.profile.get_target_temperature(self.runtime)
pid = self.pid.compute(self.target, self.temp_sensor.temperature + config.thermocouple_offset)
heat_on = float(0)
heat_off = float(self.time_step)
if pid > 0:
heat_on = float(self.time_step * pid)
heat_off = float(self.time_step * (1 - pid))
time_left = self.totaltime - self.runtime
log.info("temp=%.1f, target=%.1f, pid=%.3f, heat_on=%.2f, heat_off=%.2f, run_time=%d, total_time=%d, time_left=%d" %
(self.temp_sensor.temperature + config.thermocouple_offset,
self.target,
pid,
heat_on,
heat_off,
self.runtime,
self.totaltime,
time_left))
# FIX - this whole thing should be replaced with
# a warning low and warning high below and above
# set value. If either of these are exceeded,
# warn in the interface. DO NOT RESET.
# if we are WAY TOO HOT, shut down
if(self.temp_sensor.temperature + config.thermocouple_offset >= config.emergency_shutoff_temp):
log.info("emergency!!! temperature too high, shutting down")
self.reset()
# Capture the last temperature value. This must be done before set_heat,
# since there is a sleep in there now.
last_temp = self.temp_sensor.temperature + config.thermocouple_offset
self.set_heat(pid)
if self.runtime > self.totaltime:
log.info("schedule ended, shutting down")
self.reset()
# amount of time to sleep with the heater off
# for example if pid = .6 and time step is 1, sleep for .4s
if pid > 0:
time.sleep(self.time_step * (1 - pid))
else:
time.sleep(self.time_step)
def set_heat(self, value):
if value > 0:
self.heat = 1.0
if gpio_available:
if config.heater_invert:
GPIO.output(config.gpio_heat, GPIO.LOW)
time.sleep(self.time_step * value)
GPIO.output(config.gpio_heat, GPIO.HIGH)
else:
GPIO.output(config.gpio_heat, GPIO.HIGH)
time.sleep(self.time_step * value)
GPIO.output(config.gpio_heat, GPIO.LOW)
else:
# for runs that are simulations
time.sleep(self.time_step * value)
def create_temp_sensor(self):
if config.simulate == True:
self.temp_sensor = TempSensorSimulate()
else:
self.heat = 0.0
if gpio_available:
if config.heater_invert:
GPIO.output(config.gpio_heat, GPIO.HIGH)
else:
GPIO.output(config.gpio_heat, GPIO.LOW)
def get_state(self):
state = {
'runtime': self.runtime,
'temperature': self.temp_sensor.temperature + config.thermocouple_offset,
'target': self.target,
'state': self.state,
'heat': self.heat,
'totaltime': self.totaltime,
}
return state
self.temp_sensor = TempSensorReal()
class BoardSimulated(object):
def __init__(self):
self.temp_sensor = TempSensorSimulated()
class TempSensor(threading.Thread):
def __init__(self, time_step):
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
self.temperature = 0
self.time_step = time_step
self.time_step = config.sensor_time_wait
class TempSensorSimulated(TempSensor):
'''not much here, just need to be able to set the temperature'''
def __init__(self):
TempSensor.__init__(self)
class TempSensorReal(TempSensor):
def __init__(self, time_step):
TempSensor.__init__(self, time_step)
if config.max6675:
log.info("init MAX6675")
self.thermocouple = MAX6675(config.gpio_sensor_cs,
config.gpio_sensor_clock,
config.gpio_sensor_data,
config.temp_scale)
'''real temperature sensor thread that takes N measurements
during the time_step'''
def __init__(self):
TempSensor.__init__(self)
if config.max31855:
log.info("init MAX31855")
self.thermocouple = MAX31855(config.gpio_sensor_cs,
@ -218,14 +100,19 @@ class TempSensorReal(TempSensor):
config.gpio_sensor_data,
config.temp_scale)
if config.max31855spi:
log.info("init MAX31855-spi")
self.thermocouple = MAX31855SPI(spi_dev=SPI.SpiDev(port=0, device=config.spi_sensor_chip_id))
if config.max31856:
log.info("init MAX31856")
software_spi = { 'cs': config.gpio_sensor_cs,
'clk': config.gpio_sensor_clock,
'do': config.gpio_sensor_data }
self.thermocouple = MAX31856(tc_type=config.thermocouple_type,
software_spi = sofware_spi,
units = config.temp_scale
)
def run(self):
while True:
maxtries = 5
maxtries = 5
sleeptime = self.time_step / float(maxtries)
maxtemp = 0
for x in range(0,maxtries):
@ -237,48 +124,217 @@ class TempSensorReal(TempSensor):
maxtemp = temp
time.sleep(sleeptime)
self.temperature = maxtemp
#time.sleep(self.time_step)
class Oven(threading.Thread):
'''parent oven class. this has all the common code
for either a real or simulated oven'''
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
self.temperature = 0
self.time_step = config.sensor_time_wait
self.reset()
class TempSensorSimulate(TempSensor):
def __init__(self, oven, time_step, sleep_time):
TempSensor.__init__(self, time_step)
self.oven = oven
self.sleep_time = sleep_time
def reset(self):
self.state = "IDLE"
self.profile = None
self.start_time = 0
self.runtime = 0
self.totaltime = 0
self.target = 0
self.heat = 0
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.profile = profile
self.totaltime = profile.get_duration()
self.state = "RUNNING"
self.start_time = datetime.datetime.now()
self.startat = startat * 60
log.info("Starting")
def abort_run(self):
self.reset()
def kiln_must_catch_up(self):
'''shift the whole schedule forward in time by one time_step
to wait for the kiln to catch up'''
if config.kiln_must_catch_up == True:
temp = self.board.temp_sensor.temperature + \
config.thermocouple_offset
if self.target - temp > config.kiln_must_catch_up_max_error:
log.info("kiln must catch up, shifting schedule")
self.start_time = self.start_time + \
datetime.timedelta(seconds=self.time_step)
def update_runtime(self):
runtime_delta = datetime.datetime.now() - self.start_time
if self.startat > 0:
self.runtime = self.startat + runtime_delta.total_seconds()
else:
self.runtime = runtime_delta.total_seconds()
def update_target_temp(self):
self.target = self.profile.get_target_temperature(self.runtime)
def reset_if_emergency(self):
'''reset if the temperature is way TOO HOT'''
if (self.board.temp_sensor.temperature + config.thermocouple_offset >=
config.emergency_shutoff_temp):
log.info("emergency!!! temperature too high, shutting down")
self.reset()
def reset_if_schedule_ended(self):
if self.runtime > self.totaltime:
log.info("schedule ended, shutting down")
self.reset()
def get_state(self):
state = {
'runtime': self.runtime,
'temperature': self.board.temp_sensor.temperature + config.thermocouple_offset,
'target': self.target,
'state': self.state,
'heat': self.heat,
'totaltime': self.totaltime,
}
return state
def run(self):
t_env = config.sim_t_env
c_heat = config.sim_c_heat
c_oven = config.sim_c_oven
p_heat = config.sim_p_heat
R_o_nocool = config.sim_R_o_nocool
R_ho_noair = config.sim_R_ho_noair
R_ho = R_ho_noair
t = t_env # deg C temp in oven
t_h = t # deg C temp of heat element
while True:
#heating energy
Q_h = p_heat * self.time_step * self.oven.heat
if self.state == "IDLE":
time.sleep(1)
continue
if self.state == "RUNNING":
self.kiln_must_catch_up()
self.update_runtime()
self.update_target_temp()
self.heat_then_cool()
self.reset_if_emergency()
self.reset_if_schedule_ended()
#temperature change of heat element by heating
t_h += Q_h / c_heat
#energy flux heat_el -> oven
p_ho = (t_h - t) / R_ho
class SimulatedOven(Oven):
#temperature change of oven and heat el
t += p_ho * self.time_step / c_oven
t_h -= p_ho * self.time_step / c_heat
def __init__(self):
self.reset()
self.board = BoardSimulated()
#temperature change of oven by cooling to env
p_env = (t - t_env) / R_o_nocool
t -= p_env * self.time_step / c_oven
log.debug("energy sim: -> %dW heater: %.0f -> %dW oven: %.0f -> %dW env" % (int(p_heat * self.oven.heat), t_h, int(p_ho), t, int(p_env)))
self.temperature = t
self.t_env = config.sim_t_env
self.c_heat = config.sim_c_heat
self.c_oven = config.sim_c_oven
self.p_heat = config.sim_p_heat
self.R_o_nocool = config.sim_R_o_nocool
self.R_ho_noair = config.sim_R_ho_noair
self.R_ho = self.R_ho_noair
time.sleep(self.sleep_time)
# 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")
def heating_energy(self,pid):
# using pid here simulates the element being on for
# only part of the time_step
self.Q_h = self.p_heat * self.time_step * pid
def temp_changes(self):
#temperature change of heat element by heating
self.t_h += self.Q_h / self.c_heat
#energy flux heat_el -> oven
self.p_ho = (self.t_h - self.t) / self.R_ho
#temperature change of oven and heating element
self.t += self.p_ho * self.time_step / self.c_oven
self.t_h -= self.p_ho * self.time_step / self.c_heat
#temperature change of oven by cooling to environment
self.p_env = (self.t - self.t_env) / self.R_o_nocool
self.t -= self.p_env * self.time_step / self.c_oven
self.temperature = self.t
self.board.temp_sensor.temperature = self.t
def heat_then_cool(self):
pid = self.pid.compute(self.target,
self.board.temp_sensor.temperature +
config.thermocouple_offset)
heat_on = float(self.time_step * pid)
heat_off = float(self.time_step * (1 - pid))
self.heating_energy(pid)
self.temp_changes()
# self.heat is for the front end to display if the heat is on
self.heat = 0.0
if heat_on > 0:
self.heat = 1.0
log.info("simulation: -> %dW heater: %.0f -> %dW oven: %.0f -> %dW env" % (int(self.p_heat * pid),
self.t_h,
int(self.p_ho),
self.t,
int(self.p_env)))
time_left = self.totaltime - self.runtime
log.info("temp=%.1f, target=%.1f, 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,
self.target,
pid,
heat_on,
heat_off,
self.runtime,
self.totaltime,
time_left))
# we don't actually spend time heating & cooling during
# a simulation, so sleep.
time.sleep(self.time_step)
class RealOven(Oven):
def __init__(self):
self.board = Board()
self.reset()
# call parent init
Oven.__init__(self)
# start thread
self.start()
def heat_then_cool(self):
pid = self.pid.compute(self.target,
self.board.temp_sensor.temperature +
config.thermocouple_offset)
heat_on = float(self.time_step * pid)
heat_off = float(self.time_step * (1 - pid))
# self.heat is for the front end to display if the heat is on
self.heat = 0.0
if heat_on > 0:
self.heat = 1.0
self.output.heat(heat_on)
self.output.cool(heat_off)
time_left = self.totaltime - self.runtime
log.info("temp=%.1f, target=%.1f, 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,
self.target,
pid,
heat_on,
heat_off,
self.runtime,
self.totaltime,
time_left))
class Profile():
def __init__(self, json_data):
@ -304,13 +360,6 @@ class Profile():
return (prev_point, next_point)
def is_rising(self, time):
(prev_point, next_point) = self.get_surrounding_points(time)
if prev_point and next_point:
return prev_point[1] < next_point[1]
else:
return False
def get_target_temperature(self, time):
if time > self.get_duration():
return 0
@ -323,6 +372,7 @@ class Profile():
class PID():
def __init__(self, ki=1, kp=1, kd=1):
self.ki = ki
self.kp = kp
@ -345,4 +395,8 @@ class PID():
self.lastErr = error
self.lastNow = now
# not actively cooling, so
if output < 0:
output = 0
return output

Wyświetl plik

@ -27,7 +27,7 @@ class OvenWatcher(threading.Thread):
oven_state = self.oven.get_state()
# record state for any new clients that join
if oven_state.get("state") == Oven.STATE_RUNNING:
if oven_state.get("state") == "RUNNING":
self.last_log.append(oven_state)
else:
self.recording = False

Wyświetl plik

@ -5,3 +5,4 @@ gevent
gevent-websocket
#RPi.GPIO
#Adafruit-MAX31855
#Adafruit-GPIO