diff options
Diffstat (limited to 'klippy/extras/heaters.py')
-rw-r--r-- | klippy/extras/heaters.py | 275 |
1 files changed, 172 insertions, 103 deletions
diff --git a/klippy/extras/heaters.py b/klippy/extras/heaters.py index fce3c49a..ce06955e 100644 --- a/klippy/extras/heaters.py +++ b/klippy/extras/heaters.py @@ -12,11 +12,12 @@ import os, logging, threading KELVIN_TO_CELSIUS = -273.15 MAX_HEAT_TIME = 3.0 -AMBIENT_TEMP = 25. -PID_PARAM_BASE = 255. +AMBIENT_TEMP = 25.0 +PID_PARAM_BASE = 255.0 MAX_MAINTHREAD_TIME = 5.0 QUELL_STALE_TIME = 7.0 + class Heater: def __init__(self, config, sensor): self.printer = config.get_printer() @@ -24,63 +25,69 @@ class Heater: self.short_name = short_name = self.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.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', 1., above=0.) - self.inv_smooth_time = 1. / self.smooth_time - self.verify_mainthread_time = -999. + "min_extrude_temp", 170.0, 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.0 or is_fileoutput + self.max_power = config.getfloat("max_power", 1.0, above=0.0, maxval=1.0) + self.smooth_time = config.getfloat("smooth_time", 1.0, above=0.0) + self.inv_smooth_time = 1.0 / self.smooth_time + self.verify_mainthread_time = -999.0 self.lock = threading.Lock() - self.last_temp = self.smoothed_temp = self.target_temp = 0. - self.last_temp_time = 0. + self.last_temp = self.smoothed_temp = self.target_temp = 0.0 + self.last_temp_time = 0.0 # pwm caching - self.next_pwm_time = 0. - self.last_pwm_value = 0. + self.next_pwm_time = 0.0 + self.last_pwm_value = 0.0 # Setup control algorithm sub-class - algos = {'watermark': ControlBangBang, 'pid': ControlPID} - algo = config.getchoice('control', algos) + 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') - 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) + heater_pin = config.get("heater_pin") + ppins = self.printer.lookup_object("pins") + self.mcu_pwm = ppins.setup_pin("pwm", heater_pin) + pwm_cycle_time = config.getfloat( + "pwm_cycle_time", 0.100, above=0.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.load_object(config, "verify_heater %s" % (short_name,)) self.printer.load_object(config, "pid_calibrate") gcode = self.printer.lookup_object("gcode") - gcode.register_mux_command("SET_HEATER_TEMPERATURE", "HEATER", - short_name, self.cmd_SET_HEATER_TEMPERATURE, - desc=self.cmd_SET_HEATER_TEMPERATURE_help) - self.printer.register_event_handler("klippy:shutdown", - self._handle_shutdown) + gcode.register_mux_command( + "SET_HEATER_TEMPERATURE", + "HEATER", + short_name, + self.cmd_SET_HEATER_TEMPERATURE, + desc=self.cmd_SET_HEATER_TEMPERATURE_help, + ) + self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown) + def set_pwm(self, read_time, value): - if self.target_temp <= 0. or read_time > self.verify_mainthread_time: - value = 0. - if ((read_time < self.next_pwm_time or not self.last_pwm_value) - and abs(value - self.last_pwm_value) < 0.05): + if self.target_temp <= 0.0 or read_time > self.verify_mainthread_time: + value = 0.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 self.mcu_pwm.set_pwm(pwm_time, value) - #logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])", + # 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) + def temperature_callback(self, read_time, temp): with self.lock: time_diff = read_time - self.last_temp_time @@ -88,49 +95,62 @@ class Heater: 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.) + adj_time = min(time_diff * self.inv_smooth_time, 1.0) 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) + self.can_extrude = self.smoothed_temp >= self.min_extrude_temp + # logging.debug("temp: %.3f %f = %f", read_time, temp) + def _handle_shutdown(self): - self.verify_mainthread_time = -999. + self.verify_mainthread_time = -999.0 + # External commands def get_name(self): return self.name + 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)) + % (degrees, self.min_temp, self.max_temp) + ) with self.lock: self.target_temp = degrees + def get_temp(self, eventtime): est_print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) quell_time = est_print_time - QUELL_STALE_TIME with self.lock: if self.last_temp_time < quell_time: - return 0., self.target_temp + return 0.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) + 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. + self.target_temp = 0.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): est_print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) if not self.printer.is_shutdown(): @@ -139,20 +159,30 @@ class Heater: 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.short_name, target_temp, last_temp, last_pwm_value) + is_active = target_temp or last_temp > 50.0 + return is_active, "%s: target=%.0f temp=%.1f pwm=%.3f" % ( + self.short_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 last_pwm_value = self.last_pwm_value - return {'temperature': round(smoothed_temp, 2), 'target': target_temp, - 'power': last_pwm_value} + return { + "temperature": round(smoothed_temp, 2), + "target": target_temp, + "power": last_pwm_value, + } + cmd_SET_HEATER_TEMPERATURE_help = "Sets a heater temperature" + def cmd_SET_HEATER_TEMPERATURE(self, gcmd): - temp = gcmd.get_float('TARGET', 0.) - pheaters = self.printer.lookup_object('heaters') + temp = gcmd.get_float("TARGET", 0.0) + pheaters = self.printer.lookup_object("heaters") pheaters.set_temperature(self, temp) @@ -160,47 +190,52 @@ class Heater: # 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.max_delta = config.getfloat("max_delta", 2.0, above=0.0) self.heating = False + def temperature_update(self, read_time, temp, target_temp): - if self.heating and temp >= target_temp+self.max_delta: + if self.heating and temp >= target_temp + self.max_delta: self.heating = False - elif not self.heating and temp <= target_temp-self.max_delta: + 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.) + self.heater.set_pwm(read_time, 0.0) + def check_busy(self, eventtime, smoothed_temp, target_temp): - return smoothed_temp < target_temp-self.max_delta + return smoothed_temp < target_temp - self.max_delta ###################################################################### # Proportional Integral Derivative (PID) control algo ###################################################################### -PID_SETTLE_DELTA = 1. -PID_SETTLE_SLOPE = .1 +PID_SETTLE_DELTA = 1.0 +PID_SETTLE_SLOPE = 0.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.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() - self.temp_integ_max = 0. + self.temp_integ_max = 0.0 if self.Ki: self.temp_integ_max = self.heater_max_power / self.Ki self.prev_temp = AMBIENT_TEMP - self.prev_temp_time = 0. - self.prev_temp_deriv = 0. - self.prev_temp_integ = 0. + self.prev_temp_time = 0.0 + self.prev_temp_deriv = 0.0 + self.prev_temp_integ = 0.0 + def temperature_update(self, read_time, temp, target_temp): time_diff = read_time - self.prev_temp_time # Calculate change of temperature @@ -208,17 +243,18 @@ class ControlPID: 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 + 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)) + temp_integ = max(0.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", + 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)) + bounded_co = max(0.0, min(self.heater_max_power, co)) self.heater.set_pwm(read_time, bounded_co) # Store state for next measurement self.prev_temp = temp @@ -226,16 +262,20 @@ class ControlPID: 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) + 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() @@ -247,30 +287,40 @@ class PrinterHeaters: self.available_monitors = [] self.has_started = self.have_load_sensors = False self.printer.register_event_handler("klippy:ready", self._handle_ready) - self.printer.register_event_handler("gcode:request_restart", - self.turn_off_all_heaters) + 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 = 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) - gcode.register_command("TEMPERATURE_WAIT", self.cmd_TEMPERATURE_WAIT, - desc=self.cmd_TEMPERATURE_WAIT_help) + gcode.register_command( + "TEMPERATURE_WAIT", + self.cmd_TEMPERATURE_WAIT, + desc=self.cmd_TEMPERATURE_WAIT_help, + ) + def load_config(self, config): self.have_load_sensors = True # Load default temperature sensors - pconfig = self.printer.lookup_object('configfile') + pconfig = self.printer.lookup_object("configfile") dir_name = os.path.dirname(__file__) - filename = os.path.join(dir_name, 'temperature_sensors.cfg') + filename = os.path.join(dir_name, "temperature_sensors.cfg") try: dconfig = pconfig.read_config(filename) except Exception: logging.exception("Unable to load temperature_sensors.cfg") raise config.error("Cannot load config '%s'" % (filename,)) - for c in dconfig.get_prefix_sections(''): + for c in dconfig.get_prefix_sections(""): self.printer.load_object(dconfig, c.get_name()) + 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: @@ -282,46 +332,60 @@ class PrinterHeaters: 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,)) + raise self.printer.config_error("Unknown heater '%s'" % (heater_name,)) return self.heaters[heater_name] + def setup_sensor(self, config): if not self.have_load_sensors: self.load_config(config) - sensor_type = config.get('sensor_type') + 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,)) + "Unknown temperature sensor '%s'" % (sensor_type,) + ) return self.sensor_factories[sensor_type](config) + def register_sensor(self, config, psensor, gcode_id=None): self.available_sensors.append(config.get_name()) if gcode_id is None: - gcode_id = config.get('gcode_id', 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,)) + "G-Code sensor id %s already registered" % (gcode_id,) + ) self.gcode_id_to_sensor[gcode_id] = psensor + def register_monitor(self, config): self.available_monitors.append(config.get_name()) + def get_status(self, eventtime): - return {'available_heaters': self.available_heaters, - 'available_sensors': self.available_sensors, - 'available_monitors': self.available_monitors} - def turn_off_all_heaters(self, print_time=0.): + return { + "available_heaters": self.available_heaters, + "available_sensors": self.available_sensors, + "available_monitors": self.available_monitors, + } + + def turn_off_all_heaters(self, print_time=0.0): for heater in self.heaters.values(): - heater.set_temp(0.) + heater.set_temp(0.0) + cmd_TURN_OFF_HEATERS_help = "Turn off all heaters" + def cmd_TURN_OFF_HEATERS(self, gcmd): 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 = [] @@ -332,6 +396,7 @@ class PrinterHeaters: if not out: return "T:0" return " ".join(out) + def cmd_M105(self, gcmd): # Get Extruder Temperature reactor = self.printer.get_reactor() @@ -339,9 +404,10 @@ class PrinterHeaters: did_ack = gcmd.ack(msg) if not did_ack: gcmd.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: + if self.printer.get_start_args().get("debugoutput") is not None: return toolhead = self.printer.lookup_object("toolhead") gcode = self.printer.lookup_object("gcode") @@ -350,24 +416,26 @@ class PrinterHeaters: 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.) + eventtime = reactor.pause(eventtime + 1.0) + def set_temperature(self, heater, temp, wait=False): - toolhead = self.printer.lookup_object('toolhead') + toolhead = self.printer.lookup_object("toolhead") toolhead.register_lookahead_callback((lambda pt: None)) heater.set_temp(temp) if wait and temp: self._wait_for_temperature(heater) + cmd_TEMPERATURE_WAIT_help = "Wait for a temperature on a sensor" + def cmd_TEMPERATURE_WAIT(self, gcmd): - sensor_name = gcmd.get('SENSOR') + sensor_name = gcmd.get("SENSOR") if sensor_name not in self.available_sensors: raise gcmd.error("Unknown sensor '%s'" % (sensor_name,)) - min_temp = gcmd.get_float('MINIMUM', float('-inf')) - max_temp = gcmd.get_float('MAXIMUM', float('inf'), above=min_temp) - if min_temp == float('-inf') and max_temp == float('inf'): - raise gcmd.error( - "Error on 'TEMPERATURE_WAIT': missing MINIMUM or MAXIMUM.") - if self.printer.get_start_args().get('debugoutput') is not None: + min_temp = gcmd.get_float("MINIMUM", float("-inf")) + max_temp = gcmd.get_float("MAXIMUM", float("inf"), above=min_temp) + if min_temp == float("-inf") and max_temp == float("inf"): + raise gcmd.error("Error on 'TEMPERATURE_WAIT': missing MINIMUM or MAXIMUM.") + if self.printer.get_start_args().get("debugoutput") is not None: return if sensor_name in self.heaters: sensor = self.heaters[sensor_name] @@ -382,7 +450,8 @@ class PrinterHeaters: return print_time = toolhead.get_last_move_time() gcmd.respond_raw(self._get_temp(eventtime)) - eventtime = reactor.pause(eventtime + 1.) + eventtime = reactor.pause(eventtime + 1.0) + def load_config(config): return PrinterHeaters(config) |