This commit is contained in:
marktilles 2021-07-05 22:20:46 +02:00
parent b0988ade10
commit f73636701c
4 changed files with 114 additions and 43 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
lib/.DS_Store vendored

Binary file not shown.

View File

@ -8,6 +8,7 @@ import config
log = logging.getLogger(__name__)
class Output(object):
def __init__(self):
self.active = False
@ -26,13 +27,15 @@ class Output(object):
log.warning(msg)
self.active = False
def heat(self,sleepfor):
def heat(self,sleepfor, tuning=False):
self.GPIO.output(config.gpio_heat, self.GPIO.HIGH)
if tuning:
return
time.sleep(sleepfor)
self.GPIO.output(config.gpio_heat, self.GPIO.LOW)
def cool(self,sleepfor):
'''no active cooling, so sleep'''
self.GPIO.output(config.gpio_heat, self.GPIO.LOW)
time.sleep(sleepfor)
# FIX - Board class needs to be completely removed
@ -54,7 +57,7 @@ class Board(object):
self.active = True
log.info("import %s " % (self.name))
except ImportError:
msg = "max31855 config set, but import failed"
msg = "max31855 config set, but import failed"
log.warning(msg)
if config.max31856:
@ -64,7 +67,7 @@ class Board(object):
self.active = True
log.info("import %s " % (self.name))
except ImportError:
msg = "max31856 config set, but import failed"
msg = "max31856 config set, but import failed"
log.warning(msg)
def create_temp_sensor(self):
@ -76,13 +79,15 @@ class Board(object):
class BoardSimulated(object):
def __init__(self):
self.temp_sensor = TempSensorSimulated()
class TempSensor(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
self.temperature = 0
self.bad_percent = 0
self.time_step = config.sensor_time_wait
self.noConnection = self.shortToGround = self.shortToVCC = self.unknownError = False
class TempSensorSimulated(TempSensor):
'''not much here, just need to be able to set the temperature'''
@ -94,6 +99,11 @@ class TempSensorReal(TempSensor):
during the time_step'''
def __init__(self):
TempSensor.__init__(self)
self.sleeptime = self.time_step / float(config.temperature_average_samples)
self.bad_count = 0
self.ok_count = 0
self.bad_stamp = 0
if config.max31855:
log.info("init MAX31855")
from max31855 import MAX31855, MAX31855Error
@ -108,31 +118,50 @@ class TempSensorReal(TempSensor):
software_spi = { 'cs': config.gpio_sensor_cs,
'clk': config.gpio_sensor_clock,
'do': config.gpio_sensor_data,
### MARK TILLES ADDED
'di': config.gpio_sensor_di,
#'gpio': config.gpio_heat
}
'di': config.gpio_sensor_di }
self.thermocouple = MAX31856(tc_type=config.thermocouple_type,
software_spi = software_spi,
units = config.temp_scale
units = config.temp_scale,
ac_freq_50hz = config.ac_freq_50hz,
)
def run(self):
'''take 5 measurements over each time period and return the
average'''
'''use a moving average of config.temperature_average_samples across the time_step'''
temps = []
while True:
maxtries = 5
sleeptime = self.time_step / float(maxtries)
temps = []
for x in range(0,maxtries):
try:
temp = self.thermocouple.get()
temps.append(temp)
except Exception:
log.exception("problem reading temp")
time.sleep(sleeptime)
self.temperature = sum(temps)/len(temps)
# reset error counter if time is up
if (time.time() - self.bad_stamp) > (self.time_step * 2):
if self.bad_count + self.ok_count:
self.bad_percent = (self.bad_count / (self.bad_count + self.ok_count)) * 100
else:
self.bad_percent = 0
self.bad_count = 0
self.ok_count = 0
self.bad_stamp = time.time()
temp = self.thermocouple.get()
self.noConnection = self.thermocouple.noConnection
self.shortToGround = self.thermocouple.shortToGround
self.shortToVCC = self.thermocouple.shortToVCC
self.unknownError = self.thermocouple.unknownError
is_bad_value = self.noConnection | self.unknownError
if config.honour_theromocouple_short_errors:
is_bad_value |= self.shortToGround | self.shortToVCC
if not is_bad_value:
temps.append(temp)
if len(temps) > config.temperature_average_samples:
del temps[0]
self.ok_count += 1
else:
log.error("Problem reading temp N/C:%s GND:%s VCC:%s ???:%s" % (self.noConnection,self.shortToGround,self.shortToVCC,self.unknownError))
self.bad_count += 1
if len(temps):
self.temperature = sum(temps) / len(temps)
time.sleep(self.sleeptime)
class Oven(threading.Thread):
'''parent oven class. this has all the common code
@ -155,8 +184,22 @@ class Oven(threading.Thread):
self.pid = PID(ki=config.pid_ki, kd=config.pid_kd, kp=config.pid_kp)
def run_profile(self, profile, startat=0):
log.info("Running schedule %s" % profile.name)
self.reset()
if self.board.temp_sensor.noConnection:
log.info("Refusing to start profile - thermocouple not connected")
return
if self.board.temp_sensor.shortToGround:
log.info("Refusing to start profile - thermocouple short to ground")
return
if self.board.temp_sensor.shortToVCC:
log.info("Refusing to start profile - thermocouple short to VCC")
return
if self.board.temp_sensor.unknownError:
log.info("Refusing to start profile - thermocouple unknown error")
return
log.info("Running schedule %s" % profile.name)
self.profile = profile
self.totaltime = profile.get_duration()
self.state = "RUNNING"
@ -186,6 +229,9 @@ class Oven(threading.Thread):
def update_runtime(self):
runtime_delta = datetime.datetime.now() - self.start_time
if runtime_delta.total_seconds() < 0:
runtime_delta = datetime.timedelta(0)
if self.startat > 0:
self.runtime = self.startat + runtime_delta.total_seconds()
else:
@ -195,12 +241,24 @@ class Oven(threading.Thread):
self.target = self.profile.get_target_temperature(self.runtime)
def reset_if_emergency(self):
'''reset if the temperature is way TOO HOT'''
'''reset if the temperature is way TOO HOT, or other critical errors detected'''
if (self.board.temp_sensor.temperature + config.thermocouple_offset >=
config.emergency_shutoff_temp):
log.info("emergency!!! temperature too high, shutting down")
self.reset()
if self.board.temp_sensor.noConnection:
log.info("emergency!!! lost connection to thermocouple, shutting down")
self.reset()
if self.board.temp_sensor.unknownError:
log.info("emergency!!! unknown thermocouple error, shutting down")
self.reset()
if self.board.temp_sensor.bad_percent > 30:
log.info("emergency!!! too many errors in a short period, shutting down")
self.reset()
def reset_if_schedule_ended(self):
if self.runtime > self.totaltime:
log.info("schedule ended, shutting down")
@ -214,6 +272,8 @@ class Oven(threading.Thread):
'state': self.state,
'heat': self.heat,
'totaltime': self.totaltime,
'kwh_rate': config.kwh_rate,
'currency_type': config.currency_type,
'profile': self.profile.name if self.profile else None,
}
return state
@ -249,10 +309,10 @@ class SimulatedOven(Oven):
# 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")
@ -310,9 +370,9 @@ class SimulatedOven(Oven):
self.runtime,
self.totaltime,
time_left))
# we don't actually spend time heating & cooling during
# a simulation, so sleep.
# a simulation, so sleep.
time.sleep(self.time_step)
@ -329,6 +389,10 @@ class RealOven(Oven):
# start thread
self.start()
def reset(self):
super().reset()
self.output.cool(0)
def heat_then_cool(self):
pid = self.pid.compute(self.target,
self.board.temp_sensor.temperature +
@ -341,8 +405,10 @@ class RealOven(Oven):
if heat_on > 0:
self.heat = 1.0
self.output.heat(heat_on)
self.output.cool(heat_off)
if heat_on:
self.output.heat(heat_on)
if heat_off:
self.output.cool(heat_off)
time_left = self.totaltime - self.runtime
log.info("temp=%.2f, target=%.2f, pid=%.3f, heat_on=%.2f, heat_off=%.2f, run_time=%d, total_time=%d, time_left=%d" %
(self.board.temp_sensor.temperature + config.thermocouple_offset,
@ -400,7 +466,7 @@ class PID():
self.lastErr = 0
# FIX - this was using a really small window where the PID control
# takes effect from -1 to 1. I changed this to various numbers and
# takes effect from -1 to 1. I changed this to various numbers and
# settled on -50 to 50 and then divide by 50 at the end. This results
# in a larger PID control window and much more accurate control...
# instead of what used to be binary on/off control.
@ -413,8 +479,12 @@ class PID():
error = float(setpoint - ispoint)
if self.ki > 0:
self.iterm += (error * timeDelta * (1/self.ki))
if config.stop_integral_windup == True:
if abs(self.kp * error) < window_size:
self.iterm += (error * timeDelta * (1/self.ki))
else:
self.iterm += (error * timeDelta * (1/self.ki))
dErr = (error - self.lastErr) / timeDelta
output = self.kp * error + self.iterm + self.kd * dErr
out4logs = output
@ -426,15 +496,16 @@ class PID():
if output < 0:
output = 0
#if output > 1:
# output = 1
output = float(output / window_size)
if out4logs > 0:
log.info("pid percents pid=%0.2f p=%0.2f i=%0.2f d=%0.2f" % (out4logs,
((self.kp * error)/out4logs)*100,
(self.iterm/out4logs)*100,
((self.kd * dErr)/out4logs)*100))
if out4logs > 0:
# log.info("pid percents pid=%0.2f p=%0.2f i=%0.2f d=%0.2f" % (out4logs,
# ((self.kp * error)/out4logs)*100,
# (self.iterm/out4logs)*100,
# ((self.kd * dErr)/out4logs)*100))
log.info("pid actuals pid=%0.2f p=%0.2f i=%0.2f d=%0.2f" % (out4logs,
self.kp * error,
self.iterm,
self.kd * dErr))
return output

BIN
public/.DS_Store vendored

Binary file not shown.