Big Picture - Klipper’s Fan Control


With the Klipper development goals for 2024 the intention was formulated to improve the fan controlling possibilities in Klipper.

Today’s state:

The Big Picture

This topic is intended to serve as a collection of use cases that are relevant in 3D printing today. It should be a collection of ideas and concepts of what is needed while being agnostic to a potential future solution.

:warning: Important:

  • This thread makes no claims whatsoever that anything listed here or below will ever be implemented.
  • Equally makes no claims about when and how this might happen.

Use Cases


  • Support of water-cooled hotends: The “new cool kid on the block” are water-cooled hotends, which require a radiator that reduces the temperature via a fan.
  • Support of air filters: Controlling filters like Nevermore and similar devices
  • Chamber heaters: Controlling the temperature within the printer’s enclosure. Catering for intake, exhaust and recirculation
  • IDEX: Flexible control of multiple part cooling fans attach to different hotends

Process controls

  • PID / Fan Curves / Bang-Bang steering: Different control principles depending on the application needs
  • Normalizing Fan Power: Make sure that upper and lower bounds of fan speeds keeps a linear control between 0% and 100%
  • Hysteresis: Introduce a “dead-band” to avoid oscillation around set-points
  • Direction Control: Use fans for heating, cooling and recirculation purposes
  • “Combined Objects”: Flexibly combine heaters and sensors with fans
  • Slope Control: Control fans according to rate of temperature changes
  • Always On: Allow fans to always run at a minimum speed, but to speed up when needed
  • Fan Safety: Create an error when system critical fans fail (requires fans with a tachometer signal)

As this list is not complete, please add your use cases below and explain, why you think it would benefit Klipper and its users.


I was literally just working on this yesterday and was going to post it when done to see if it had any takers on usability.

I think the PID in temperature_fan is kind of clunky and confusing to users since the PID can’t control the process variable (the temperature) directly, it can only control the fan and try it’s best to hit the set point.

I was working on a few additions to temperature fan specifically to take care of a few issues I’ve seen in PRs and read about on the forum.


Adding a minimum temperature cut off as optional for people who want quieter operation. This would be in addition to, or can optionally take the place of, the min_speed per the users choice. Most likely with a warning if the cut off temp is within a certain % of the max temp for safety.

Add new control options to temp_fan called “slope” and “curve”

With slope, you declare the slope type you want and the number of points on the curve and it will generate a fan curve for you. More points = more smooth transition but at the cost of a more varying and possibly annoying fan speed, less points = more “jumps” but potentially a quieter experience.

I would limit the number of points to a certain max so it doesn’t blow up the code trying to curve fit a huge number of points. I need to test different setups but I was thinking more than 10 points is excessive for temp change points.

Prelim I’m working on “Linear”, "Exponential’, “Log” and I’m debating between “Ramp” or “Step”. The picture below isn’t in English but it gives the general idea of how it would work.

In the code behind the scenes it would apply interpolation to smooth out the ranges and curve fit so less technical saavy users don’t have to worry about much. To me this is MUCH more intuitive than trying to tune a PID that can’t directly control the process variable.

Example config:

[temperature_fan SB2040_Fan]
pin: sb2040:gpio15
sensor_mcu: sb2040
sensor_type: temperature_mcu
cycle_time: 0.010
hardware_pwm: False
min_temp: 0
max_temp: 85
min_temp_cutoff: 30
control: slope
slope: linear
points: 6

For users who wanted just a BIT more control I was thinking of implementing a “curve” option for them to define their own fan curve and then eventually implement a visual chart in Mainsail/Fluidd to make it easy to define for themselves.

Prelim idea was something similar to this:

[temperature_fan SB2040_Fan]
pin: sb2040:gpio15
sensor_mcu: sb2040
sensor_type: temperature_mcu
cycle_time: 0.010
hardware_pwm: False
min_temp: 0
max_temp: 85
min_temp_cutoff: 30
min_speed: .1
temp_rng1: 40
speed_rng1: .25
temp_rng2: 50
speed_rng2: .6
temp_rng3: 65
speed_rng3: 1

So temp range 30 to 39c … Use the min speed
40-49: 25% of max power
50 to 64: 60% power
Above 65: Max power
and of course if it’s below 30 it’s at room temp anyways so just turn off the fan

Of course there would be some hysteresis around these points so the fan wasn’t blipping on and off annoyingly if the temp fluctuated around one of the set points.

