Source files / scripts · raw
fan-control.sh
#!/usr/bin/env bash # Drive a mainboard PWM fan header from the hottest GPU's temperature. # The board itself only regulates on CPU temperature. # # tersler-fan-control --discover list hwmon chips and PWM outputs # tersler-fan-control --test PWM sweep one output (e.g. nct6779/pwm2) to identify it # tersler-fan-control run the control loop (needs FAN_HWMON_NAME + FAN_PWM) # # Fails safe: on any error or exit it sets the fan to full speed and hands # control back to the board. set -euo pipefail # shellcheck source=lib.sh source "$(dirname "$(readlink -f "$0")")/lib.sh" jr_load_config hwmon_by_name() { local d for d in /sys/class/hwmon/hwmon*; do [[ $(cat "$d/name" 2>/dev/null) == "$1" ]] && { echo "$d"; return 0; } done return 1 } if [[ ${1:-} == --discover ]]; then command -v sensors-detect >/dev/null && jr_log "tip: 'sudo sensors-detect --auto' loads Super-I/O drivers first" for d in /sys/class/hwmon/hwmon*; do n=$(cat "$d/name" 2>/dev/null || echo '?') pwms=$(cd "$d" && ls pwm[0-9] 2>/dev/null | tr '\n' ' ' || true) fans=$(cd "$d" && for f in fan[0-9]_input; do [[ -f $f ]] && printf '%s=%s ' "${f%_input}" "$(cat "$f")"; done; true) printf '%-10s %-12s pwm: %-24s rpm: %s\n' "${d##*/}" "$n" "${pwms:--}" "${fans:--}" done echo "No PWM listed for the board chip? Then the BMC owns the fans: set a fixed speed in BIOS instead." exit 0 fi if [[ ${1:-} == --test ]]; then [[ $EUID -eq 0 ]] || jr_die "run as root" chip=${2%%/*}; pwm=${2##*/} d=$(hwmon_by_name "$chip") || jr_die "no hwmon named $chip" echo 1 > "$d/${pwm}_enable" for v in 255 120 60 255; do echo "$v" > "$d/$pwm"; sleep 4 printf '%s=%s rpm: %s\n' "$pwm" "$v" "$(cat "$d"/fan*_input 2>/dev/null | tr '\n' ' ')" done echo 2 > "$d/${pwm}_enable" 2>/dev/null || true exit 0 fi [[ $EUID -eq 0 ]] || jr_die "run as root" [[ -n $FAN_HWMON_NAME && -n $FAN_PWM ]] || jr_die "set FAN_HWMON_NAME and FAN_PWM (see --discover)" dir=$(hwmon_by_name "$FAN_HWMON_NAME") || jr_die "hwmon $FAN_HWMON_NAME not found" out=$dir/$FAN_PWM [[ -w $out ]] || jr_die "$out not writable" failsafe() { echo 255 > "$out" 2>/dev/null || true; echo 2 > "${out}_enable" 2>/dev/null || true; } trap failsafe EXIT echo 1 > "${out}_enable" # Temperatures come in whole degrees, so with a steep curve the target jumps # by one degree's worth of pwm (30+ at 72-78 °C) and the fan pulses. The # output follows the target with a time constant of FAN_SMOOTH intervals; # at FAN_T_MAX or without readings it goes to full speed immediately. FAN_SMOOTH=${FAN_SMOOTH:-8} last=-1 acc=$(( 255 * FAN_SMOOTH )) # smoothed pwm x FAN_SMOOTH; start at full speed while true; do if t=$(nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2>/dev/null | sort -n | tail -1) \ && [[ $t =~ ^[0-9]+$ ]]; then if (( t <= FAN_T_MIN )); then target=$FAN_PWM_MIN elif (( t >= FAN_T_MAX )); then target=255 else target=$(( FAN_PWM_MIN + (255 - FAN_PWM_MIN) * (t - FAN_T_MIN) / (FAN_T_MAX - FAN_T_MIN) )) fi else t='?'; target=255 # can't read temps: full speed fi if (( target == 255 )); then acc=$(( 255 * FAN_SMOOTH )) else acc=$(( acc + target - acc / FAN_SMOOTH )) fi v=$(( acc / FAN_SMOOTH )) if (( v != last )); then echo "$v" > "$out"; last=$v; echo "gpu max ${t}°C -> pwm $v (target $target)"; fi sleep "$FAN_INTERVAL" done