2013-12-10 00:29:21 +00:00
import threading
import time
import random
import datetime
import logging
import json
2013-11-29 23:13:06 +00:00
import config
2013-11-23 22:50:59 +00:00
2013-11-23 21:25:20 +00:00
log = logging . getLogger ( __name__ )
2013-11-23 23:36:10 +00:00
try :
2017-12-01 00:48:39 +00:00
if config . max31855 + config . max6675 + config . max31855spi > 1 :
log . error ( " choose (only) one converter IC " )
exit ( )
2014-12-19 08:54:15 +00:00
if config . max31855 :
2017-12-01 00:48:39 +00:00
from max31855 import MAX31855 , MAX31855Error
log . info ( " import MAX31855 " )
if config . max31855spi :
import Adafruit_GPIO . SPI as SPI
2017-12-01 02:05:16 +00:00
from max31855spi import MAX31855SPI , MAX31855SPIError
2017-12-01 00:48:39 +00:00
log . info ( " import MAX31855SPI " )
2017-12-02 00:59:07 +00:00
spi_reserved_gpio = [ 7 , 8 , 9 , 10 , 11 ]
if config . gpio_air in spi_reserved_gpio :
raise Exception ( " gpio_air pin %s collides with SPI pins %s " % ( config . gpio_air , spi_reserved_gpio ) )
if config . gpio_cool in spi_reserved_gpio :
raise Exception ( " gpio_cool pin %s collides with SPI pins %s " % ( config . gpio_cool , spi_reserved_gpio ) )
if config . gpio_door in spi_reserved_gpio :
raise Exception ( " gpio_door pin %s collides with SPI pins %s " % ( config . gpio_door , spi_reserved_gpio ) )
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 ) )
2014-12-19 08:54:15 +00:00
if config . max6675 :
2017-12-01 00:48:39 +00:00
from max6675 import MAX6675 , MAX6675Error
log . info ( " import MAX6675 " )
2013-11-29 22:27:30 +00:00
sensor_available = True
2013-11-23 23:36:10 +00:00
except ImportError :
2017-12-01 02:05:16 +00:00
log . exception ( " Could not initialize temperature sensor, using dummy values! " )
2013-11-29 22:27:30 +00:00
sensor_available = False
2013-11-23 23:36:10 +00:00
2013-11-29 17:54:51 +00:00
try :
import RPi . GPIO as GPIO
2013-11-29 18:29:11 +00:00
GPIO . setmode ( GPIO . BCM )
2013-11-29 22:30:06 +00:00
GPIO . setwarnings ( False )
2013-11-29 23:13:06 +00:00
GPIO . setup ( config . gpio_heat , GPIO . OUT )
GPIO . setup ( config . gpio_cool , GPIO . OUT )
GPIO . setup ( config . gpio_air , GPIO . OUT )
2013-11-30 12:59:35 +00:00
GPIO . setup ( config . gpio_door , GPIO . IN , pull_up_down = GPIO . PUD_UP )
2013-11-29 23:13:06 +00:00
2013-11-29 22:27:30 +00:00
gpio_available = True
2013-11-29 17:54:51 +00:00
except ImportError :
2013-12-10 00:29:21 +00:00
msg = " Could not initialize GPIOs, oven operation will only be simulated! "
log . warning ( msg )
2013-11-29 22:27:30 +00:00
gpio_available = False
2013-11-29 17:54:51 +00:00
2013-12-10 00:29:21 +00:00
2013-11-23 21:25:20 +00:00
class Oven ( threading . Thread ) :
2013-12-10 00:29:21 +00:00
STATE_IDLE = " IDLE "
STATE_RUNNING = " RUNNING "
2013-11-23 22:40:46 +00:00
2017-12-01 00:58:47 +00:00
def __init__ ( self , simulate = False , time_step = config . sensor_time_wait ) :
2013-11-23 21:25:20 +00:00
threading . Thread . __init__ ( self )
2013-11-23 22:54:45 +00:00
self . daemon = True
2013-12-06 22:02:07 +00:00
self . simulate = simulate
self . time_step = time_step
2013-11-25 00:50:10 +00:00
self . reset ( )
2013-12-06 22:02:07 +00:00
if simulate :
2013-12-10 00:29:21 +00:00
self . temp_sensor = TempSensorSimulate ( self , 0.5 , self . time_step )
2013-12-01 03:15:58 +00:00
if sensor_available :
2013-12-06 22:02:07 +00:00
self . temp_sensor = TempSensorReal ( self . time_step )
2013-12-01 03:15:58 +00:00
else :
2013-12-10 00:29:21 +00:00
self . temp_sensor = TempSensorSimulate ( self ,
self . time_step ,
self . time_step )
2013-11-25 00:50:10 +00:00
self . temp_sensor . start ( )
self . start ( )
2013-11-28 16:44:06 +00:00
2013-11-25 00:50:10 +00:00
def reset ( self ) :
2013-11-23 21:25:20 +00:00
self . profile = None
self . start_time = 0
2013-11-23 23:30:06 +00:00
self . runtime = 0
2013-11-24 14:27:29 +00:00
self . totaltime = 0
2013-11-24 17:35:08 +00:00
self . target = 0
2013-11-30 12:50:56 +00:00
self . door = self . get_door_state ( )
2013-11-23 21:25:20 +00:00
self . state = Oven . STATE_IDLE
2013-11-29 22:27:30 +00:00
self . set_heat ( False )
self . set_cool ( False )
self . set_air ( False )
2013-12-10 00:29:21 +00:00
self . pid = PID ( ki = config . pid_ki , kd = config . pid_kd , kp = config . pid_kp )
2013-11-28 16:44:06 +00:00
2013-11-23 21:25:20 +00:00
def run_profile ( self , profile ) :
2013-12-10 00:29:21 +00:00
log . info ( " Running profile %s " % profile . name )
2013-11-23 21:25:20 +00:00
self . profile = profile
2013-11-24 19:40:09 +00:00
self . totaltime = profile . get_duration ( )
2013-11-23 21:25:20 +00:00
self . state = Oven . STATE_RUNNING
self . start_time = datetime . datetime . now ( )
log . info ( " Starting " )
2013-11-23 22:40:46 +00:00
2013-11-23 21:25:20 +00:00
def abort_run ( self ) :
2013-11-25 00:50:10 +00:00
self . reset ( )
2013-11-23 22:40:46 +00:00
2013-11-23 21:25:20 +00:00
def run ( self ) :
2015-07-02 17:47:34 +00:00
temperature_count = 0
last_temp = 0
2018-06-09 20:42:55 +00:00
pid = 0
2013-11-23 21:25:20 +00:00
while True :
2013-11-30 12:50:56 +00:00
self . door = self . get_door_state ( )
2013-12-05 19:07:18 +00:00
2013-11-23 21:25:20 +00:00
if self . state == Oven . STATE_RUNNING :
2013-12-06 22:02:07 +00:00
if self . simulate :
self . runtime + = 0.5
else :
2013-12-10 00:29:21 +00:00
runtime_delta = datetime . datetime . now ( ) - self . start_time
self . runtime = runtime_delta . total_seconds ( )
log . info ( " running at %.1f deg C (Target: %.1f ) , heat %.2f , cool %.2f , air %.2f , door %s ( %.1f s/ %.0f ) " % ( self . temp_sensor . temperature , self . target , self . heat , self . cool , self . air , self . door , self . runtime , self . totaltime ) )
2013-11-24 17:35:08 +00:00
self . target = self . profile . get_target_temperature ( self . runtime )
2013-12-05 17:31:36 +00:00
pid = self . pid . compute ( self . target , self . temp_sensor . temperature )
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
log . info ( " pid: %.3f " % pid )
2013-12-05 19:07:18 +00:00
2013-12-05 17:31:36 +00:00
self . set_cool ( pid < = - 1 )
2015-07-02 17:47:34 +00:00
if ( pid > 0 ) :
# The temp should be changing with the heat on
# Count the number of time_steps encountered with no change and the heat on
if last_temp == self . temp_sensor . temperature :
temperature_count + = 1
else :
temperature_count = 0
# If the heat is on and nothing is changing, reset
# The direction or amount of change does not matter
# This prevents runaway in the event of a sensor read failure
2018-11-21 16:35:28 +00:00
#if temperature_count > 20:
# log.info("Error reading sensor, oven temp not responding to heat.")
# self.reset()
2015-07-02 17:47:34 +00:00
else :
temperature_count = 0
2018-06-09 23:22:40 +00:00
#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
2015-07-02 17:47:34 +00:00
2018-06-09 20:42:55 +00:00
self . set_heat ( pid )
2015-07-02 17:47:34 +00:00
2013-12-05 17:31:36 +00:00
#if self.profile.is_rising(self.runtime):
# self.set_cool(False)
# self.set_heat(self.temp_sensor.temperature < self.target)
#else:
# self.set_heat(False)
# self.set_cool(self.temp_sensor.temperature > self.target)
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
if self . temp_sensor . temperature > 200 :
2013-12-01 03:15:58 +00:00
self . set_air ( False )
2013-12-10 00:29:21 +00:00
elif self . temp_sensor . temperature < 180 :
2013-12-01 03:15:58 +00:00
self . set_air ( True )
2013-12-05 19:07:18 +00:00
2013-11-24 14:27:29 +00:00
if self . runtime > = self . totaltime :
2013-11-25 00:50:10 +00:00
self . reset ( )
2013-12-06 22:02:07 +00:00
2018-06-09 23:22:40 +00:00
2018-06-09 20:42:55 +00:00
if pid > 0 :
time . sleep ( self . time_step * ( 1 - pid ) )
else :
time . sleep ( self . time_step )
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
def set_heat ( self , value ) :
2018-06-09 20:42:55 +00:00
if value > 0 :
2013-11-29 22:27:30 +00:00
self . heat = 1.0
if gpio_available :
2016-04-07 14:51:46 +00:00
if config . heater_invert :
2017-12-01 00:48:39 +00:00
GPIO . output ( config . gpio_heat , GPIO . LOW )
2018-06-09 20:42:55 +00:00
time . sleep ( self . time_step * value )
GPIO . output ( config . gpio_heat , GPIO . HIGH )
2016-04-07 14:51:46 +00:00
else :
GPIO . output ( config . gpio_heat , GPIO . HIGH )
2018-06-09 20:42:55 +00:00
time . sleep ( self . time_step * value )
GPIO . output ( config . gpio_heat , GPIO . LOW )
2013-11-29 22:27:30 +00:00
else :
self . heat = 0.0
if gpio_available :
2016-04-07 14:51:46 +00:00
if config . heater_invert :
2017-12-01 00:48:39 +00:00
GPIO . output ( config . gpio_heat , GPIO . HIGH )
2016-04-07 14:51:46 +00:00
else :
GPIO . output ( config . gpio_heat , GPIO . LOW )
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
def set_cool ( self , value ) :
2013-11-29 22:27:30 +00:00
if value :
self . cool = 1.0
if gpio_available :
2013-11-29 23:13:06 +00:00
GPIO . output ( config . gpio_cool , GPIO . LOW )
2013-11-29 22:27:30 +00:00
else :
self . cool = 0.0
if gpio_available :
2013-11-29 23:13:06 +00:00
GPIO . output ( config . gpio_cool , GPIO . HIGH )
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
def set_air ( self , value ) :
2013-11-29 22:27:30 +00:00
if value :
self . air = 1.0
if gpio_available :
2013-11-29 23:13:06 +00:00
GPIO . output ( config . gpio_air , GPIO . LOW )
2013-11-29 22:27:30 +00:00
else :
self . air = 0.0
if gpio_available :
2013-11-29 23:13:06 +00:00
GPIO . output ( config . gpio_air , GPIO . HIGH )
2013-12-05 19:07:18 +00:00
2013-11-23 21:25:20 +00:00
def get_state ( self ) :
state = {
2013-11-23 23:30:06 +00:00
' runtime ' : self . runtime ,
2013-11-23 21:25:20 +00:00
' temperature ' : self . temp_sensor . temperature ,
2013-11-24 17:35:08 +00:00
' target ' : self . target ,
2013-11-23 21:25:20 +00:00
' state ' : self . state ,
2013-11-29 22:27:30 +00:00
' heat ' : self . heat ,
' cool ' : self . cool ,
2013-12-10 00:29:21 +00:00
' air ' : self . air ,
2013-11-30 12:48:07 +00:00
' totaltime ' : self . totaltime ,
' door ' : self . door
2013-11-23 21:25:20 +00:00
}
return state
2013-12-05 19:07:18 +00:00
2013-11-30 12:50:56 +00:00
def get_door_state ( self ) :
2013-11-30 12:48:07 +00:00
if gpio_available :
2013-11-30 12:50:56 +00:00
return " OPEN " if GPIO . input ( config . gpio_door ) else " CLOSED "
2013-11-30 12:48:07 +00:00
else :
return " UNKNOWN "
2013-12-05 19:07:18 +00:00
2013-11-23 22:40:46 +00:00
2013-11-23 21:25:20 +00:00
class TempSensor ( threading . Thread ) :
2013-12-10 00:29:21 +00:00
def __init__ ( self , time_step ) :
2013-11-23 21:25:20 +00:00
threading . Thread . __init__ ( self )
2013-11-23 22:54:45 +00:00
self . daemon = True
2013-11-23 21:25:20 +00:00
self . temperature = 0
2013-12-06 22:02:07 +00:00
self . time_step = time_step
2013-12-10 00:29:21 +00:00
2013-12-01 03:15:58 +00:00
class TempSensorReal ( TempSensor ) :
2013-12-10 00:29:21 +00:00
def __init__ ( self , time_step ) :
TempSensor . __init__ ( self , time_step )
2014-12-19 08:54:15 +00:00
if config . max6675 :
2017-12-01 00:48:39 +00:00
log . info ( " init MAX6675 " )
self . thermocouple = MAX6675 ( config . gpio_sensor_cs ,
2014-12-19 08:54:15 +00:00
config . gpio_sensor_clock ,
config . gpio_sensor_data ,
2016-07-08 04:41:06 +00:00
config . temp_scale )
2014-12-19 08:54:15 +00:00
if config . max31855 :
2017-12-01 00:48:39 +00:00
log . info ( " init MAX31855 " )
self . thermocouple = MAX31855 ( config . gpio_sensor_cs ,
2013-12-10 00:29:21 +00:00
config . gpio_sensor_clock ,
config . gpio_sensor_data ,
2016-07-08 04:41:06 +00:00
config . temp_scale )
2013-12-05 19:07:18 +00:00
2017-12-01 00:48:39 +00:00
if config . max31855spi :
log . info ( " init MAX31855-spi " )
self . thermocouple = MAX31855SPI ( spi_dev = SPI . SpiDev ( port = 0 , device = config . spi_sensor_chip_id ) )
2013-12-01 03:15:58 +00:00
def run ( self ) :
while True :
2017-12-01 01:09:29 +00:00
try :
self . temperature = self . thermocouple . get ( )
2017-12-01 02:05:16 +00:00
except Exception :
log . exception ( " problem reading temp " )
2013-12-01 03:15:58 +00:00
time . sleep ( self . time_step )
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
2013-12-01 03:15:58 +00:00
class TempSensorSimulate ( TempSensor ) :
2013-12-10 00:29:21 +00:00
def __init__ ( self , oven , time_step , sleep_time ) :
TempSensor . __init__ ( self , time_step )
2013-11-23 21:25:20 +00:00
self . oven = oven
2013-12-06 22:02:07 +00:00
self . sleep_time = sleep_time
2013-12-05 19:07:18 +00:00
2013-11-23 21:25:20 +00:00
def run ( self ) :
2013-12-06 22:16:51 +00:00
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_o_cool = config . sim_R_o_cool
R_ho_noair = config . sim_R_ho_noair
2013-12-10 00:29:21 +00:00
R_ho_air = config . sim_R_ho_air
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
t = t_env # deg C temp in oven
t_h = t # deg C temp of heat element
2013-11-23 21:25:20 +00:00
while True :
2013-12-01 03:15:58 +00:00
#heating energy
2013-12-05 19:07:18 +00:00
Q_h = p_heat * self . time_step * self . oven . heat
2013-12-01 03:15:58 +00:00
#temperature change of heat element by heating
2013-12-05 19:07:18 +00:00
t_h + = Q_h / c_heat
2013-12-01 03:15:58 +00:00
if self . oven . air :
R_ho = R_ho_air
2013-11-23 22:50:59 +00:00
else :
2013-12-01 03:15:58 +00:00
R_ho = R_ho_noair
2013-12-05 19:07:18 +00:00
2013-12-01 03:15:58 +00:00
#energy flux heat_el -> oven
p_ho = ( t_h - t ) / R_ho
2013-12-05 19:07:18 +00:00
2013-12-01 03:15:58 +00:00
#temperature change of oven and heat el
2013-12-10 00:29:21 +00:00
t + = p_ho * self . time_step / c_oven
t_h - = p_ho * self . time_step / c_heat
2013-12-05 19:07:18 +00:00
2013-12-01 03:15:58 +00:00
#energy flux oven -> env
if self . oven . cool :
p_env = ( t - t_env ) / R_o_cool
else :
p_env = ( t - t_env ) / R_o_nocool
2013-12-05 19:07:18 +00:00
2013-12-01 03:15:58 +00:00
#temperature change of oven by cooling to env
2013-12-10 00:29:21 +00:00
t - = p_env * self . time_step / c_oven
log . debug ( " energy sim: -> %d W heater: %.0f -> %d W oven: %.0f -> %d W env " % ( int ( p_heat * self . oven . heat ) , t_h , int ( p_ho ) , t , int ( p_env ) ) )
2013-12-01 03:15:58 +00:00
self . temperature = t
2013-12-05 19:07:18 +00:00
2013-12-06 22:02:07 +00:00
time . sleep ( self . sleep_time )
2013-11-23 22:40:46 +00:00
2013-12-10 00:29:21 +00:00
2013-11-24 16:57:12 +00:00
class Profile ( ) :
2013-12-10 00:29:21 +00:00
def __init__ ( self , json_data ) :
2013-11-24 16:57:12 +00:00
obj = json . loads ( json_data )
self . name = obj [ " name " ]
2013-11-24 17:35:08 +00:00
self . data = sorted ( obj [ " data " ] )
2013-11-28 16:44:06 +00:00
2013-11-24 16:57:12 +00:00
def get_duration ( self ) :
2013-12-10 00:29:21 +00:00
return max ( [ t for ( t , x ) in self . data ] )
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
def get_surrounding_points ( self , time ) :
2013-11-24 17:35:08 +00:00
if time > self . get_duration ( ) :
2013-12-10 00:29:21 +00:00
return ( None , None )
2013-12-05 19:07:18 +00:00
2013-11-24 17:35:08 +00:00
prev_point = None
next_point = None
2013-11-28 16:44:06 +00:00
2013-11-24 17:35:08 +00:00
for i in range ( len ( self . data ) ) :
2013-11-24 17:54:29 +00:00
if time < self . data [ i ] [ 0 ] :
prev_point = self . data [ i - 1 ]
next_point = self . data [ i ]
2013-11-24 17:35:08 +00:00
break
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
return ( prev_point , next_point )
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
def is_rising ( self , time ) :
( prev_point , next_point ) = self . get_surrounding_points ( time )
2013-11-29 22:46:21 +00:00
if prev_point and next_point :
return prev_point [ 1 ] < next_point [ 1 ]
else :
return False
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
def get_target_temperature ( self , time ) :
2013-11-29 22:27:30 +00:00
if time > self . get_duration ( ) :
return 0
2013-11-28 16:44:06 +00:00
2013-12-10 00:29:21 +00:00
( prev_point , next_point ) = self . get_surrounding_points ( time )
2013-12-05 19:07:18 +00:00
2013-11-24 17:54:29 +00:00
incl = float ( next_point [ 1 ] - prev_point [ 1 ] ) / float ( next_point [ 0 ] - prev_point [ 0 ] )
2013-11-24 17:35:08 +00:00
temp = prev_point [ 1 ] + ( time - prev_point [ 0 ] ) * incl
return temp
2013-12-05 17:31:36 +00:00
2013-12-10 00:29:21 +00:00
2013-12-05 17:31:36 +00:00
class PID ( ) :
2013-12-10 00:29:21 +00:00
def __init__ ( self , ki = 1 , kp = 1 , kd = 1 ) :
2013-12-05 17:31:36 +00:00
self . ki = ki
self . kp = kp
self . kd = kd
self . lastNow = datetime . datetime . now ( )
self . iterm = 0
self . lastErr = 0
2013-12-05 19:07:18 +00:00
2013-12-10 00:29:21 +00:00
def compute ( self , setpoint , ispoint ) :
2013-12-05 17:31:36 +00:00
now = datetime . datetime . now ( )
timeDelta = ( now - self . lastNow ) . total_seconds ( )
2013-12-05 19:07:18 +00:00
2013-12-05 17:31:36 +00:00
error = float ( setpoint - ispoint )
self . iterm + = ( error * timeDelta * self . ki )
2013-12-10 00:29:21 +00:00
self . iterm = sorted ( [ - 1 , self . iterm , 1 ] ) [ 1 ]
2013-12-05 17:31:36 +00:00
dErr = ( error - self . lastErr ) / timeDelta
2013-12-05 19:07:18 +00:00
2013-12-05 17:31:36 +00:00
output = self . kp * error + self . iterm + self . kd * dErr
2013-12-10 00:29:21 +00:00
output = sorted ( [ - 1 , output , 1 ] ) [ 1 ]
2013-12-05 17:31:36 +00:00
self . lastErr = error
self . lastNow = now
2013-12-05 19:07:18 +00:00
2013-12-05 17:31:36 +00:00
return output