The Mainsail/Fluidd interface I was thinking something similar to this…

Where users could click on the fan and in the pop up box set their curve and when they save it will use the existing “SAVE_CONFIG” functionality to save their fan curve for that fan.

So far these are my rough draft ideas and something I’ve already started working on.

Any feedback, criticism, ideas, name calling or moral support is more than welcome.

Nice! Thanks for working on it.

I can only recommend to closely align with @koconnor before investing more work into it.
Apart from “as few bugs as possible”, most likely Klipper’s most important development philosophy is:

Maybe your work could be the foundation for some “fan refactoring” but this surely depends on your and @koconnor alignment / willingness to work on it. :+1:
I am just the janitor here :laughing:

1 Like

Thanks and I agree, I’d like to try to refactor all the fans into a more concise setup maybe we can have a singular (or minimal at least) fan class that is as flexible and simple as possible (but also backwards compatible).

Seeing as how fans are a big part of 3d printing that would make more sense to look at the use cases like you’ve said here, and then build a solid and flexible framework.

This might be wishful thinking but then maybe we can do the same with probes… sensors… etc.

That might end up as part of the overall plan for how “Plugins” might possibly work.

without “engrafting” change upon change in an existing base

That’s kind of what I see when I learn more about the Klipper source, you can tell where functions were added and some more “tacked on” over time. There seems to be quite a bit of opportunity for simplification and/or refactoring to avoid duplication.

Like you said, This is all dependent on how @koconnor wants to approach it, or if he even does. I’m a HUGE proponent of the K-I-S-S method.

To stir up this discussion I was taking a look at the pull requests to see what could be merged together into one “Fan Fix” solution.

Going to try to explain them a bit for anyone following this as a sort of “TL;DR” for where things are at. Sineos has the key points up top. This just provides a little more detail for anyone to say “Oh, I need that!” or provide additional use cases/ideas.

I will throw out another thought of mine first…

Why do we need:

  • A heater_fan that turns on when an associated heater is on
  • A controller_fan that turns on when an associated heater/stepper driver is on
  • A temperature_fan that is enabled when a sensor is above a set temp

The only difference I see between controller fan and heater fan is controller fan has an idle timeout and heater fan defaults to full on when power fails.

These seem like they could be combined and streamlined to be more flexible.
I’m open to suggestions/being proven wrong though.

On to the PRs…

Normalising Fan PWM power

This PR request deals with an attempt to “normalize” PWM fan power so that “50% duty cycle” = “50% fan speed”. I’m honestly not sure how that would be achieved as every fan responds to PWM a bit different.

A few thoughts on that would be…

  • Implement PID in the base fan class for those that have a fan with a tachometer and then have an auto-tune to closely model the actual fan curve (start at full power, measure that RPM, then apply various duty cycles and see how the fan responds RPM wise). That shouldn’t be OVERLY difficult to implement but I’m not sure how many people are using tach fans.

  • Apply a type of dithering to smooth out the fan response and maybe “average out” the general rpm around the duty cycle point. Don’t know if that would actually work or if it would be detrimental to fan lifetime.

  • Allow users to define a custom duty cycle to fan speed curve similar to how custom thermistor. Not sure how value added this is or if users would actually use it.

heater_fan: added hysteresis

Exactly what it sounds like, All fans should have hysteresis if they don’t already. Or at least sensor smoothing. If it’s not already “good enough” for most users maybe we could add in a “smoothing factor” for fans to allow users to fine tune how quickly they want their fan to respond to temp changes.

fan: Support multiple printer fans

This PR is requesting to allow multiple fans for part cooling fans for IDEX printers. I’m honestly scratching my head at this one a little bit because I don’t have an IDEX printer. Is this not something a slicer should control? Will need additional discussion from IDEX users on the barriers with this one.

controller_fan: add ability to watch temperature_sensors too

I don’t quite understand this one either, It seems as though this user is trying to turn a controller fan into a temperature_fan. This might already be covered and the person just might not understand how to set it up. Maybe someone else has some ideas about this or can explain if I’m missing some point.

heater_fan: log a warning if pin is non among initial pins

I really couldn’t wrap my head around this one. The initial pins are setup through the mcu firmware code as far as I can tell so I’m not sure what this person is alluding to or asking. As far as I can tell klipper (the klipper running on the Pi) doesn’t do anything with initial pins. I might be mistaken or missing something on this one.

