diff --git a/README.md b/README.md index cb86c7508..053992343 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ If I want my printer to light itself on fire, I should be able to make my printe - [configfile: recursive globs](https://github.com/DangerKlippers/danger-klipper/pull/200) / ([klipper#6375](https://github.com/Klipper3d/klipper/pull/6375)) +- [temperature_fan: curve control algorithm](https://github.com/DangerKlippers/danger-klipper/pull/193) + If you're feeling adventurous, take a peek at the extra features in the bleeding-edge branch [feature documentation](docs/Bleeding_Edge.md) and [feature configuration reference](docs/Config_Reference_Bleeding_Edge.md): diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index b43bd0676..c6f4cfa66 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -3337,6 +3337,30 @@ information. # The default is False. ``` +``` +control: curve +#points: +# 50.0, 0.0 +# 55.0, 0.5 +# A user might defne a list of points which consist of a temperature with +# it's associated fan speed (temp, fan_speed). +# The target_temp value defines the temperature at which the fan will run +# at full speed. +# The algorithm will use linear interpolation to get the fan speeds +# between two points (if one has defined 0.0 for 50° and 1.0 for 60° the +# fan would run with 0.5 at 55°) +#cooling_hysteresis: 0.0 +# define the temperature hysteresis for lowering the fan speed +# (temperature differences to the last measured value that are lower than +# the hysteresis will not cause lowering of the fan speed) +#heating_hysteresis: 0.0 +# same as cooling_hysteresis but for increasing the fan speed, it is +# recommended to be left at 0 for safety reasons +#smooth_readings: 10 +# the amount of readings a median should be taken of to determine the fan +# speed at each update interval, the default is 10 +``` + ### [fan_generic] Manually controlled fan (one may define any number of sections with a diff --git a/klippy/extras/temperature_fan.py b/klippy/extras/temperature_fan.py index 4ad9a7bc9..782db2358 100644 --- a/klippy/extras/temperature_fan.py +++ b/klippy/extras/temperature_fan.py @@ -3,6 +3,8 @@ # Copyright (C) 2016-2020 Kevin O'Connor # # This file may be distributed under the terms of the GNU GPLv3 license. +import statistics + from . import fan KELVIN_TO_CELSIUS = -273.15 @@ -41,7 +43,11 @@ def __init__(self, config): maxval=self.max_temp, ) self.target_temp = self.target_temp_conf - algos = {"watermark": ControlBangBang, "pid": ControlPID} + algos = { + "watermark": ControlBangBang, + "pid": ControlPID, + "curve": ControlCurve, + } algo = config.getchoice("control", algos) self.control = algo(self, config) self.next_speed_time = 0.0 @@ -89,6 +95,7 @@ def get_status(self, eventtime): status = self.fan.get_status(eventtime) status["temperature"] = round(self.last_temp, 2) status["target"] = self.target_temp + status["control"] = self.control.get_type() return status def is_adc_faulty(self): @@ -101,8 +108,9 @@ def is_adc_faulty(self): ) def cmd_SET_TEMPERATURE_FAN_TARGET(self, gcmd): - temp = gcmd.get_float("TARGET", self.target_temp_conf) - self.set_temp(temp) + temp = gcmd.get_float("TARGET", None) + if temp is not None and self.control.get_type() == "curve": + raise gcmd.error("Setting Target not supported for control curve") min_speed = gcmd.get_float("MIN_SPEED", self.min_speed) max_speed = gcmd.get_float("MAX_SPEED", self.max_speed) if min_speed > max_speed: @@ -112,6 +120,7 @@ def cmd_SET_TEMPERATURE_FAN_TARGET(self, gcmd): ) self.set_min_speed(min_speed) self.set_max_speed(max_speed) + self.set_temp(self.target_temp_conf if temp is None else temp) def set_temp(self, degrees): if degrees and (degrees < self.min_temp or degrees > self.max_temp): @@ -167,6 +176,9 @@ def temperature_callback(self, read_time, temp): read_time, self.temperature_fan.get_max_speed() ) + def get_type(self): + return "watermark" + ###################################################################### # Proportional Integral Derivative (PID) control algo @@ -231,6 +243,120 @@ def temperature_callback(self, read_time, temp): if co == bounded_co: self.prev_temp_integ = temp_integ + def get_type(self): + return "pid" + + +class ControlCurve: + def __init__(self, temperature_fan, config, controlled_fan=None): + self.temperature_fan = temperature_fan + self.controlled_fan = ( + temperature_fan if controlled_fan is None else controlled_fan + ) + self.points = [] + points = config.getlists( + "points", seps=(",", "\n"), parser=float, count=2 + ) + for temp, pwm in points: + current_point = [temp, pwm] + if current_point is None: + continue + if len(current_point) != 2: + raise temperature_fan.printer.config_error( + "Point needs to have exactly one temperature and one speed " + "value." + ) + if current_point[0] > temperature_fan.target_temp: + raise temperature_fan.printer.config_error( + "Temperature in point can not exceed target temperature." + ) + if current_point[0] < temperature_fan.min_temp: + raise temperature_fan.printer.config_error( + "Temperature in point can not fall below min_temp." + ) + if current_point[1] > temperature_fan.get_max_speed(): + raise temperature_fan.printer.config_error( + "Speed in point can not exceed max_speed." + ) + if current_point[1] < temperature_fan.get_min_speed(): + raise temperature_fan.printer.config_error( + "Speed in point can not fall below min_speed." + ) + self.points.append(current_point) + self.points.append( + [temperature_fan.target_temp, temperature_fan.get_max_speed()] + ) + if len(self.points) < 2: + raise temperature_fan.printer.config_error( + "At least two points need to be defined for curve in " + "temperature_fan." + ) + self.points.sort(key=lambda p: p[0]) + last_point = [temperature_fan.min_temp, temperature_fan.get_min_speed()] + for point in self.points: + if point[1] < last_point[1]: + raise temperature_fan.printer.config_error( + "Points with higher temperatures have to have higher or " + "equal speed than points with lower temperatures." + ) + last_point = point + self.cooling_hysteresis = config.getfloat("cooling_hysteresis", 0.0) + self.heating_hysteresis = config.getfloat("heating_hysteresis", 0.0) + self.smooth_readings = config.getint("smooth_readings", 10, minval=1) + self.stored_temps = [] + for i in range(self.smooth_readings): + self.stored_temps.append(0.0) + self.last_temp = 0.0 + + def temperature_callback(self, read_time, temp): + current_temp, target_temp = self.temperature_fan.get_temp(read_time) + temp = self.smooth_temps(temp) + if temp >= target_temp: + self.temperature_fan.set_speed( + read_time, self.temperature_fan.get_max_speed() + ) + return + below = [ + self.temperature_fan.min_temp, + self.temperature_fan.get_min_speed(), + ] + above = [ + self.temperature_fan.max_temp, + self.temperature_fan.get_max_speed(), + ] + for config_temp in self.points: + if config_temp[0] < temp: + below = config_temp + else: + above = config_temp + break + self.controlled_fan.set_speed( + read_time, self.interpolate(below, above, temp) + ) + + def interpolate(self, below, above, temp): + return ( + (below[1] * (above[0] - temp)) + (above[1] * (temp - below[0])) + ) / (above[0] - below[0]) + + def smooth_temps(self, current_temp): + if ( + self.last_temp - self.cooling_hysteresis + <= current_temp + <= self.last_temp + self.heating_hysteresis + ): + temp = self.last_temp + else: + temp = current_temp + self.last_temp = temp + for i in range(1, len(self.stored_temps)): + self.stored_temps[i] = self.stored_temps[i - 1] + self.stored_temps[0] = temp + return statistics.median(self.stored_temps) + + def get_type(self): + return "curve" + def load_config_prefix(config): return TemperatureFan(config) diff --git a/test/klippy/curve_control.cfg b/test/klippy/curve_control.cfg new file mode 100644 index 000000000..960126f7e --- /dev/null +++ b/test/klippy/curve_control.cfg @@ -0,0 +1,78 @@ +# Config for extruder testing +[stepper_x] +step_pin: PF0 +dir_pin: PF1 +enable_pin: !PD7 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PE5 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_y] +step_pin: PF6 +dir_pin: !PF7 +enable_pin: !PF2 +microsteps: 16 +rotation_distance: 40 +endstop_pin: ^PJ1 +position_endstop: 0 +position_max: 200 +homing_speed: 50 + +[stepper_z] +step_pin: PL3 +dir_pin: PL1 +enable_pin: !PK0 +microsteps: 16 +rotation_distance: 8 +endstop_pin: ^PD3 +position_endstop: 0.5 +position_max: 200 + +[extruder] +step_pin: PA4 +dir_pin: PA6 +enable_pin: !PA2 +microsteps: 16 +rotation_distance: 33.5 +nozzle_diameter: 0.500 +filament_diameter: 3.500 +heater_pin: PB4 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK5 +control: pid +pid_Kp: 22.2 +pid_Ki: 1.08 +pid_Kd: 114 +min_temp: 0 +max_temp: 210 + +[temperature_fan my_temp_fan] +pin: PB5 +sensor_type: EPCOS 100K B57560G104F +sensor_pin: PK6 +target_temp: 65.0 +control: curve +points: + 50.0, 0.0 + 55.0, 0.5 + 60.0, 1.0 +smooth_readings: 100 +cooling_hysteresis: 5.0 +heating_hysteresis: 0.0 +min_temp: 0 +max_temp: 200 +min_speed: 0.0 +max_speed: 1.0 + +[mcu] +serial: /dev/ttyACM0 + +[printer] +kinematics: cartesian +max_velocity: 300 +max_accel: 3000 +max_z_velocity: 5 +max_z_accel: 100 diff --git a/test/klippy/curve_control.test b/test/klippy/curve_control.test new file mode 100644 index 000000000..5bc0919cd --- /dev/null +++ b/test/klippy/curve_control.test @@ -0,0 +1,13 @@ +# Pid hot modify tests +DICTIONARY atmega2560.dict +CONFIG curve_control.cfg + +# Extrude only +G1 E5 +G1 E-2 +G1 E7 + +# Home and extrusion moves +G28 +G1 X20 Y20 Z1 +G1 X25 Y25 E7.5