How to: IDEX Part Cooling Fans - Quick and Dirty (Klipper)

The Problem

I was recently working on a Klipper conversion for my MakerGear M3 IDEX when I made a discovery. Unlike Marlin which more or less has multi part cooling fan controls built-in, Klipper only supports a single part cooling fan. Well thats not fair to say, it "does" support more via [fan_generic] and [output_pin] definitions. However there is no real native support via the M106 command. In M106 P0 S255 the "P" value is never used. If you try to add fans via fan_generic or output_pin then you run into the new issue of either using a completely different command to control your fans which means time to get familiar with gcode post processing on your favorite slicer OR being forwarded guides with 100's of lines of macros needed to add support for a second part cooling fan. 

To be fair the people that built out these very robust macros for working around this problem (and a few other things also at once) did put in a lot of effort and were willing to share their source code with the rest of us freely. 
One of a few very good references: Tri-Zero

Alternatively, you can just do things the lazy way like many of the budge Chinese brands do and just drive both part cooling fans at the same time. This adds additional wear and tear to a fan that might otherwise only get light use and it adds to the noise floor of your 3D Printer. Who wants to deal with both of those? I sure don't.

The Solution (kinda?)

Replace the default M106 macro in Klipper with a more robust one!
Original credit: https://github.com/Klipper3d/klipper/issues/2174#issuecomment-613730313

This work around does work!
M106 now properly functions correctly with P flags.
But there is still a draw back to this. You still have to do some tinkering in your preferred slicer and often setup post processing or special scripts to make sure your triggering the correct part cooling fan. By default the slicer will simply use M106 S255 so it wont usually include the P to enable your second extruders part cooling fan, generally referred to as M106 P1 S255.

The Solution Re-Worked

Lets rework the solution to make it more robust! If you can do everything on the firmware side it allows for much more flexibility in your choice of slicer and allows for a much less complex profile configuration on said slicer. With that said this is only the "fan" portion of the equation. This does not include fan speed control across tool change handling. Maybe Ill cover that in a later blog.

Start by setting up your fans

While there might be a cleaner way to do this, this was "Quick and Dirty" and did exactly as I needed it to do.
Pay special attention to the fan naming, this is important to follow as the section later very much needs those values to be this way.

[output_pin fanextruder]
pin: PA0      ;Set this to the correct PIN on your mainboard
pwm: True
cycle_time: 0.0100
hardware_pwm: false
value: 0.05
scale: 255
shutdown_value: 0.0

[output_pin fanextruder1]
pin: PC8      ;Set this to the correct PIN on your mainboard
pwm: True
cycle_time: 0.0100
hardware_pwm: false
value: 0.05
scale: 255
shutdown_value: 0.0

Now lets change the M106 macro

[gcode_macro M106]
gcode:
    {% if params.P is defined %}
      {% if params.S is defined %}
      {% set activetool = params.P|default(0)|int %}

       {% if activetool == 0 %}
        SET_PIN PIN=fanextruder VALUE={params.S|int}
       {% else %}
        SET_PIN PIN=fanextruder{params.P|int} VALUE={params.S|int}
       {% endif %}

      {% else %}
       {% if activetool == 0 %}
        SET_PIN PIN=fanextruder VALUE=255
       {% else %}
        SET_PIN PIN=fanextruder{params.P|int} VALUE=255
       {% endif %}  
      {% endif %}

    {% else %}
      {% if params.S is defined %}
       {% if activetool == 0 %}
        SET_PIN PIN=fanextruder VALUE={params.S|int}
       {% else %}
        SET_PIN PIN=fan{printer.toolhead.extruder} VALUE={params.S|int}
       {% endif %}
      {% else %}
       {% if activetool == 0 %}
        SET_PIN PIN=fanextruder VALUE=255
       {% else %}
        SET_PIN PIN=fan{printer.toolhead.extruder} VALUE=255        
       {% endif %}
      {% endif %}
    {% endif %}

What does this all do?

Simply stated it allows M106 P# S### to work as you would expected. But it also adds support for M106 S### for the active extruder. So if there is no adjusted post processing in the slicer to support the P value. When setting M106, it will apply it to the current active extruders part cooling fan.

Short comings

There is a draw back to this macro that is still not handled and it comes from the slicer side (or firmware?) pending how you look at it. Your typical slicer does not set fan speeds between tool changes, unless you have modified the tool change gcode for that slicer to do so. This means fan speed changes happen during various features or layers not the tool change, so the parked extruder will have its fan still running at its last known speed. The new extruder may or may not have its fan on yet and might not be at the correct speed if it is on until the next instance of M106 in the gcode, which might or might not ever come up again. The appropriate handling for these conditions would come from the Tool change macros section of the config, not the M106 section. So that was not included in this guide.

Conclusion - Final Thoughts

While this does work very well, its not perfect. There is many ways to improve this configuration. The issue with this approach means that in Mainsail or Fluidd your part cooling fans will have funky looking names. This is a required side effect of using this config. It would be possible to clean up the names on the interface but this would require a more robust version of this macro. But as the intention of this macro was to be Quick and Dirty and more of a proof of concept, that wasn't a goal of this project.

I don't consider myself an experienced programmer. I am just one of those people that can stumble their way across code and sometimes make something that works. I know there is better code handling that could be used to further optimize this code and make it shorter. As that is outside of my skills at this time, my goal was to make it work as simply as possible.