Update to have working PID option

This one has a misleading title and the person is trying to implement functionality breaking changes. Essentially they want the fan to turn off at a certain temp. I’ve already implemented a “min_temp_shutoff” config option in my fan class described above and I think that covers this situation. This person is trying to do it in the PID section of temperature_fan but that would be extremely confusing to users.

Thanks for digging deeper!

TBH, except for inspiration, I would not look too closely at the existing PRs. It will drag you down a rabbit hole of edge cases, and you end up with a lot of ifs, whens, and buts associated with multiple settings.

I would propose to come up with a kind of specification of what a 3D printer fan control should ideally be capable of and then work from there.

If I were to write a high-level specification for this, then a first draft would look like:

Basic Requirements:

  • Fans shall be able to support heating and cooling, i.e. offer on OR off at a minimum target, respectively on OR off at a maximum target
  • Fans shall be able to react to various sensor values, including but not limited to temperature, humidity, VOC etc
  • Fan control shall allow on/off, linear and PWM control. (I’m not sure that a PID control for fans make any sense or is just an hard-to-manage overkill)
  • Fans shall not oscillate around the set-points / target values
  • User facing fan speed shall be linearly scaled between off_below and max_power without considering potential non-linear fan PWM curves. This avoids user confusion that a max_power of 0.5 and a fan_speed of 0.5 effectively results in a speed of 0.25
  • Fans shall be able to be tied to various printer conditions / states, including but not limited to the state of heaters, steppers
  • Fans shall be able to constantly run at a defined minimum speed regardless of sensor value or printer condition
  • Fans with a tachometer signal shall be able to raise an error conditions, if no signal is received
  • Part fan and hotend fan shall be kept as is. They are well defined and should not require additional functionality. Changing them might negatively impact user experience and lot of configuration.
  • Existing fan settings shall be kept, except being overridden by the above or made obsolete by the above
  • Controller fan is TBD. Could it be implemented “generic” with above requirements, e.g. not needing an own setting / section?

Application - Water-cooled hotends:

Should not require additional features that are not already covered under the basic requirements

Application – Air Filters:

Should not require additional features that are not already covered under the basic requirements

Application – Chamber Heaters

Should not require additional features that are not already covered under the basic requirements

Application – IDEX

  • Multiple independent part fans that are associated with own extruders shall be possible
  • Switching the extruder shall transparently switch the governing part fan

Since I generally know just enough to be dangerous, take everything above with a grain of salt. I make no claim that the above is comprehensive or makes any sense.
Since I’m not a developer either, that’s all easy for me to say. But that’s the privilege of the proletarian user.

I think we’re on the same page and have the same thoughts. I was thinking about it a bit more this morning as well. I might be able to put together a “first pass rough draft” implementation this weekend and post it for review/testing.

Then as the discussion progresses I can add/remove things as needed.

I realize Kevin may nix the entire thing and that’s fine, It also helps me get more familiar with the Klipper code and is good practice in general. Python isn’t my go to language so I’m still learning it’s quirks.

1 Like

Well, it would be great to reach an alignment also with @koconnor to avoid limiting it to a “leraning experience”. Hope he will chime in with his view.

On a sidenote:
In the “specification” above, Kevin’s initial intend is missing: To extend the fan’s capabilities with a templating system as already available for LEDs. Nice example here.

I’m not sure how many items of the above would profit / being obsolete by this. In any case, it is more targeted at power users and requires in-depth understanding of Klipper and Klipper macros.
It is my personal view that “regular use-cases” should rather be hard-coded settings that can be well documented and understood also by regular users.

I reached out to Kevin about something separate and he said he’s traveling this weekend so it may be next week before he can reply.

As for that LED templating… I can only speak for myself but Klipper macros with jinja2 are an esoteric lost language of dark magic and mystical powers. It’s essentially like learning a new programming language which falls into 3 categories in my opinion…

  • Those who are curious and driven and take the time to learn it
  • Those who know how to program already and don’t want to try to figure out a NEW syntax/quirks
  • People who just want things to work “out of the box” and will never take the time to learn

With the last part, They’ll probably just copy and paste other peoples and come here when it doesn’t work.

That being said! It would be ultra cool to explore having some more flexibility with commands outside of typical G-Code so that people could handle some of the edge cases without asking for things to be in the base code (though I’m sure they will).

