kopia lustrzana https://github.com/jbruce12000/kiln-controller
- complete rewrite of oven class
- simulation more accurate - added kiln_must_catch_up functionality - added max31856 supportpull/15/head
rodzic
5bd8c28388
commit
c5b7f21044
35
config.py
35
config.py
|
@ -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
|
||||
|
|
|
@ -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,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)
|
||||
|
||||
|
125
lib/max6675.py
125
lib/max6675.py
|
@ -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()
|
512
lib/oven.py
512
lib/oven.py
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,3 +5,4 @@ gevent
|
|||
gevent-websocket
|
||||
#RPi.GPIO
|
||||
#Adafruit-MAX31855
|
||||
#Adafruit-GPIO
|
||||
|
|
Ładowanie…
Reference in New Issue