| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | # Printer heater support | 
					
						
							|  |  |  | # | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  | # Copyright (C) 2016,2017  Kevin O'Connor <kevin@koconnor.net> | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | # | 
					
						
							|  |  |  | # This file may be distributed under the terms of the GNU GPLv3 license. | 
					
						
							|  |  |  | import math, logging, threading | 
					
						
							| 
									
										
										
										
											2017-08-21 11:25:26 -04:00
										 |  |  | import pins | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | # Sensors | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | KELVIN_TO_CELCIUS = -273.15 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Thermistor calibrated with three temp measurements | 
					
						
							|  |  |  | class Thermistor: | 
					
						
							|  |  |  |     def __init__(self, config, params): | 
					
						
							|  |  |  |         self.pullup = config.getfloat('pullup_resistor', 4700., above=0.) | 
					
						
							|  |  |  |         # Calculate Steinhart-Hart coefficents from temp measurements | 
					
						
							|  |  |  |         inv_t1 = 1. / (params['t1'] - KELVIN_TO_CELCIUS) | 
					
						
							|  |  |  |         inv_t2 = 1. / (params['t2'] - KELVIN_TO_CELCIUS) | 
					
						
							|  |  |  |         inv_t3 = 1. / (params['t3'] - KELVIN_TO_CELCIUS) | 
					
						
							|  |  |  |         ln_r1 = math.log(params['r1']) | 
					
						
							|  |  |  |         ln_r2 = math.log(params['r2']) | 
					
						
							|  |  |  |         ln_r3 = math.log(params['r3']) | 
					
						
							| 
									
										
										
										
											2017-08-18 13:28:04 -04:00
										 |  |  |         ln3_r1, ln3_r2, ln3_r3 = ln_r1**3, ln_r2**3, ln_r3**3 | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-18 13:28:04 -04:00
										 |  |  |         inv_t12, inv_t13 = inv_t1 - inv_t2, inv_t1 - inv_t3 | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |         ln_r12, ln_r13 = ln_r1 - ln_r2, ln_r1 - ln_r3 | 
					
						
							|  |  |  |         ln3_r12, ln3_r13 = ln3_r1 - ln3_r2, ln3_r1 - ln3_r3 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.c3 = ((inv_t12 - inv_t13 * ln_r12 / ln_r13) | 
					
						
							|  |  |  |                    / (ln3_r12 - ln3_r13 * ln_r12 / ln_r13)) | 
					
						
							|  |  |  |         self.c2 = (inv_t12 - self.c3 * ln3_r12) / ln_r12 | 
					
						
							| 
									
										
										
										
											2017-08-18 13:28:04 -04:00
										 |  |  |         self.c1 = inv_t1 - self.c2 * ln_r1 - self.c3 * ln3_r1 | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |     def calc_temp(self, adc): | 
					
						
							| 
									
										
										
										
											2017-11-13 11:07:20 -05:00
										 |  |  |         adc = max(.00001, min(.99999, adc)) | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |         r = self.pullup * adc / (1.0 - adc) | 
					
						
							|  |  |  |         ln_r = math.log(r) | 
					
						
							| 
									
										
										
										
											2017-08-18 13:28:04 -04:00
										 |  |  |         inv_t = self.c1 + self.c2 * ln_r + self.c3 * ln_r**3 | 
					
						
							|  |  |  |         return 1.0/inv_t + KELVIN_TO_CELCIUS | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |     def calc_adc(self, temp): | 
					
						
							| 
									
										
										
										
											2017-08-18 13:28:04 -04:00
										 |  |  |         inv_t = 1. / (temp - KELVIN_TO_CELCIUS) | 
					
						
							| 
									
										
										
										
											2017-08-17 14:34:25 -04:00
										 |  |  |         if self.c3: | 
					
						
							| 
									
										
										
										
											2017-08-18 13:28:04 -04:00
										 |  |  |             y = (self.c1 - inv_t) / (2. * self.c3) | 
					
						
							|  |  |  |             x = math.sqrt((self.c2 / (3. * self.c3))**3 + y**2) | 
					
						
							|  |  |  |             ln_r = math.pow(x - y, 1./3.) - math.pow(x + y, 1./3.) | 
					
						
							| 
									
										
										
										
											2017-08-17 14:34:25 -04:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2017-08-18 13:28:04 -04:00
										 |  |  |             ln_r = (inv_t - self.c1) / self.c2 | 
					
						
							| 
									
										
										
										
											2017-08-20 12:42:50 -04:00
										 |  |  |         r = math.exp(ln_r) | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |         return r / (self.pullup + r) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-17 14:34:25 -04:00
										 |  |  | # Thermistor calibrated from one temp measurement and its beta | 
					
						
							|  |  |  | class ThermistorBeta(Thermistor): | 
					
						
							|  |  |  |     def __init__(self, config, params): | 
					
						
							|  |  |  |         self.pullup = config.getfloat('pullup_resistor', 4700., above=0.) | 
					
						
							|  |  |  |         # Calculate Steinhart-Hart coefficents from beta | 
					
						
							|  |  |  |         inv_t1 = 1. / (params['t1'] - KELVIN_TO_CELCIUS) | 
					
						
							|  |  |  |         ln_r1 = math.log(params['r1']) | 
					
						
							|  |  |  |         self.c3 = 0. | 
					
						
							|  |  |  |         self.c2 = 1. / params['beta'] | 
					
						
							|  |  |  |         self.c1 = inv_t1 - self.c2 * ln_r1 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  | # Linear style conversion chips calibrated with two temp measurements | 
					
						
							|  |  |  | class Linear: | 
					
						
							|  |  |  |     def __init__(self, config, params): | 
					
						
							|  |  |  |         adc_voltage = config.getfloat('adc_voltage', 5., above=0.) | 
					
						
							|  |  |  |         slope = (params['t2'] - params['t1']) / (params['v2'] - params['v1']) | 
					
						
							|  |  |  |         self.gain = adc_voltage * slope | 
					
						
							|  |  |  |         self.offset = params['t1'] - params['v1'] * slope | 
					
						
							|  |  |  |     def calc_temp(self, adc): | 
					
						
							|  |  |  |         return adc * self.gain + self.offset | 
					
						
							|  |  |  |     def calc_adc(self, temp): | 
					
						
							|  |  |  |         return (temp - self.offset) / self.gain | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-06 15:33:21 -05:00
										 |  |  | # Available sensors | 
					
						
							|  |  |  | Sensors = { | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |     "EPCOS 100K B57560G104F": { | 
					
						
							|  |  |  |         'class': Thermistor, 't1': 25., 'r1': 100000., | 
					
						
							|  |  |  |         't2': 150., 'r2': 1641.9, 't3': 250., 'r3': 226.15 }, | 
					
						
							|  |  |  |     "ATC Semitec 104GT-2": { | 
					
						
							|  |  |  |         'class': Thermistor, 't1': 20., 'r1': 126800., | 
					
						
							|  |  |  |         't2': 150., 'r2': 1360., 't3': 300., 'r3': 80.65 }, | 
					
						
							| 
									
										
										
										
											2017-08-17 14:34:25 -04:00
										 |  |  |     "NTC 100K beta 3950": { | 
					
						
							|  |  |  |         'class': ThermistorBeta, 't1': 25., 'r1': 100000., 'beta': 3950. }, | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |     "AD595": { 'class': Linear, 't1': 25., 'v1': .25, 't2': 300., 'v2': 3.022 }, | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | # Heater | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | SAMPLE_TIME = 0.001 | 
					
						
							|  |  |  | SAMPLE_COUNT = 8 | 
					
						
							|  |  |  | REPORT_TIME = 0.300 | 
					
						
							| 
									
										
										
										
											2017-03-08 20:00:27 -05:00
										 |  |  | PWM_CYCLE_TIME = 0.100 | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | MAX_HEAT_TIME = 5.0 | 
					
						
							|  |  |  | AMBIENT_TEMP = 25. | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  | PID_PARAM_BASE = 255. | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-21 10:48:42 -05:00
										 |  |  | class error(Exception): | 
					
						
							|  |  |  |     pass | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | class PrinterHeater: | 
					
						
							| 
									
										
										
										
											2017-02-21 10:48:42 -05:00
										 |  |  |     error = error | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     def __init__(self, printer, config): | 
					
						
							| 
									
										
										
										
											2017-03-12 22:43:05 -04:00
										 |  |  |         self.name = config.section | 
					
						
							| 
									
										
										
										
											2017-03-06 16:11:58 -05:00
										 |  |  |         sensor_params = config.getchoice('sensor_type', Sensors) | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |         self.sensor = sensor_params['class'](config, sensor_params) | 
					
						
							| 
									
										
										
										
											2017-04-11 11:37:09 -04:00
										 |  |  |         self.min_temp = config.getfloat('min_temp', minval=0.) | 
					
						
							|  |  |  |         self.max_temp = config.getfloat('max_temp', above=self.min_temp) | 
					
						
							|  |  |  |         self.min_extrude_temp = config.getfloat( | 
					
						
							|  |  |  |             'min_extrude_temp', 170., minval=self.min_temp, maxval=self.max_temp) | 
					
						
							|  |  |  |         self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.lock = threading.Lock() | 
					
						
							|  |  |  |         self.last_temp = 0. | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         self.last_temp_time = 0. | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.target_temp = 0. | 
					
						
							| 
									
										
										
										
											2017-01-10 18:36:43 -05:00
										 |  |  |         algos = {'watermark': ControlBangBang, 'pid': ControlPID} | 
					
						
							| 
									
										
										
										
											2017-03-12 22:43:05 -04:00
										 |  |  |         algo = config.getchoice('control', algos) | 
					
						
							|  |  |  |         heater_pin = config.get('heater_pin') | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |         if algo is ControlBangBang and self.max_power == 1.: | 
					
						
							| 
									
										
										
										
											2017-08-21 11:25:26 -04:00
										 |  |  |             self.mcu_pwm = pins.setup_pin(printer, 'digital_out', heater_pin) | 
					
						
							| 
									
										
										
										
											2017-01-10 18:36:43 -05:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2017-08-21 11:25:26 -04:00
										 |  |  |             self.mcu_pwm = pins.setup_pin(printer, 'pwm', heater_pin) | 
					
						
							|  |  |  |             self.mcu_pwm.setup_cycle_time(PWM_CYCLE_TIME) | 
					
						
							|  |  |  |         self.mcu_pwm.setup_max_duration(MAX_HEAT_TIME) | 
					
						
							|  |  |  |         self.mcu_adc = pins.setup_pin(printer, 'adc', config.get('sensor_pin')) | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |         adc_range = [self.sensor.calc_adc(self.min_temp), | 
					
						
							|  |  |  |                      self.sensor.calc_adc(self.max_temp)] | 
					
						
							| 
									
										
										
										
											2017-08-21 11:25:26 -04:00
										 |  |  |         self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, | 
					
						
							|  |  |  |                                   minval=min(adc_range), maxval=max(adc_range)) | 
					
						
							|  |  |  |         self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback) | 
					
						
							| 
									
										
										
										
											2017-09-13 12:06:08 -04:00
										 |  |  |         is_fileoutput = self.mcu_adc.get_mcu().is_fileoutput() | 
					
						
							|  |  |  |         self.can_extrude = self.min_extrude_temp <= 0. or is_fileoutput | 
					
						
							| 
									
										
										
										
											2017-03-12 22:43:05 -04:00
										 |  |  |         self.control = algo(self, config) | 
					
						
							|  |  |  |         # pwm caching | 
					
						
							|  |  |  |         self.next_pwm_time = 0. | 
					
						
							|  |  |  |         self.last_pwm_value = 0 | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |     def set_pwm(self, read_time, value): | 
					
						
							| 
									
										
										
										
											2017-03-13 13:13:31 -04:00
										 |  |  |         if self.target_temp <= 0.: | 
					
						
							|  |  |  |             value = 0. | 
					
						
							|  |  |  |         if ((read_time < self.next_pwm_time or not self.last_pwm_value) | 
					
						
							|  |  |  |             and abs(value - self.last_pwm_value) < 0.05): | 
					
						
							|  |  |  |             # No significant change in value - can suppress update | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |             return | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         pwm_time = read_time + REPORT_TIME + SAMPLE_TIME*SAMPLE_COUNT | 
					
						
							|  |  |  |         self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.last_pwm_value = value | 
					
						
							| 
									
										
										
										
											2017-09-27 11:43:14 -04:00
										 |  |  |         logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])", | 
					
						
							|  |  |  |                       self.name, value, pwm_time, | 
					
						
							|  |  |  |                       self.last_temp, self.last_temp_time, self.target_temp) | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         self.mcu_pwm.set_pwm(pwm_time, value) | 
					
						
							|  |  |  |     def adc_callback(self, read_time, read_value): | 
					
						
							| 
									
										
										
										
											2017-08-17 13:18:13 -04:00
										 |  |  |         temp = self.sensor.calc_temp(read_value) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         with self.lock: | 
					
						
							|  |  |  |             self.last_temp = temp | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |             self.last_temp_time = read_time | 
					
						
							| 
									
										
										
										
											2016-11-08 09:22:43 -05:00
										 |  |  |             self.can_extrude = (temp >= self.min_extrude_temp) | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |             self.control.adc_callback(read_time, temp) | 
					
						
							| 
									
										
										
										
											2017-09-27 11:43:14 -04:00
										 |  |  |         #logging.debug("temp: %.3f %f = %f", read_time, read_value, temp) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     # External commands | 
					
						
							|  |  |  |     def set_temp(self, print_time, degrees): | 
					
						
							| 
									
										
										
										
											2017-02-21 10:48:42 -05:00
										 |  |  |         if degrees and (degrees < self.min_temp or degrees > self.max_temp): | 
					
						
							|  |  |  |             raise error("Requested temperature (%.1f) out of range (%.1f:%.1f)" | 
					
						
							|  |  |  |                         % (degrees, self.min_temp, self.max_temp)) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         with self.lock: | 
					
						
							|  |  |  |             self.target_temp = degrees | 
					
						
							| 
									
										
										
										
											2017-10-11 22:16:02 -04:00
										 |  |  |     def get_temp(self, eventtime): | 
					
						
							|  |  |  |         print_time = self.mcu_adc.get_mcu().estimated_print_time(eventtime) - 5. | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         with self.lock: | 
					
						
							| 
									
										
										
										
											2017-10-11 22:16:02 -04:00
										 |  |  |             if self.last_temp_time < print_time: | 
					
						
							|  |  |  |                 return 0., self.target_temp | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |             return self.last_temp, self.target_temp | 
					
						
							|  |  |  |     def check_busy(self, eventtime): | 
					
						
							|  |  |  |         with self.lock: | 
					
						
							|  |  |  |             return self.control.check_busy(eventtime) | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |     def start_auto_tune(self, degrees): | 
					
						
							|  |  |  |         if degrees and (degrees < self.min_temp or degrees > self.max_temp): | 
					
						
							|  |  |  |             raise error("Requested temperature (%.1f) out of range (%.1f:%.1f)" | 
					
						
							|  |  |  |                         % (degrees, self.min_temp, self.max_temp)) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         with self.lock: | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |             self.control = ControlAutoTune(self, self.control) | 
					
						
							|  |  |  |             self.target_temp = degrees | 
					
						
							|  |  |  |     def finish_auto_tune(self, old_control): | 
					
						
							|  |  |  |         self.control = old_control | 
					
						
							|  |  |  |         self.target_temp = 0 | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | # Bang-bang control algo | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ControlBangBang: | 
					
						
							|  |  |  |     def __init__(self, heater, config): | 
					
						
							|  |  |  |         self.heater = heater | 
					
						
							| 
									
										
										
										
											2017-04-11 11:37:09 -04:00
										 |  |  |         self.max_delta = config.getfloat('max_delta', 2.0, above=0.) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.heating = False | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |     def adc_callback(self, read_time, temp): | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         if self.heating and temp >= self.heater.target_temp+self.max_delta: | 
					
						
							|  |  |  |             self.heating = False | 
					
						
							|  |  |  |         elif not self.heating and temp <= self.heater.target_temp-self.max_delta: | 
					
						
							|  |  |  |             self.heating = True | 
					
						
							|  |  |  |         if self.heating: | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |             self.heater.set_pwm(read_time, self.heater.max_power) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |             self.heater.set_pwm(read_time, 0.) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     def check_busy(self, eventtime): | 
					
						
							|  |  |  |         return self.heater.last_temp < self.heater.target_temp-self.max_delta | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | # Proportional Integral Derivative (PID) control algo | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-03 10:44:01 -05:00
										 |  |  | PID_SETTLE_DELTA = 1. | 
					
						
							|  |  |  | PID_SETTLE_SLOPE = .1 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | class ControlPID: | 
					
						
							|  |  |  |     def __init__(self, heater, config): | 
					
						
							|  |  |  |         self.heater = heater | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |         self.Kp = config.getfloat('pid_Kp') / PID_PARAM_BASE | 
					
						
							|  |  |  |         self.Ki = config.getfloat('pid_Ki') / PID_PARAM_BASE | 
					
						
							|  |  |  |         self.Kd = config.getfloat('pid_Kd') / PID_PARAM_BASE | 
					
						
							| 
									
										
										
										
											2017-04-11 11:37:09 -04:00
										 |  |  |         self.min_deriv_time = config.getfloat('pid_deriv_time', 2., above=0.) | 
					
						
							|  |  |  |         imax = config.getfloat('pid_integral_max', heater.max_power, minval=0.) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.temp_integ_max = imax / self.Ki | 
					
						
							|  |  |  |         self.prev_temp = AMBIENT_TEMP | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         self.prev_temp_time = 0. | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.prev_temp_deriv = 0. | 
					
						
							|  |  |  |         self.prev_temp_integ = 0. | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |     def adc_callback(self, read_time, temp): | 
					
						
							|  |  |  |         time_diff = read_time - self.prev_temp_time | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         # Calculate change of temperature | 
					
						
							|  |  |  |         temp_diff = temp - self.prev_temp | 
					
						
							|  |  |  |         if time_diff >= self.min_deriv_time: | 
					
						
							|  |  |  |             temp_deriv = temp_diff / time_diff | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             temp_deriv = (self.prev_temp_deriv * (self.min_deriv_time-time_diff) | 
					
						
							|  |  |  |                           + temp_diff) / self.min_deriv_time | 
					
						
							|  |  |  |         # Calculate accumulated temperature "error" | 
					
						
							|  |  |  |         temp_err = self.heater.target_temp - temp | 
					
						
							|  |  |  |         temp_integ = self.prev_temp_integ + temp_err * time_diff | 
					
						
							|  |  |  |         temp_integ = max(0., min(self.temp_integ_max, temp_integ)) | 
					
						
							|  |  |  |         # Calculate output | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |         co = self.Kp*temp_err + self.Ki*temp_integ - self.Kd*temp_deriv | 
					
						
							| 
									
										
										
										
											2017-09-27 11:43:14 -04:00
										 |  |  |         #logging.debug("pid: %f@%.3f -> diff=%f deriv=%f err=%f integ=%f co=%d", | 
					
						
							|  |  |  |         #    temp, read_time, temp_diff, temp_deriv, temp_err, temp_integ, co) | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |         bounded_co = max(0., min(self.heater.max_power, co)) | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         self.heater.set_pwm(read_time, bounded_co) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         # Store state for next measurement | 
					
						
							|  |  |  |         self.prev_temp = temp | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         self.prev_temp_time = read_time | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.prev_temp_deriv = temp_deriv | 
					
						
							|  |  |  |         if co == bounded_co: | 
					
						
							|  |  |  |             self.prev_temp_integ = temp_integ | 
					
						
							|  |  |  |     def check_busy(self, eventtime): | 
					
						
							|  |  |  |         temp_diff = self.heater.target_temp - self.heater.last_temp | 
					
						
							| 
									
										
										
										
											2018-01-03 10:44:01 -05:00
										 |  |  |         return (abs(temp_diff) > PID_SETTLE_DELTA | 
					
						
							|  |  |  |                 or abs(self.prev_temp_deriv) > PID_SETTLE_SLOPE) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | # Ziegler-Nichols PID autotuning | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | TUNE_PID_DELTA = 5.0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ControlAutoTune: | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |     def __init__(self, heater, old_control): | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.heater = heater | 
					
						
							|  |  |  |         self.old_control = old_control | 
					
						
							|  |  |  |         self.heating = False | 
					
						
							|  |  |  |         self.peaks = [] | 
					
						
							|  |  |  |         self.peak = 0. | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         self.peak_time = 0. | 
					
						
							|  |  |  |     def adc_callback(self, read_time, temp): | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |         if self.heating and temp >= self.heater.target_temp: | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |             self.heating = False | 
					
						
							|  |  |  |             self.check_peaks() | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |         elif (not self.heating | 
					
						
							|  |  |  |               and temp <= self.heater.target_temp - TUNE_PID_DELTA): | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |             self.heating = True | 
					
						
							|  |  |  |             self.check_peaks() | 
					
						
							|  |  |  |         if self.heating: | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |             self.heater.set_pwm(read_time, self.heater.max_power) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |             if temp < self.peak: | 
					
						
							|  |  |  |                 self.peak = temp | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |                 self.peak_time = read_time | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |             self.heater.set_pwm(read_time, 0.) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |             if temp > self.peak: | 
					
						
							|  |  |  |                 self.peak = temp | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |                 self.peak_time = read_time | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     def check_peaks(self): | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         self.peaks.append((self.peak, self.peak_time)) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         if self.heating: | 
					
						
							|  |  |  |             self.peak = 9999999. | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.peak = -9999999. | 
					
						
							|  |  |  |         if len(self.peaks) < 4: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         temp_diff = self.peaks[-1][0] - self.peaks[-2][0] | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         time_diff = self.peaks[-1][1] - self.peaks[-3][1] | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |         max_power = self.heater.max_power | 
					
						
							|  |  |  |         Ku = 4. * (2. * max_power) / (abs(temp_diff) * math.pi) | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         Tu = time_diff | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |         Kp = 0.6 * Ku | 
					
						
							|  |  |  |         Ti = 0.5 * Tu | 
					
						
							|  |  |  |         Td = 0.125 * Tu | 
					
						
							|  |  |  |         Ki = Kp / Ti | 
					
						
							|  |  |  |         Kd = Kp * Td | 
					
						
							| 
									
										
										
										
											2017-09-27 11:43:14 -04:00
										 |  |  |         logging.info("Autotune: raw=%f/%f Ku=%f Tu=%f  Kp=%f Ki=%f Kd=%f", | 
					
						
							|  |  |  |                      temp_diff, max_power, Ku, Tu, Kp * PID_PARAM_BASE, | 
					
						
							|  |  |  |                      Ki * PID_PARAM_BASE, Kd * PID_PARAM_BASE) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |     def check_busy(self, eventtime): | 
					
						
							|  |  |  |         if self.heating or len(self.peaks) < 12: | 
					
						
							|  |  |  |             return True | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |         self.heater.finish_auto_tune(self.old_control) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | # Tuning information test | 
					
						
							|  |  |  | ###################################################################### | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ControlBumpTest: | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |     def __init__(self, heater, old_control): | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         self.heater = heater | 
					
						
							|  |  |  |         self.old_control = old_control | 
					
						
							|  |  |  |         self.temp_samples = {} | 
					
						
							|  |  |  |         self.pwm_samples = {} | 
					
						
							|  |  |  |         self.state = 0 | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |     def set_pwm(self, read_time, value): | 
					
						
							|  |  |  |         self.pwm_samples[read_time + 2*REPORT_TIME] = value | 
					
						
							|  |  |  |         self.heater.set_pwm(read_time, value) | 
					
						
							|  |  |  |     def adc_callback(self, read_time, temp): | 
					
						
							|  |  |  |         self.temp_samples[read_time] = temp | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         if not self.state: | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |             self.set_pwm(read_time, 0.) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |             if len(self.temp_samples) >= 20: | 
					
						
							|  |  |  |                 self.state += 1 | 
					
						
							|  |  |  |         elif self.state == 1: | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |             if temp < self.heater.target_temp: | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |                 self.set_pwm(read_time, self.heater.max_power) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |                 return | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |             self.set_pwm(read_time, 0.) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |             self.state += 1 | 
					
						
							|  |  |  |         elif self.state == 2: | 
					
						
							| 
									
										
										
										
											2016-08-25 11:24:18 -04:00
										 |  |  |             self.set_pwm(read_time, 0.) | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |             if temp <= (self.heater.target_temp + AMBIENT_TEMP) / 2.: | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |                 self.dump_stats() | 
					
						
							|  |  |  |                 self.state += 1 | 
					
						
							|  |  |  |     def dump_stats(self): | 
					
						
							| 
									
										
										
										
											2016-08-24 15:16:02 -04:00
										 |  |  |         out = ["%.3f %.1f %d" % (time, temp, self.pwm_samples.get(time, -1.)) | 
					
						
							|  |  |  |                for time, temp in sorted(self.temp_samples.items())] | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         f = open("/tmp/heattest.txt", "wb") | 
					
						
							|  |  |  |         f.write('\n'.join(out)) | 
					
						
							|  |  |  |         f.close() | 
					
						
							|  |  |  |     def check_busy(self, eventtime): | 
					
						
							|  |  |  |         if self.state < 3: | 
					
						
							|  |  |  |             return True | 
					
						
							| 
									
										
										
										
											2017-06-22 12:09:55 -04:00
										 |  |  |         self.heater.finish_auto_tune(self.old_control) | 
					
						
							| 
									
										
										
										
											2016-05-25 11:37:40 -04:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2017-04-29 13:57:02 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | def add_printer_objects(printer, config): | 
					
						
							|  |  |  |     if config.has_section('heater_bed'): | 
					
						
							|  |  |  |         printer.add_object('heater_bed', PrinterHeater( | 
					
						
							|  |  |  |             printer, config.getsection('heater_bed'))) |