Right now I’m identifying with the less tech savvy users because for some reason I can get klipper to read a fan tach signal right and it’s getting frustrating. Might be the fan, but I tried another and IT’S not working either. Bleh.

1 Like

Couldn’t agree more. Still, the macro system is great and offers unparalleled flexibility for those who know how to handle the dark magic (a while back I had a small humorous take on it).
I’m often amazed at what people come up with.

And yes, I also perceive the copying of macros without understanding (mostly also not needing them) as an issue

1 Like

I suggest “Mutiplier Fan”
(useful when you speed-up or down your print and need to resize all M106 commands)

When you speed up or slow down your print, it’s (unless I’m way off base somwhere) reading your gcode faster which has the m106 commands built in. So it should already be adjusting your cooling but I guess it doesn’t really “multiply” it.

How often are you changing your printer speed by hand that much, and why?

As for M106 not being supported for multiple fans, I see why Kevin was so hesitant, it will require a complete rewrite of the configuration section as far as I can tell. I’ve been trying to figure out a way to do it and maintain backwards compatibility.

If you set the printer speed to 50%, all the M106s in the gcode (Orca sets the fan for multiple line types) remain the same.

I see what you’re getting at, The question still stands though, how often are you changing your print speed by hand? Why don’t you just slice it with a slower movement speed?

I can understand changing it when you’re testing things out but, by definition, that’s just testing and not your normal everyday prints.

Still kind of an interesting concept.

General question to any readers, specifically those that use multiple extruders or multiple part cooling fans.

But first some background:

The RepRap M106 Gcode (change part cooling fan speed) definition is

Pnnn Fan number (optional, defaults to 0)
Snnn Fan speed ( 0.0 to 1.0)

As we all know, Klipper has never supported a fan number in the M106 command. Digging into it more, like I stated, I can see why Kevin was hesitant to correct that.
The way Klipper currently loads objects when the config file is parsed, for there to be multiple objects they have to be “prefixed” (Actually suffixed, the code calls it a prefix, but whatever).

In other words [fan] would need to be [fan 0] or [fan 1].
But due to the way Klipper works, this would immediately throw an error for anyone using the older [fan] config. Which immediately breaks backwards compatibility.

There have been “fixes” for this, namely [fan_generic] but that requires jumping through hoops with gcode macros as far as I can tell because slicers will default to using M106 per the RepRap standard.


There MAY be a way to fix this.

I say MAY because this is me tinkering and probably 90%+ chance Kevin will nix it or postpone it until the next major numbered Klipper release.That and I haven’t tested it yet, I wanted to get a feel for how important this is to everyone.

A poll might be the easiest way to get an idea of that.

  • Fixing M106 is manditory. I use, or will use in the future, multiple cooling fans/extruders.
  • I only use one cooling fan and that’s all I will probably ever use So it doesn’t matter to me.
  • I only use one fan, but I think it’s important that Klipper supports all the commands from slicers with all their parameters (within reason).
  • Fans? Who needs cooling?
0 voters

If I’m mistaken about Klipper functionality or anything I said above, please correct me.

Going by klipper/config/sample-idex.cfg at master · Klipper3d/klipper · GitHub would it be possible to do something like:

# The definition for the primary extruder

# Definition for the secondary carriage and extruder1


# By definition [fan] is the part fan belonging to the [extruder]

# Following the logic of [extruder1], it should be

Since Klipper should know which extruder is active it should be able to transparently control the matching fan without any need for some M106 shenanigans

Maybe I’m overlooking some implications but this would more feel like “the way of the Klipper”

The problem is… (which is better defined in detail at the link below)…

Code Overview -Adding a host module

The Klippy host code has a dynamic module loading capability. If a config section named “[my_module]” is found in the printer config file then the software will automatically attempt to load the python module klippy/extras/ . This module system is the preferred method for adding new functionality to Klipper.

Hence if you do [extruder1] then Klipper will automatically try to find the file… Which obviously doesn’t exist. So it will throw an error.

Edit: I’m wrong about extruders, I didn’t know there was dual carriage functionality.
Change my example to [fan] and [fan1]

Double hence my comment that I understand better now why Kevin was hesitant to want to implement these changes people keep asking for. It would take rewriting the entire config system and totally shift how klipper loads modules and printer objects. Which is like… literally the first thing it does… So you’re risking breaking Klipper altogether.

