aboutsummaryrefslogtreecommitdiffstats
path: root/klippy/extras/heaters.py
diff options
context:
space:
mode:
Diffstat (limited to 'klippy/extras/heaters.py')
-rw-r--r--klippy/extras/heaters.py275
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)