Skip to content

Commit

Permalink
Curve Fan Control (#193)
Browse files Browse the repository at this point in the history
* Initial Implementation

* Forgot min and max temp

* me being dumb

* Update curve_control.cfg

* Updated curve definition and docs

* fix tests
  • Loading branch information
Zeanon authored Apr 29, 2024
1 parent 5ab5eac commit 311bd58
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
24 changes: 24 additions & 0 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
132 changes: 129 additions & 3 deletions klippy/extras/temperature_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Copyright (C) 2016-2020 Kevin O'Connor <[email protected]>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import statistics

from . import fan

KELVIN_TO_CELSIUS = -273.15
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
78 changes: 78 additions & 0 deletions test/klippy/curve_control.cfg
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions test/klippy/curve_control.test
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 311bd58

Please sign in to comment.