Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

temperature_fan: Improved code quality of fan curve and fixed hysteresis #466

Merged
merged 1 commit into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3518,14 +3518,17 @@ control: curve
# 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)
# (in simple terms this setting offsets the fan curve when cooling down
# by the specified amount of degrees celsius. For example, if the
# hysteresis is set to 5°C, the fan curve will be moved by -5°C. This
# setting can be used to reduce the effects of quickly changing
# temperatures around a target temperature which would cause the fan to
# speed up and slow down repeatedly.)
#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
# This parameter is deprecated and should no longer be used.
```

### [fan_generic]
Expand Down
165 changes: 76 additions & 89 deletions klippy/extras/temperature_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +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

import numpy as np

from . import fan

Expand Down Expand Up @@ -248,106 +249,92 @@ def get_type(self):


class ControlCurve:
def __init__(self, temperature_fan, config, controlled_fan=None):
def __init__(self, temperature_fan, config):
self.temperature_fan = temperature_fan
self.controlled_fan = (
temperature_fan if controlled_fan is None else controlled_fan
)
self.curve_table = []
points = config.getlists(
"points", seps=(",", "\n"), parser=float, count=2

points = list(
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 temp > temperature_fan.target_temp:
raise temperature_fan.printer.config_error(
"Temperature in point can not exceed max_temp."
)
if temp < temperature_fan.min_temp:
raise temperature_fan.printer.config_error(
"Temperature in point can not fall below min_temp."
)
if pwm > temperature_fan.get_max_speed():
raise temperature_fan.printer.config_error(
"Speed in point can not exceed max_speed."
)
if pwm < temperature_fan.get_min_speed():
raise temperature_fan.printer.config_error(
"Speed in point can not fall below min_speed."
)
for _temp, _pwm in self.curve_table:
if _temp == temp:
raise temperature_fan.printer.config_error(
"Temperature can not exist twice in curve table."
)
self.curve_table.append(current_point)
if len(self.curve_table) < 2:
points.sort(key=lambda x: x[0])

if len(points) < 2:
raise temperature_fan.printer.config_error(
"At least two points need to be defined for curve in "
"At least two points must be defined for curve in "
"temperature_fan."
)
self.curve_table.sort(key=lambda p: p[0])
if any(len(point) != 2 for point in points):
raise temperature_fan.printer.config_error(
"A point must have exactly one temperature and one pwm setting "
"value."
)
if not all(points[i] <= points[i + 1] for i in range(len(points) - 1)):
raise temperature_fan.printer.config_error(
"The fan curve must be monotonically increasing."
)

temp_values, pwm_values = zip(*points)

if len(temp_values) > len(set(temp_values)):
raise temperature_fan.printer.config_error(
"Temperature may not exist twice in curve table."
)
if temp_values[-1] > temperature_fan.target_temp:
raise temperature_fan.printer.config_error(
"Temperature in point may not exceed target_temp."
)
if temp_values[0] < temperature_fan.min_temp:
raise temperature_fan.printer.config_error(
"Temperature in point may not fall below min_temp."
)
if pwm_values[-1] > temperature_fan.get_max_speed():
raise temperature_fan.printer.config_error(
"Speed in point may not exceed max_speed."
)
if pwm_values[0] < temperature_fan.get_min_speed():
raise temperature_fan.printer.config_error(
"Speed in point may not fall below min_speed."
)

if points[0][0] > temperature_fan.min_temp:
points.insert(0, (temperature_fan.min_temp, points[0][1]))
if points[-1][0] < temperature_fan.max_temp:
points.append((temperature_fan.max_temp, points[-1][1]))

self.smooth_readings = config.getint(
"smooth_readings", default=None, minval=0
)
if self.smooth_readings is not None:
config.deprecate("smooth_readings")

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)
NokkOnEffect marked this conversation as resolved.
Show resolved Hide resolved
self.stored_temps = []
for i in range(self.smooth_readings):
self.stored_temps.append(0.0)
self.last_temp = 0.0
self.curve_standard = np.array([*points]).transpose()
self.curve_heating = np.copy(self.curve_standard)
self.curve_cooling = np.copy(self.curve_standard)
self.curve_heating[0, :] += self.heating_hysteresis
self.curve_cooling[0, :] -= self.cooling_hysteresis

def temperature_callback(self, read_time, temp):
def _interpolate(below, above, temp):
return (
(below[1] * (above[0] - temp)) + (above[1] * (temp - below[0]))
) / (above[0] - below[0])

temp = self.smooth_temps(temp)
if temp <= self.curve_table[0][0]:
self.controlled_fan.set_speed(read_time, self.curve_table[0][1])
return
if temp >= self.curve_table[-1][0]:
self.controlled_fan.set_speed(read_time, self.curve_table[-1][1])
return

below = [
self.curve_table[0][0],
self.curve_table[0][1],
]
above = [
self.curve_table[-1][0],
self.curve_table[-1][1],
]
for config_temp in self.curve_table:
if config_temp[0] < temp:
below = config_temp
else:
above = config_temp
break
self.controlled_fan.set_speed(
read_time, _interpolate(below, above, temp)
current_speed = self.temperature_fan.last_speed_value
upper_temp = np.interp(
current_speed, self.curve_heating[1], self.curve_heating[0]
)
lower_temp = np.interp(
current_speed, self.curve_cooling[1], self.curve_cooling[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
if temp < lower_temp:
next_speed = np.interp(
temp, self.curve_cooling[0], self.curve_cooling[1]
)
elif temp > upper_temp:
next_speed = np.interp(
temp, self.curve_heating[0], self.curve_heating[1]
)
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)
next_speed = current_speed

self.temperature_fan.set_speed(read_time, next_speed)

def get_type(self):
return "curve"
Expand Down
1 change: 0 additions & 1 deletion test/klippy/curve_control.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ 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
Expand Down
Loading