diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index a9211d2..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/lib/.DS_Store b/lib/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/lib/.DS_Store and /dev/null differ diff --git a/lib/oven.py b/lib/oven.py index 65078b6..35b7288 100644 --- a/lib/oven.py +++ b/lib/oven.py @@ -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 diff --git a/public/.DS_Store b/public/.DS_Store deleted file mode 100644 index eba7a31..0000000 Binary files a/public/.DS_Store and /dev/null differ