I have a few ideas for “Workarounds” but I’ll need to most likely try a few before I find one that might actually work. So I totally understand from Kevin’s point of view it was “Do I sit down and devote time to figuring out how to completely rewrite the configuration system in Klipper and possibly not make any progress. or do I deal with the massive amount of questions and PR in Github and work on things that I KNOW will work?”

He’s a one man army.

(mostly, I know others have programmed other portions of the core of Klipper, not diminishing their contributions but Kevin is the “face” of Klipper development).

So I figured since I’m just a random internet guy over here and not bogged down bearing the weight of greatness like Kevin, Maybe I can poke around and see if I can find a way to implement some changes in the config system to support multiples of certain things, while at the same time not breaking the entire system.

Possible Solutions?

On the flip side, Yes, I agree and that’s how I would want it to be in your example.

One of the things I was thinking was trying to extend out the way Klipper stores the printer objects and dynamically adjust to multiples of certain things.

Let’s say for example… The M106 command which per RepRap takes a P# for fan number.


pin: PB0
hardware_pwm: False
kick_start_time: 0
off_below: 0.05

Klipper loads this like normal, no issues and M106 P0 refers to this fan by default which is what it currently does.


pin: PB0
hardware_pwm: False
kick_start_time: 0
off_below: 0.05

pin: PA1
hardware_pwm: False
kick_start_time: 0
off_below: 0.05

In this situation Klipper overwrites the first fan with the second one it finds (I just tested to be sure). Obviously this isn’t what we want…

New Scenario 2 (The “right way” in what I envision):

pin: PB0
hardware_pwm: False
kick_start_time: 0
off_below: 0.05

pin: PA1
*slicer_fan_number: 1* #M106 defaults to 0 as first fan, This would be the 2nd fan
hardware_pwm: False
kick_start_time: 0
off_below: 0.05

Klipper would dynamically load these and keep track of the instances of [fan] and allow storing more than one IF DEFINED PROPERLY

In this situation above, Klipper would by default assign the first [fan] as fan “0” and the second fan as fan “1” because it’s defined as the fan the slicer will refer to as 1

New Scenario 2: (The “wrong way”)

pin: PB0
hardware_pwm: False
kick_start_time: 0
off_below: 0.05

pin: PA1
hardware_pwm: False
kick_start_time: 0
off_below: 0.05

Where as previously Klipper just overwrote the first one with the second one, This will now throw an configuration error. Something like “Multiple fans defined under [fan], must define slicer fan number for any fans after the first.”

Obviously the config reference would need to be updated to reflect this.

This is the easiest way I can think of to allow M106 to work, and since M106 is THE default fan speed function in Slicers. I think its kind of necessary. Unless we want to tell every Slicer dev group out there to re-write their slicers to support Klippers way of doing things.

Again! I don’t have multiple part printing fans so maybe there is something I’m missing regarding this. I’m 100% okay with being wrong, I actually welcome it.


There was a lot of different suggestions in this thread, so it’s hard for me to give concise feedback.

Some high level thoughts:

  1. My main plan right now for fans is to extend the “template” system to fans, output pins, and pwm pins. This should allow one to supply a math formula for the fan speed. Most of what’s been discussed over the last few years have been various tweaks to the fan speeds, and hopefully a generic fan math formula system will meet those requirements.
  2. Admittedly, Jinja2 isn’t the greatest way to specify math formulas. Maybe in the longer term we’ll have some sort of “plugin system” that could make that base functionality more approachable as a set of high-level plugins… Not sure.
  3. As to extending M106 - for what it is worth, I’m very leery of adding new functionality to old g-code commands. This type of functionality can already be achieved by using human readable macro commands (eg, SET_ACTIVE_LAYER_FAN_SPEED) and [fan_generic] config sections. Again, I understand there are limitations to Jinja2, but I’m not sure “W31RD GC0D3” is an appropriate solution to that.


Just before considering my PR about PID temperature fan as misleading and extremely confusing to users, please explain the logic behind the present code and in which situation this code can be applied to a temperature fan supposed to hold a reference temperature with a kind of PID control.
Present implementation of the ‘PID’ control is extremely confusing to me. Please explain with an example what is the present use of the code.
Whatever the control, it can’t try to hold a temperature without stopping the fan completely below the reference temperature.
My version of the code applies to the control of a radiator fan for a water-cooled printer and to the control of a passively heated chamber (heated by the bed)., which are 2 relatively common cases.