# PID Controlled Exhaust Fan (Quiet!)

**URL:** <https://forum.sovol3d.com/t/pid-controlled-exhaust-fan-quiet/9025>\
**Category:** Sovol Zero\
**Tags:** klipper\
**Created:** [November 17, 2025, 4:28am UTC](https://forum.sovol3d.com/t/pid-controlled-exhaust-fan-quiet/9025 "2025-11-17T04:28:46Z")\
**Posts on this page:** 1\
**Showing post:** 15

<div class="post-metadata">

**Author:** ![tektalon](https://yyz2.discourse-cdn.com/flex030/user_avatar/forum.sovol3d.com/tektalon/32/5391_2.png) [@tektalon](https://forum.sovol3d.com/u/tektalon)\
**Post date:** [February 1, 2026, 7:44pm UTC](https://forum.sovol3d.com/t/pid-controlled-exhaust-fan-quiet/9025/15 "2026-02-01T19:44:58Z")

</div>

OMG

in my pain and desperation i turned to chatGPT and after arguing and denying all the wrong solutions i now have a workaround  
if i put SET\_FAN\_SPEED FAN=fan2 SPEED=0 right before the TEMPERATURE\_WAIT it works  
let’s ignore the fact that it is such an obvious approach that i could have tried it even without understanding why \>\<  
It seems to be a problem with the klipper resend loop during the wait command which somehow sends the wrong last fan2 state ?

here’s the long and as always way to chatty version of it

> ## The critical mechanism: _fan state replay during G-code blocking_
> 
> This is not documented, and it’s subtle.
> 
> ### What Klipper does internally
> 
> When a G-code command blocks (and `TEMPERATURE_WAIT` is one of the _very few_ that does):
> 
> 1. Klipper continues to run the reactor loop
> 
> 2. It periodically replays **last-known fan states** to the MCU
> 
> 3. This is done to keep MCU state synchronized while host G-code execution is paused
> 
> Normally, this is harmless.
> 
> However…
> 
> * * *
> 
> ## Why this hits **fan2 only**
> 
> Klipper tracks fan state in two layers:
> 
> - **Logical state** (host-side)
> 
> - **MCU pin state** (low-level)
> 
> If a fan was _ever_ written with a non-zero value in the current session — even briefly — that value becomes the “last-known” state.
> 
> During a blocking wait:
> 
> - Klipper re-sends that value
> 
> - Even if the logical state is currently zero
> 
> - **No G-code is emitted**
> 
> - No macro is involved
> 
> This is why:
> 
> - You cannot find it in config
> 
> - You cannot reproduce it outside `TEMPERATURE_WAIT`
> 
> - Emergency stop behaves differently (host disconnect → MCU reset → pin cleared)
> 
> * * *
> 
> ## The smoking gun: **fan2 is written once earlier in the script**
> 
> Not necessarily visibly.
> 
> Typical hidden writes:
> 
> - `M106` without `P` (defaults to fan0 but still updates fan table)
> 
> - `SET_FAN_SPEED FAN=fan2 SPEED=0` (yes, zero still registers)
> 
> - A macro that touches _all_ fans
> 
> - A conditional that evaluates differently during normal execution
> 
> Once Klipper has _ever_ touched `fan2`, it is eligible for replay.
> 
> * * *
> 
> ## How to prove this beyond doubt
> 
> Add **one line** before your `TEMPERATURE_WAIT`:
> 
> ```auto
> SET_FAN_SPEED FAN=fan2 SPEED=0
> 
> ```
> 
> Then restart Klipper and re-run your script.
> 
> If the fan **stops turning on** , this confirms fan-state replay.
> 
> Why?
> 
> - You overwrite the “last-known” cached state with zero
> 
> - The replay sends zero instead of the stale value

---

_[View the full topic](https://forum.sovol3d.com/t/pid-controlled-exhaust-fan-quiet/9025)._
