From 4dffbb070c7abf89e720be68e3b51c0813a14357 Mon Sep 17 00:00:00 2001 From: Zeb Hering Date: Fri, 28 Aug 2026 22:33:38 -0700 Subject: [PATCH] Let the fan settle downward on a re-assert A symmetric deadband catches the fan on the way up and holds it there: on iz-pve0 under load, 17 of 20 samples ran at 38% while the curve wanted 34-36%, and nothing would bring it down until the request fell a full 8 points. Damping was turning into a permanently louder machine. A re-assert may now settle the speed downward to the current request. Upward moves still require a full deadband crossing, so the drift this was built to prevent cannot come back - covered by a test. Verified under a live migration: settled 38 -> 36 on the first re-assert, then one change in ten minutes while the request wandered 36-42, CPU stable at 66-69c and disks a degree cooler. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 7 +++++++ fan_speed.sh | 18 +++++++++++++----- monitor.sh | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3c51db4..fca70a7 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,13 @@ settle — measurably worse than stock. Measured on iz-pve0: stock iDRAC held on minutes off the same signal. A request for full speed is never held back, and the held value is re-pushed every `FAN_REASSERT_INTERVAL` in case the BMC forgets it. +A deadband alone makes a machine permanently louder: it catches the fan on the way up and +then nothing brings it down until the request falls a full deadband. Measured on `iz-pve0` +under load, 17 of 20 samples ran at 38% while the curve wanted 34-36%. So a re-assert - +and only a re-assert - is allowed to settle the speed **downward** to what is actually +wanted. Upward moves still need a full deadband crossing, which is what stops this +reintroducing the drift it was built to prevent. + Note that iDRAC's own profile regulates to a target *RPM* (closed loop, rock steady), while manual control sets a *PWM* percentage (open loop, so measured RPM still wanders a percent or two at a fixed setpoint). That residual is inherent to manual control, not diff --git a/fan_speed.sh b/fan_speed.sh index 2ea7472..b79e983 100755 --- a/fan_speed.sh +++ b/fan_speed.sh @@ -117,6 +117,14 @@ selftest() { # leak is what let the setpoint drift 32 -> 36 -> 31 on iz-pve0. check "re-assert re-pushes held value" 32 "$(next_fan_speed 34 32)" + echo "settling downward (damping must not make the machine permanently louder):" + check "holds above the want between re-asserts" 38 "$(settle_fan_speed 34 38 0)" + check "settles down to the want on re-assert" 34 "$(settle_fan_speed 34 38 1)" + check "small rise still held on re-assert" 38 "$(settle_fan_speed 42 38 1)" + check "real rise still adopted" 46 "$(settle_fan_speed 46 38 1)" + # The old upward leak: a re-assert must never drift the setpoint up. + check "no upward drift on re-assert" 32 "$(settle_fan_speed 36 32 1)" + echo "trend verdict (load is not a fault - only cooling that cannot keep up is):" TREND_SAMPLES=5 HIGH_FAN_SPEED=50 @@ -293,12 +301,12 @@ one_pass() { # Hold the current speed unless the change is worth making - see the deadband # note in monitor.sh. Chasing every degree is what made this louder than stock. - local target now held="" - target=$(next_fan_speed "$DECIMAL_CURRENT_FAN_SPEED" "$APPLIED_FAN_SPEED") - [ "$target" = "$DECIMAL_CURRENT_FAN_SPEED" ] || held=" held, want $DECIMAL_CURRENT_FAN_SPEED" + local target now due=0 held="" now=$(date +%s) - if [ "$target" != "$APPLIED_FAN_SPEED" ] || - [ $((now - APPLIED_FAN_SPEED_AT)) -ge "$FAN_REASSERT_INTERVAL" ]; then + [ $((now - APPLIED_FAN_SPEED_AT)) -ge "$FAN_REASSERT_INTERVAL" ] && due=1 + target=$(settle_fan_speed "$DECIMAL_CURRENT_FAN_SPEED" "$APPLIED_FAN_SPEED" "$due") + [ "$target" = "$DECIMAL_CURRENT_FAN_SPEED" ] || held=" held, want $DECIMAL_CURRENT_FAN_SPEED" + if [ "$target" != "$APPLIED_FAN_SPEED" ] || [ "$due" = 1 ]; then apply_user_fan_control "$target" APPLIED_FAN_SPEED=$target APPLIED_FAN_SPEED_AT=$now diff --git a/monitor.sh b/monitor.sh index b3ec59b..badb470 100644 --- a/monitor.sh +++ b/monitor.sh @@ -97,6 +97,25 @@ next_fan_speed() { fi } +# settle_fan_speed +# +# A symmetric deadband catches the fan on the way up and then holds it there: +# measured on iz-pve0 under load, 17 of 20 samples ran at 38% while the curve +# wanted 34-36%, and nothing would bring it down until the request fell a full +# deadband. That is how damping turns into a permanently louder machine. +# +# So on a re-assert - and only then - let the speed settle DOWNWARD to what is +# actually wanted. Upward moves still require a full deadband crossing, so this +# cannot bring back the leak that let the setpoint drift 32 -> 36 -> 31. +settle_fan_speed() { + local wanted=$1 applied=$2 due=$3 target + target=$(next_fan_speed "$wanted" "$applied") + if [ "$due" = 1 ] && [ -n "$applied" ] && [ "$wanted" -lt "$target" ]; then + target=$wanted + fi + echo "$target" +} + ALERT_EMAIL=${ALERT_EMAIL:-Servers@ntfy1.izebra.xyz} ALERT_COOLDOWN=${ALERT_COOLDOWN:-3600} STATE_DIR=${STATE_DIR:-/root/fan_speed/state}