From 17123889f698e2339049bd18e7ece01b9c53e892 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 25 Apr 2020 13:27:41 -0400 Subject: heaters: Make heater.py an "extras" module The heater logic is an independent module that does not need to be treated as part of the "core" klipper code. Signed-off-by: Kevin O'Connor --- klippy/extras/adc_temperature.py | 10 +- klippy/extras/bme280.py | 4 +- klippy/extras/controller_fan.py | 5 +- klippy/extras/heater_bed.py | 8 +- klippy/extras/heater_fan.py | 5 +- klippy/extras/heater_generic.py | 3 +- klippy/extras/heaters.py | 327 ++++++++++++++++++++++++++++++++++++ klippy/extras/homing_heaters.py | 8 +- klippy/extras/pid_calibrate.py | 6 +- klippy/extras/spi_temperature.py | 4 +- klippy/extras/temperature_fan.py | 5 +- klippy/extras/temperature_sensor.py | 5 +- klippy/extras/thermistor.py | 8 +- klippy/extras/verify_heater.py | 4 +- klippy/heater.py | 327 ------------------------------------ klippy/kinematics/extruder.py | 8 +- klippy/klippy.py | 4 +- 17 files changed, 373 insertions(+), 368 deletions(-) create mode 100644 klippy/extras/heaters.py delete mode 100644 klippy/heater.py diff --git a/klippy/extras/adc_temperature.py b/klippy/extras/adc_temperature.py index 0ec13776..f00ba778 100644 --- a/klippy/extras/adc_temperature.py +++ b/klippy/extras/adc_temperature.py @@ -273,7 +273,7 @@ PT1000 = [ def load_config(config): # Register default sensors - pheater = config.get_printer().lookup_object("heater") + pheaters = config.get_printer().try_load_module(config, "heaters") for sensor_type, params in [("AD595", AD595), ("AD8494", AD8494), ("AD8495", AD8495), @@ -282,17 +282,17 @@ def load_config(config): ("PT100 INA826", PT100)]: func = (lambda config, params=params: PrinterADCtoTemperature(config, LinearVoltage(config, params))) - pheater.add_sensor_factory(sensor_type, func) + pheaters.add_sensor_factory(sensor_type, func) for sensor_type, params in [("PT1000", PT1000)]: func = (lambda config, params=params: PrinterADCtoTemperature(config, LinearResistance(config, params))) - pheater.add_sensor_factory(sensor_type, func) + pheaters.add_sensor_factory(sensor_type, func) def load_config_prefix(config): if config.get("resistance1", None) is None: custom_sensor = CustomLinearVoltage(config) else: custom_sensor = CustomLinearResistance(config) - pheater = config.get_printer().lookup_object("heater") - pheater.add_sensor_factory(custom_sensor.name, custom_sensor.create) + pheaters = config.get_printer().try_load_module(config, "heaters") + pheaters.add_sensor_factory(custom_sensor.name, custom_sensor.create) diff --git a/klippy/extras/bme280.py b/klippy/extras/bme280.py index 0809ac6d..f5fba66e 100644 --- a/klippy/extras/bme280.py +++ b/klippy/extras/bme280.py @@ -198,5 +198,5 @@ class BME280: def load_config(config): # Register sensor - pheater = config.get_printer().lookup_object("heater") - pheater.add_sensor_factory("BME280", BME280) + pheaters = config.get_printer().try_load_module(config, "heaters") + pheaters.add_sensor_factory("BME280", BME280) diff --git a/klippy/extras/controller_fan.py b/klippy/extras/controller_fan.py index adf78ccc..ccd617a2 100644 --- a/klippy/extras/controller_fan.py +++ b/klippy/extras/controller_fan.py @@ -14,6 +14,7 @@ class ControllerFan: self.stepper_names = [] self.stepper_enable = self.printer.try_load_module(config, 'stepper_enable') + self.printer.try_load_module(config, 'heaters') self.heaters = [] self.fan = fan.PrinterFan(config) self.mcu = self.fan.mcu_fan.get_mcu() @@ -27,8 +28,8 @@ class ControllerFan: self.heater_name = config.get("heater", "extruder") self.last_on = self.idle_timeout def handle_ready(self): - pheater = self.printer.lookup_object('heater') - self.heaters = [pheater.lookup_heater(n.strip()) + pheaters = self.printer.lookup_object('heaters') + self.heaters = [pheaters.lookup_heater(n.strip()) for n in self.heater_name.split(',')] kin = self.printer.lookup_object('toolhead').get_kinematics() self.stepper_names = [s.get_name() for s in kin.get_steppers()] diff --git a/klippy/extras/heater_bed.py b/klippy/extras/heater_bed.py index c3505528..c2f56a99 100644 --- a/klippy/extras/heater_bed.py +++ b/klippy/extras/heater_bed.py @@ -7,8 +7,8 @@ class PrinterHeaterBed: def __init__(self, config): self.printer = config.get_printer() - pheater = self.printer.lookup_object('heater') - self.heater = pheater.setup_heater(config, 'B') + pheaters = self.printer.try_load_module(config, 'heaters') + self.heater = pheaters.setup_heater(config, 'B') self.get_status = self.heater.get_status self.stats = self.heater.stats # Register commands @@ -21,8 +21,8 @@ class PrinterHeaterBed: temp = gcode.get_float('S', params, 0.) self.heater.set_temp(temp) if wait and temp: - pheater = self.printer.lookup_object('heater') - pheater.wait_for_temperature(self.heater) + pheaters = self.printer.lookup_object('heaters') + pheaters.wait_for_temperature(self.heater) def cmd_M190(self, params): # Set Bed Temperature and Wait self.cmd_M140(params, wait=True) diff --git a/klippy/extras/heater_fan.py b/klippy/extras/heater_fan.py index 337dcc09..4a3ace84 100644 --- a/klippy/extras/heater_fan.py +++ b/klippy/extras/heater_fan.py @@ -10,6 +10,7 @@ PIN_MIN_TIME = 0.100 class PrinterHeaterFan: def __init__(self, config): self.printer = config.get_printer() + self.printer.try_load_module(config, 'heaters') self.printer.register_event_handler("klippy:ready", self.handle_ready) self.heater_name = config.get("heater", "extruder") self.heater_temp = config.getfloat("heater_temp", 50.0) @@ -18,8 +19,8 @@ class PrinterHeaterFan: self.mcu = self.fan.mcu_fan.get_mcu() self.fan_speed = config.getfloat("fan_speed", 1., minval=0., maxval=1.) def handle_ready(self): - pheater = self.printer.lookup_object('heater') - self.heaters = [pheater.lookup_heater(n.strip()) + pheaters = self.printer.lookup_object('heaters') + self.heaters = [pheaters.lookup_heater(n.strip()) for n in self.heater_name.split(',')] reactor = self.printer.get_reactor() reactor.register_timer(self.callback, reactor.NOW) diff --git a/klippy/extras/heater_generic.py b/klippy/extras/heater_generic.py index fbe13edc..564e4179 100644 --- a/klippy/extras/heater_generic.py +++ b/klippy/extras/heater_generic.py @@ -5,4 +5,5 @@ # This file may be distributed under the terms of the GNU GPLv3 license. def load_config_prefix(config): - return config.get_printer().lookup_object('heater').setup_heater(config) + pheaters = config.get_printer().try_load_module(config, 'heaters') + return pheaters.setup_heater(config) diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py new file mode 100644 index 00000000..29fb5d6e --- /dev/null +++ b/klippy/extras/heaters.py @@ -0,0 +1,327 @@ +# Tracking of PWM controlled heaters and their temperature control +# +# Copyright (C) 2016-2020 Kevin O'Connor +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import logging, threading + + +###################################################################### +# Heater +###################################################################### + +KELVIN_TO_CELSIUS = -273.15 +MAX_HEAT_TIME = 5.0 +AMBIENT_TEMP = 25. +PID_PARAM_BASE = 255. + +class Heater: + def __init__(self, config, sensor): + self.printer = config.get_printer() + self.gcode = self.printer.lookup_object("gcode") + self.name = config.get_name().split()[-1] + # Setup sensor + self.sensor = sensor + self.min_temp = config.getfloat('min_temp', minval=KELVIN_TO_CELSIUS) + self.max_temp = config.getfloat('max_temp', above=self.min_temp) + self.sensor.setup_minmax(self.min_temp, self.max_temp) + self.sensor.setup_callback(self.temperature_callback) + self.pwm_delay = self.sensor.get_report_time_delta() + # Setup temperature checks + self.min_extrude_temp = config.getfloat( + 'min_extrude_temp', 170., + minval=self.min_temp, maxval=self.max_temp) + is_fileoutput = (self.printer.get_start_args().get('debugoutput') + is not None) + self.can_extrude = self.min_extrude_temp <= 0. or is_fileoutput + self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.) + self.smooth_time = config.getfloat('smooth_time', 2., above=0.) + self.inv_smooth_time = 1. / self.smooth_time + self.lock = threading.Lock() + self.last_temp = self.smoothed_temp = self.target_temp = 0. + self.last_temp_time = 0. + # pwm caching + self.next_pwm_time = 0. + self.last_pwm_value = 0. + # Setup control algorithm sub-class + algos = {'watermark': ControlBangBang, 'pid': ControlPID} + algo = config.getchoice('control', algos) + self.control = algo(self, config) + # Setup output heater pin + heater_pin = config.get('heater_pin') + ppins = self.printer.lookup_object('pins') + if algo is ControlBangBang and self.max_power == 1.: + self.mcu_pwm = ppins.setup_pin('digital_out', heater_pin) + else: + self.mcu_pwm = ppins.setup_pin('pwm', heater_pin) + pwm_cycle_time = config.getfloat( + 'pwm_cycle_time', 0.100, above=0., maxval=self.pwm_delay) + self.mcu_pwm.setup_cycle_time(pwm_cycle_time) + self.mcu_pwm.setup_max_duration(MAX_HEAT_TIME) + # Load additional modules + self.printer.try_load_module(config, "verify_heater %s" % (self.name,)) + self.printer.try_load_module(config, "pid_calibrate") + self.gcode.register_mux_command( + "SET_HEATER_TEMPERATURE", "HEATER", self.name, + self.cmd_SET_HEATER_TEMPERATURE, + desc=self.cmd_SET_HEATER_TEMPERATURE_help) + def set_pwm(self, read_time, value): + 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 + return + pwm_time = read_time + self.pwm_delay + self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME + self.last_pwm_value = value + 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) + self.mcu_pwm.set_pwm(pwm_time, value) + def temperature_callback(self, read_time, temp): + with self.lock: + time_diff = read_time - self.last_temp_time + self.last_temp = temp + self.last_temp_time = read_time + self.control.temperature_update(read_time, temp, self.target_temp) + temp_diff = temp - self.smoothed_temp + adj_time = min(time_diff * self.inv_smooth_time, 1.) + self.smoothed_temp += temp_diff * adj_time + self.can_extrude = (self.smoothed_temp >= self.min_extrude_temp) + #logging.debug("temp: %.3f %f = %f", read_time, temp) + # External commands + def get_pwm_delay(self): + return self.pwm_delay + def get_max_power(self): + return self.max_power + def get_smooth_time(self): + return self.smooth_time + def set_temp(self, degrees): + if degrees and (degrees < self.min_temp or degrees > self.max_temp): + raise self.printer.command_error( + "Requested temperature (%.1f) out of range (%.1f:%.1f)" + % (degrees, self.min_temp, self.max_temp)) + with self.lock: + self.target_temp = degrees + def get_temp(self, eventtime): + print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) - 5. + with self.lock: + if self.last_temp_time < print_time: + return 0., self.target_temp + return self.smoothed_temp, self.target_temp + def check_busy(self, eventtime): + with self.lock: + return self.control.check_busy( + eventtime, self.smoothed_temp, self.target_temp) + def set_control(self, control): + with self.lock: + old_control = self.control + self.control = control + self.target_temp = 0. + return old_control + def alter_target(self, target_temp): + if target_temp: + target_temp = max(self.min_temp, min(self.max_temp, target_temp)) + self.target_temp = target_temp + def stats(self, eventtime): + with self.lock: + target_temp = self.target_temp + last_temp = self.last_temp + last_pwm_value = self.last_pwm_value + is_active = target_temp or last_temp > 50. + return is_active, '%s: target=%.0f temp=%.1f pwm=%.3f' % ( + self.name, target_temp, last_temp, last_pwm_value) + def get_status(self, eventtime): + with self.lock: + target_temp = self.target_temp + smoothed_temp = self.smoothed_temp + return {'temperature': smoothed_temp, 'target': target_temp} + cmd_SET_HEATER_TEMPERATURE_help = "Sets a heater temperature" + def cmd_SET_HEATER_TEMPERATURE(self, params): + temp = self.gcode.get_float('TARGET', params, 0.) + self.set_temp(temp) + + +###################################################################### +# Bang-bang control algo +###################################################################### + +class ControlBangBang: + def __init__(self, heater, config): + self.heater = heater + self.heater_max_power = heater.get_max_power() + self.max_delta = config.getfloat('max_delta', 2.0, above=0.) + self.heating = False + def temperature_update(self, read_time, temp, target_temp): + if self.heating and temp >= target_temp+self.max_delta: + self.heating = False + elif not self.heating and temp <= target_temp-self.max_delta: + self.heating = True + if self.heating: + self.heater.set_pwm(read_time, self.heater_max_power) + else: + self.heater.set_pwm(read_time, 0.) + def check_busy(self, eventtime, smoothed_temp, target_temp): + return smoothed_temp < target_temp-self.max_delta + + +###################################################################### +# Proportional Integral Derivative (PID) control algo +###################################################################### + +PID_SETTLE_DELTA = 1. +PID_SETTLE_SLOPE = .1 + +class ControlPID: + def __init__(self, heater, config): + self.heater = heater + self.heater_max_power = heater.get_max_power() + 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 + self.min_deriv_time = heater.get_smooth_time() + imax = config.getfloat('pid_integral_max', self.heater_max_power, + minval=0.) + self.temp_integ_max = imax / self.Ki + self.prev_temp = AMBIENT_TEMP + self.prev_temp_time = 0. + self.prev_temp_deriv = 0. + self.prev_temp_integ = 0. + def temperature_update(self, read_time, temp, target_temp): + time_diff = read_time - self.prev_temp_time + # 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 = 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 + co = self.Kp*temp_err + self.Ki*temp_integ - self.Kd*temp_deriv + #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) + bounded_co = max(0., min(self.heater_max_power, co)) + self.heater.set_pwm(read_time, bounded_co) + # Store state for next measurement + self.prev_temp = temp + self.prev_temp_time = read_time + self.prev_temp_deriv = temp_deriv + if co == bounded_co: + self.prev_temp_integ = temp_integ + def check_busy(self, eventtime, smoothed_temp, target_temp): + temp_diff = target_temp - smoothed_temp + return (abs(temp_diff) > PID_SETTLE_DELTA + or abs(self.prev_temp_deriv) > PID_SETTLE_SLOPE) + + +###################################################################### +# Sensor and heater lookup +###################################################################### + +class PrinterHeaters: + def __init__(self, config): + self.printer = config.get_printer() + self.sensor_factories = {} + self.heaters = {} + self.gcode_id_to_sensor = {} + self.available_heaters = [] + self.available_sensors = [] + self.has_started = False + self.printer.register_event_handler("klippy:ready", self._handle_ready) + self.printer.register_event_handler("gcode:request_restart", + self.turn_off_all_heaters) + # Register commands + gcode = self.printer.lookup_object('gcode') + gcode.register_command("TURN_OFF_HEATERS", self.cmd_TURN_OFF_HEATERS, + desc=self.cmd_TURN_OFF_HEATERS_help) + gcode.register_command("M105", self.cmd_M105, when_not_ready=True) + def add_sensor_factory(self, sensor_type, sensor_factory): + self.sensor_factories[sensor_type] = sensor_factory + def setup_heater(self, config, gcode_id=None): + heater_name = config.get_name().split()[-1] + if heater_name in self.heaters: + raise config.error("Heater %s already registered" % (heater_name,)) + # Setup sensor + sensor = self.setup_sensor(config) + # Create heater + self.heaters[heater_name] = heater = Heater(config, sensor) + self.register_sensor(config, heater, gcode_id) + self.available_heaters.append(config.get_name()) + return heater + def get_all_heaters(self): + return self.available_heaters + def lookup_heater(self, heater_name): + if heater_name not in self.heaters: + raise self.printer.config_error( + "Unknown heater '%s'" % (heater_name,)) + return self.heaters[heater_name] + def setup_sensor(self, config): + self.printer.try_load_module(config, "thermistor") + self.printer.try_load_module(config, "adc_temperature") + self.printer.try_load_module(config, "spi_temperature") + self.printer.try_load_module(config, "bme280") + sensor_type = config.get('sensor_type') + if sensor_type not in self.sensor_factories: + raise self.printer.config_error( + "Unknown temperature sensor '%s'" % (sensor_type,)) + return self.sensor_factories[sensor_type](config) + def register_sensor(self, config, psensor, gcode_id=None): + if gcode_id is None: + gcode_id = config.get('gcode_id', None) + if gcode_id is None: + return + if gcode_id in self.gcode_id_to_sensor: + raise self.printer.config_error( + "G-Code sensor id %s already registered" % (gcode_id,)) + self.gcode_id_to_sensor[gcode_id] = psensor + self.available_sensors.append(config.get_name()) + def get_status(self, eventtime): + return {'available_heaters': self.available_heaters, + 'available_sensors': self.available_sensors} + def turn_off_all_heaters(self, print_time=0.): + for heater in self.heaters.values(): + heater.set_temp(0.) + cmd_TURN_OFF_HEATERS_help = "Turn off all heaters" + def cmd_TURN_OFF_HEATERS(self, params): + self.turn_off_all_heaters() + # G-Code M105 temperature reporting + def _handle_ready(self): + self.has_started = True + def _get_temp(self, eventtime): + # Tn:XXX /YYY B:XXX /YYY + out = [] + if self.has_started: + for gcode_id, sensor in sorted(self.gcode_id_to_sensor.items()): + cur, target = sensor.get_temp(eventtime) + out.append("%s:%.1f /%.1f" % (gcode_id, cur, target)) + if not out: + return "T:0" + return " ".join(out) + def cmd_M105(self, params): + # Get Extruder Temperature + gcode = self.printer.lookup_object("gcode") + reactor = self.printer.get_reactor() + msg = self._get_temp(reactor.monotonic()) + did_ack = gcode.ack(msg) + if not did_ack: + gcode.respond_raw(msg) + def wait_for_temperature(self, heater): + # Helper to wait on heater.check_busy() and report M105 temperatures + if self.printer.get_start_args().get('debugoutput') is not None: + return + toolhead = self.printer.lookup_object("toolhead") + gcode = self.printer.lookup_object("gcode") + reactor = self.printer.get_reactor() + eventtime = reactor.monotonic() + while not self.printer.is_shutdown() and heater.check_busy(eventtime): + print_time = toolhead.get_last_move_time() + gcode.respond_raw(self._get_temp(eventtime)) + eventtime = reactor.pause(eventtime + 1.) + +def load_config(config): + return PrinterHeaters(config) diff --git a/klippy/extras/homing_heaters.py b/klippy/extras/homing_heaters.py index 6433869f..673b7f8d 100644 --- a/klippy/extras/homing_heaters.py +++ b/klippy/extras/homing_heaters.py @@ -19,12 +19,12 @@ class HomingHeaters: self.disable_heaters = [] self.steppers_needing_quiet = config.get("steppers", "") self.flaky_steppers = [] - self.pheater = self.printer.lookup_object('heater') + self.pheaters = self.printer.try_load_module(config, 'heaters') self.target_save = {} def handle_connect(self): # heaters to disable - all_heaters = self.pheater.get_all_heaters() + all_heaters = self.pheaters.get_all_heaters() self.disable_heaters = [n.strip() for n in self.heaters_to_disable.split(',')] if self.disable_heaters == [""]: @@ -56,14 +56,14 @@ class HomingHeaters: if not self.check_eligible(endstops): return for heater_name in self.disable_heaters: - heater = self.pheater.lookup_heater(heater_name) + heater = self.pheaters.lookup_heater(heater_name) self.target_save[heater_name] = heater.get_temp(0)[1] heater.set_temp(0.) def handle_homing_move_end(self, endstops): if not self.check_eligible(endstops): return for heater_name in self.disable_heaters: - heater = self.pheater.lookup_heater(heater_name) + heater = self.pheaters.lookup_heater(heater_name) heater.set_temp(self.target_save[heater_name]) def load_config(config): diff --git a/klippy/extras/pid_calibrate.py b/klippy/extras/pid_calibrate.py index 49ae3bb4..b5dbc773 100644 --- a/klippy/extras/pid_calibrate.py +++ b/klippy/extras/pid_calibrate.py @@ -18,9 +18,9 @@ class PIDCalibrate: heater_name = self.gcode.get_str('HEATER', params) target = self.gcode.get_float('TARGET', params) write_file = self.gcode.get_int('WRITE_FILE', params, 0) - pheater = self.printer.lookup_object('heater') + pheaters = self.printer.lookup_object('heaters') try: - heater = pheater.lookup_heater(heater_name) + heater = pheaters.lookup_heater(heater_name) except self.printer.config_error as e: raise self.gcode.error(str(e)) self.printer.lookup_object('toolhead').get_last_move_time() @@ -31,7 +31,7 @@ class PIDCalibrate: except self.printer.command_error as e: heater.set_control(old_control) raise - pheater.wait_for_temperature(heater) + pheaters.wait_for_temperature(heater) heater.set_control(old_control) if write_file: calibrate.write_file('/tmp/heattest.txt') diff --git a/klippy/extras/spi_temperature.py b/klippy/extras/spi_temperature.py index 74cf9505..606a317e 100644 --- a/klippy/extras/spi_temperature.py +++ b/klippy/extras/spi_temperature.py @@ -336,6 +336,6 @@ Sensors = { def load_config(config): # Register sensors - pheater = config.get_printer().lookup_object("heater") + pheaters = config.get_printer().try_load_module(config, "heaters") for name, klass in Sensors.items(): - pheater.add_sensor_factory(name, klass) + pheaters.add_sensor_factory(name, klass) diff --git a/klippy/extras/temperature_fan.py b/klippy/extras/temperature_fan.py index 6fc2893b..cebeee1f 100644 --- a/klippy/extras/temperature_fan.py +++ b/klippy/extras/temperature_fan.py @@ -18,10 +18,11 @@ class TemperatureFan: self.gcode = self.printer.lookup_object('gcode') self.min_temp = config.getfloat('min_temp', minval=KELVIN_TO_CELSIUS) self.max_temp = config.getfloat('max_temp', above=self.min_temp) - self.sensor = self.printer.lookup_object('heater').setup_sensor(config) + pheaters = self.printer.try_load_module(config, 'heaters') + self.sensor = pheaters.setup_sensor(config) self.sensor.setup_minmax(self.min_temp, self.max_temp) self.sensor.setup_callback(self.temperature_callback) - self.printer.lookup_object('heater').register_sensor(config, self) + pheaters.register_sensor(config, self) self.speed_delay = self.sensor.get_report_time_delta() self.max_speed = config.getfloat('max_speed', 1., above=0., maxval=1.) self.min_speed = config.getfloat('min_speed', 0.3, minval=0., maxval=1.) diff --git a/klippy/extras/temperature_sensor.py b/klippy/extras/temperature_sensor.py index 62b7afef..94779a85 100644 --- a/klippy/extras/temperature_sensor.py +++ b/klippy/extras/temperature_sensor.py @@ -9,14 +9,15 @@ KELVIN_TO_CELSIUS = -273.15 class PrinterSensorGeneric: def __init__(self, config): self.printer = config.get_printer() - self.sensor = self.printer.lookup_object('heater').setup_sensor(config) + pheaters = self.printer.try_load_module(config, 'heaters') + self.sensor = pheaters.setup_sensor(config) self.min_temp = config.getfloat('min_temp', KELVIN_TO_CELSIUS, minval=KELVIN_TO_CELSIUS) self.max_temp = config.getfloat('max_temp', 99999999.9, above=self.min_temp) self.sensor.setup_minmax(self.min_temp, self.max_temp) self.sensor.setup_callback(self.temperature_callback) - self.printer.lookup_object('heater').register_sensor(config, self) + pheaters.register_sensor(config, self) self.last_temp = 0. def temperature_callback(self, read_time, temp): self.last_temp = temp diff --git a/klippy/extras/thermistor.py b/klippy/extras/thermistor.py index f43c0a99..7e3aa3aa 100644 --- a/klippy/extras/thermistor.py +++ b/klippy/extras/thermistor.py @@ -116,12 +116,12 @@ Sensors = { def load_config(config): # Register default thermistor types - pheater = config.get_printer().lookup_object("heater") + pheaters = config.get_printer().try_load_module(config, "heaters") for sensor_type, params in Sensors.items(): func = (lambda config, params=params: PrinterThermistor(config, params)) - pheater.add_sensor_factory(sensor_type, func) + pheaters.add_sensor_factory(sensor_type, func) def load_config_prefix(config): thermistor = CustomThermistor(config) - pheater = config.get_printer().lookup_object("heater") - pheater.add_sensor_factory(thermistor.name, thermistor.create) + pheaters = config.get_printer().try_load_module(config, "heaters") + pheaters.add_sensor_factory(thermistor.name, thermistor.create) diff --git a/klippy/extras/verify_heater.py b/klippy/extras/verify_heater.py index a76646f5..8c5360d5 100644 --- a/klippy/extras/verify_heater.py +++ b/klippy/extras/verify_heater.py @@ -35,8 +35,8 @@ class HeaterCheck: if self.printer.get_start_args().get('debugoutput') is not None: # Disable verify_heater if outputting to a debug file return - pheater = self.printer.lookup_object('heater') - self.heater = pheater.lookup_heater(self.heater_name) + pheaters = self.printer.lookup_object('heaters') + self.heater = pheaters.lookup_heater(self.heater_name) logging.info("Starting heater checks for %s", self.heater_name) reactor = self.printer.get_reactor() self.check_timer = reactor.register_timer(self.check_event, reactor.NOW) diff --git a/klippy/heater.py b/klippy/heater.py deleted file mode 100644 index 4914c767..00000000 --- a/klippy/heater.py +++ /dev/null @@ -1,327 +0,0 @@ -# Printer heater support -# -# Copyright (C) 2016-2018 Kevin O'Connor -# -# This file may be distributed under the terms of the GNU GPLv3 license. -import logging, threading - - -###################################################################### -# Heater -###################################################################### - -KELVIN_TO_CELSIUS = -273.15 -MAX_HEAT_TIME = 5.0 -AMBIENT_TEMP = 25. -PID_PARAM_BASE = 255. - -class Heater: - def __init__(self, config, sensor): - self.printer = config.get_printer() - self.gcode = self.printer.lookup_object("gcode") - self.name = config.get_name().split()[-1] - # Setup sensor - self.sensor = sensor - self.min_temp = config.getfloat('min_temp', minval=KELVIN_TO_CELSIUS) - self.max_temp = config.getfloat('max_temp', above=self.min_temp) - self.sensor.setup_minmax(self.min_temp, self.max_temp) - self.sensor.setup_callback(self.temperature_callback) - self.pwm_delay = self.sensor.get_report_time_delta() - # Setup temperature checks - self.min_extrude_temp = config.getfloat( - 'min_extrude_temp', 170., - minval=self.min_temp, maxval=self.max_temp) - is_fileoutput = (self.printer.get_start_args().get('debugoutput') - is not None) - self.can_extrude = self.min_extrude_temp <= 0. or is_fileoutput - self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.) - self.smooth_time = config.getfloat('smooth_time', 2., above=0.) - self.inv_smooth_time = 1. / self.smooth_time - self.lock = threading.Lock() - self.last_temp = self.smoothed_temp = self.target_temp = 0. - self.last_temp_time = 0. - # pwm caching - self.next_pwm_time = 0. - self.last_pwm_value = 0. - # Setup control algorithm sub-class - algos = {'watermark': ControlBangBang, 'pid': ControlPID} - algo = config.getchoice('control', algos) - self.control = algo(self, config) - # Setup output heater pin - heater_pin = config.get('heater_pin') - ppins = self.printer.lookup_object('pins') - if algo is ControlBangBang and self.max_power == 1.: - self.mcu_pwm = ppins.setup_pin('digital_out', heater_pin) - else: - self.mcu_pwm = ppins.setup_pin('pwm', heater_pin) - pwm_cycle_time = config.getfloat( - 'pwm_cycle_time', 0.100, above=0., maxval=self.pwm_delay) - self.mcu_pwm.setup_cycle_time(pwm_cycle_time) - self.mcu_pwm.setup_max_duration(MAX_HEAT_TIME) - # Load additional modules - self.printer.try_load_module(config, "verify_heater %s" % (self.name,)) - self.printer.try_load_module(config, "pid_calibrate") - self.gcode.register_mux_command( - "SET_HEATER_TEMPERATURE", "HEATER", self.name, - self.cmd_SET_HEATER_TEMPERATURE, - desc=self.cmd_SET_HEATER_TEMPERATURE_help) - def set_pwm(self, read_time, value): - 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 - return - pwm_time = read_time + self.pwm_delay - self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME - self.last_pwm_value = value - 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) - self.mcu_pwm.set_pwm(pwm_time, value) - def temperature_callback(self, read_time, temp): - with self.lock: - time_diff = read_time - self.last_temp_time - self.last_temp = temp - self.last_temp_time = read_time - self.control.temperature_update(read_time, temp, self.target_temp) - temp_diff = temp - self.smoothed_temp - adj_time = min(time_diff * self.inv_smooth_time, 1.) - self.smoothed_temp += temp_diff * adj_time - self.can_extrude = (self.smoothed_temp >= self.min_extrude_temp) - #logging.debug("temp: %.3f %f = %f", read_time, temp) - # External commands - def get_pwm_delay(self): - return self.pwm_delay - def get_max_power(self): - return self.max_power - def get_smooth_time(self): - return self.smooth_time - def set_temp(self, degrees): - if degrees and (degrees < self.min_temp or degrees > self.max_temp): - raise self.printer.command_error( - "Requested temperature (%.1f) out of range (%.1f:%.1f)" - % (degrees, self.min_temp, self.max_temp)) - with self.lock: - self.target_temp = degrees - def get_temp(self, eventtime): - print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) - 5. - with self.lock: - if self.last_temp_time < print_time: - return 0., self.target_temp - return self.smoothed_temp, self.target_temp - def check_busy(self, eventtime): - with self.lock: - return self.control.check_busy( - eventtime, self.smoothed_temp, self.target_temp) - def set_control(self, control): - with self.lock: - old_control = self.control - self.control = control - self.target_temp = 0. - return old_control - def alter_target(self, target_temp): - if target_temp: - target_temp = max(self.min_temp, min(self.max_temp, target_temp)) - self.target_temp = target_temp - def stats(self, eventtime): - with self.lock: - target_temp = self.target_temp - last_temp = self.last_temp - last_pwm_value = self.last_pwm_value - is_active = target_temp or last_temp > 50. - return is_active, '%s: target=%.0f temp=%.1f pwm=%.3f' % ( - self.name, target_temp, last_temp, last_pwm_value) - def get_status(self, eventtime): - with self.lock: - target_temp = self.target_temp - smoothed_temp = self.smoothed_temp - return {'temperature': smoothed_temp, 'target': target_temp} - cmd_SET_HEATER_TEMPERATURE_help = "Sets a heater temperature" - def cmd_SET_HEATER_TEMPERATURE(self, params): - temp = self.gcode.get_float('TARGET', params, 0.) - self.set_temp(temp) - - -###################################################################### -# Bang-bang control algo -###################################################################### - -class ControlBangBang: - def __init__(self, heater, config): - self.heater = heater - self.heater_max_power = heater.get_max_power() - self.max_delta = config.getfloat('max_delta', 2.0, above=0.) - self.heating = False - def temperature_update(self, read_time, temp, target_temp): - if self.heating and temp >= target_temp+self.max_delta: - self.heating = False - elif not self.heating and temp <= target_temp-self.max_delta: - self.heating = True - if self.heating: - self.heater.set_pwm(read_time, self.heater_max_power) - else: - self.heater.set_pwm(read_time, 0.) - def check_busy(self, eventtime, smoothed_temp, target_temp): - return smoothed_temp < target_temp-self.max_delta - - -###################################################################### -# Proportional Integral Derivative (PID) control algo -###################################################################### - -PID_SETTLE_DELTA = 1. -PID_SETTLE_SLOPE = .1 - -class ControlPID: - def __init__(self, heater, config): - self.heater = heater - self.heater_max_power = heater.get_max_power() - 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 - self.min_deriv_time = heater.get_smooth_time() - imax = config.getfloat('pid_integral_max', self.heater_max_power, - minval=0.) - self.temp_integ_max = imax / self.Ki - self.prev_temp = AMBIENT_TEMP - self.prev_temp_time = 0. - self.prev_temp_deriv = 0. - self.prev_temp_integ = 0. - def temperature_update(self, read_time, temp, target_temp): - time_diff = read_time - self.prev_temp_time - # 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 = 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 - co = self.Kp*temp_err + self.Ki*temp_integ - self.Kd*temp_deriv - #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) - bounded_co = max(0., min(self.heater_max_power, co)) - self.heater.set_pwm(read_time, bounded_co) - # Store state for next measurement - self.prev_temp = temp - self.prev_temp_time = read_time - self.prev_temp_deriv = temp_deriv - if co == bounded_co: - self.prev_temp_integ = temp_integ - def check_busy(self, eventtime, smoothed_temp, target_temp): - temp_diff = target_temp - smoothed_temp - return (abs(temp_diff) > PID_SETTLE_DELTA - or abs(self.prev_temp_deriv) > PID_SETTLE_SLOPE) - - -###################################################################### -# Sensor and heater lookup -###################################################################### - -class PrinterHeaters: - def __init__(self, config): - self.printer = config.get_printer() - self.sensor_factories = {} - self.heaters = {} - self.gcode_id_to_sensor = {} - self.available_heaters = [] - self.available_sensors = [] - self.has_started = False - self.printer.register_event_handler("klippy:ready", self._handle_ready) - self.printer.register_event_handler("gcode:request_restart", - self.turn_off_all_heaters) - # Register commands - gcode = self.printer.lookup_object('gcode') - gcode.register_command("TURN_OFF_HEATERS", self.cmd_TURN_OFF_HEATERS, - desc=self.cmd_TURN_OFF_HEATERS_help) - gcode.register_command("M105", self.cmd_M105, when_not_ready=True) - def add_sensor_factory(self, sensor_type, sensor_factory): - self.sensor_factories[sensor_type] = sensor_factory - def setup_heater(self, config, gcode_id=None): - heater_name = config.get_name().split()[-1] - if heater_name in self.heaters: - raise config.error("Heater %s already registered" % (heater_name,)) - # Setup sensor - sensor = self.setup_sensor(config) - # Create heater - self.heaters[heater_name] = heater = Heater(config, sensor) - self.register_sensor(config, heater, gcode_id) - self.available_heaters.append(config.get_name()) - return heater - def get_all_heaters(self): - return self.available_heaters - def lookup_heater(self, heater_name): - if heater_name not in self.heaters: - raise self.printer.config_error( - "Unknown heater '%s'" % (heater_name,)) - return self.heaters[heater_name] - def setup_sensor(self, config): - self.printer.try_load_module(config, "thermistor") - self.printer.try_load_module(config, "adc_temperature") - self.printer.try_load_module(config, "spi_temperature") - self.printer.try_load_module(config, "bme280") - sensor_type = config.get('sensor_type') - if sensor_type not in self.sensor_factories: - raise self.printer.config_error( - "Unknown temperature sensor '%s'" % (sensor_type,)) - return self.sensor_factories[sensor_type](config) - def register_sensor(self, config, psensor, gcode_id=None): - if gcode_id is None: - gcode_id = config.get('gcode_id', None) - if gcode_id is None: - return - if gcode_id in self.gcode_id_to_sensor: - raise self.printer.config_error( - "G-Code sensor id %s already registered" % (gcode_id,)) - self.gcode_id_to_sensor[gcode_id] = psensor - self.available_sensors.append(config.get_name()) - def get_status(self, eventtime): - return {'available_heaters': self.available_heaters, - 'available_sensors': self.available_sensors} - def turn_off_all_heaters(self, print_time=0.): - for heater in self.heaters.values(): - heater.set_temp(0.) - cmd_TURN_OFF_HEATERS_help = "Turn off all heaters" - def cmd_TURN_OFF_HEATERS(self, params): - self.turn_off_all_heaters() - # G-Code M105 temperature reporting - def _handle_ready(self): - self.has_started = True - def _get_temp(self, eventtime): - # Tn:XXX /YYY B:XXX /YYY - out = [] - if self.has_started: - for gcode_id, sensor in sorted(self.gcode_id_to_sensor.items()): - cur, target = sensor.get_temp(eventtime) - out.append("%s:%.1f /%.1f" % (gcode_id, cur, target)) - if not out: - return "T:0" - return " ".join(out) - def cmd_M105(self, params): - # Get Extruder Temperature - gcode = self.printer.lookup_object("gcode") - reactor = self.printer.get_reactor() - msg = self._get_temp(reactor.monotonic()) - did_ack = gcode.ack(msg) - if not did_ack: - gcode.respond_raw(msg) - def wait_for_temperature(self, heater): - # Helper to wait on heater.check_busy() and report M105 temperatures - if self.printer.get_start_args().get('debugoutput') is not None: - return - toolhead = self.printer.lookup_object("toolhead") - gcode = self.printer.lookup_object("gcode") - reactor = self.printer.get_reactor() - eventtime = reactor.monotonic() - while not self.printer.is_shutdown() and heater.check_busy(eventtime): - print_time = toolhead.get_last_move_time() - gcode.respond_raw(self._get_temp(eventtime)) - eventtime = reactor.pause(eventtime + 1.) - -def add_printer_objects(config): - config.get_printer().add_object('heater', PrinterHeaters(config)) diff --git a/klippy/kinematics/extruder.py b/klippy/kinematics/extruder.py index c7884932..2921ef0a 100644 --- a/klippy/kinematics/extruder.py +++ b/klippy/kinematics/extruder.py @@ -11,12 +11,12 @@ class PrinterExtruder: self.printer = config.get_printer() self.name = config.get_name() shared_heater = config.get('shared_heater', None) - pheater = self.printer.lookup_object('heater') + pheaters = self.printer.try_load_module(config, 'heaters') gcode_id = 'T%d' % (extruder_num,) if shared_heater is None: - self.heater = pheater.setup_heater(config, gcode_id) + self.heater = pheaters.setup_heater(config, gcode_id) else: - self.heater = pheater.lookup_heater(shared_heater) + self.heater = pheaters.lookup_heater(shared_heater) self.stepper = stepper.PrinterStepper(config) self.nozzle_diameter = config.getfloat('nozzle_diameter', above=0.) filament_diameter = config.getfloat( @@ -167,7 +167,7 @@ class PrinterExtruder: heater = extruder.get_heater() heater.set_temp(temp) if wait and temp: - self.printer.lookup_object('heater').wait_for_temperature(heater) + self.printer.lookup_object('heaters').wait_for_temperature(heater) def cmd_M109(self, params): # Set Extruder Temperature and Wait self.cmd_M104(params, wait=True) diff --git a/klippy/klippy.py b/klippy/klippy.py index becbe113..d03e25a5 100644 --- a/klippy/klippy.py +++ b/klippy/klippy.py @@ -6,7 +6,7 @@ # This file may be distributed under the terms of the GNU GPLv3 license. import sys, os, optparse, logging, time, threading, collections, importlib import util, reactor, queuelogger, msgproto, homing -import gcode, configfile, pins, heater, mcu, toolhead +import gcode, configfile, pins, mcu, toolhead message_ready = "Printer is ready" @@ -123,7 +123,7 @@ class Printer: if self.bglogger is not None: pconfig.log_config(config) # Create printer components - for m in [pins, heater, mcu]: + for m in [pins, mcu]: m.add_printer_objects(config) for section_config in config.get_prefix_sections(''): self.try_load_module(config, section_config.get_name()) -- cgit v1.2.3-70-g09d2