#!/bin/bash
# tp-fancontrol 0.2.9 (http://thinkwiki.org/wiki/ACPI_fan_control_script)
# Provided under the GNU General Public License version 2 or later or
# the GNU Free Documentation License version 1.2 or later, at your option.
# See http://www.gnu.org/copyleft/gpl.html for the Warranty Disclaimer.
# This script dynamically controls fan speed on some ThinkPad models
# according to user-defined temperature thresholds. It implements its
# own decision algorithm, overriding the ThinkPad embedded
# controller. It also implements a workaround for the fan noise pulse
# experienced every few seconds on some ThinkPads.
#
# Run 'tp-fancontrol --help' for options.
#
# For optimal fan behavior during suspend and resume, invoke
# "tp-fancontrol -u" during the suspend process.
#
# WARNING: This script relies on undocumented hardware features and
# overrides nominal hardware behavior. It may thus cause arbitrary
# damage to your laptop or data. Watch your temperatures!
#
# WARNING: The list of temperature ranges used below is much more liberal
# than the rules used by the embedded controller firmware, and is
# derived mostly from anecdotal evidence, hunches and wishful thinking.
# It is also model-specific (see http://thinkwiki.org/wiki/Thermal_sensors).
# Temperature ranges, per sensor:
# (min temperature: when to step up from 0-th fan level,
# max temperature: when to step up to maximum fan level)
THRESHOLDS=( # Sensor ThinkPad model
# R51 T41/2 Z60t 43-26xx
# min max # ---------- ------- ----- ----- ---------------------------
50 70 # EC 0x78 CPU CPU ? CPU
47 60 # EC 0x79 miniPCI ? ? Between CPU and PCMCIA slot
43 55 # EC 0x7A HDD ? ? PCMCIA slot
49 68 # EC 0x7B GPU GPU ? GPU
40 50 # EC 0x7C BAT BAT BAT Sys BAT (front left of battery)
45 55 # EC 0x7D n/a n/a n/a UltraBay BAT
37 47 # EC 0x7E BAT BAT BAT Sys BAT (rear right of battery)
45 55 # EC 0x7F n/a n/a n/a UltraBay BAT
45 60 # EC 0xC0 ? n/a ? Between northbridge and DRAM
48 62 # EC 0xC1 ? n/a ? Southbridge (under miniPCI)
50 65 # EC 0xC2 ? n/a ? Power circuitry (under CDC)
47 60 # HDAPS HDAPS HDAPS HDAPS HDAPS readout (same as EC 0x79)
)
LEVELS=( 0 2 4 7) # Fan speed levels
ANTIPULSE=( 0 1 1 0) # Prevent fan pulsing noise at this level
# (reduces frequency of fan RPM updates)
OFF_THRESH_DELTA=3 # when gets this much cooler than 'min' above, may turn off fan
MIN_THRESH_SHIFT=0 # increase min thresholds by this much
MIN_WAIT=180 # minimum time (seconds) to spend in a given level before stepping down
IBM_ACPI=/proc/acpi/ibm
HDAPS_TEMP=/sys/bus/platform/drivers/hdaps/hdaps/temp1
LOGGER=/usr/bin/logger
INTERVAL=3 # sample+refresh interval
SETTLE_TIME=6 # wait this many seconds long before applying anti-pulsing
RESETTLE_TIME=600 # briefly disable anti-pulsing at every N seconds
SUSPEND_TIME=5 # seconds to sleep when receiving SIGUSR1
PID_FILE=/var/run/tp-fancontrol.pid
QUIET=false
DRY_RUN=false
DAEMONIZE=false
AM_DAEMON=false
KILL_DAEMON=false
SUSPEND_DAEMON=false
SYSLOG=false
usage() {
echo "
Usage: $0 [OPTION]...
Available options:
-s N shift up temperature thresholds by N degrees
(positive for quieter, negative for cooler)
-t test mode
-q quiet mode
-d daemon mode, go into background (implies -q)
-l log to syslog
-k kill already-running daemon
-u tell already-running daemon that the system is being suspended
-p pid file location for daemon mode, default: $PID_FILE
"
exit 1;
}
while getopts 's:qtdlp:kuh' OPT; do
case "$OPT" in
s) # shift thresholds
MIN_THRESH_SHIFT="$OPTARG"
;;
t) # test mode
DRY_RUN=true
;;
q) # quiet mode
QUIET=true
;;
d) # go into background and daemonize
DAEMONIZE=true
;;
l) # log to syslog
SYSLOG=true
;;
p) # different pidfile
PID_FILE="$OPTARG"
;;
k) # kill daemon
KILL_DAEMON=true
;;
u) # suspend daemon
SUSPEND_DAEMON=true
;;
h) # short help
usage
;;
\?) # error
usage
;;
esac
done
[ $OPTIND -gt $# ] || usage # no non-option args
# no logger found, no syslog capabilities
$SYSLOG && [ ! -x $LOGGER ] && SYSLOG=false || :
if $DRY_RUN; then
echo "$0: Dry run, will not change fan state."
QUIET=false
DAEMONIZE=false
fi
thermometer() { # output list of temperatures
# Base temperatures from ibm-acpi:
[ -r $IBM_ACPI/thermal ] || { echo "$0: Cannot read $IBM_ACPI/thermal" 2>&1 ; exit 1; }
read X Y1 Y2 Y3 Y4 Y5 Y6 Y7 Y8 Z < $IBM_ACPI/thermal
Y="$Y1 $Y2 $Y3 $Y4 $Y5 $Y6 $Y7 $Y8"
[ "$X" == "temperatures:" ] || { echo "$0: Bad temperatures: $X $Y $Z" >&2; exit 1; }
echo -n "$Y ";
if [[ "$Z" == *\ *\ * ]]; then # ibm_acpi provided the 3 extra sensors from at EC offsets 0xC0 to 0xC2?
echo -n "$Z "
else
[ -r $IBM_ACPI/ecdump ] || { echo "$0: Cannot read $IBM_ACPI/ecdump" 2>&1; exit 1; }
perl -e 'm/^EC 0xc0: .(..) .(..) .(..) / and print hex($1)." ".hex($2)." ".hex($3)." " and exit 0 while <>; exit 1' < $IBM_ACPI/ecdump
fi
# HDAPS temperature (optional):
if [ -r $HDAPS_TEMP ]; then
Y="`cat $HDAPS_TEMP`"
(( "$Y" > 100 )) || echo -n "$Y " # the HDAPS readouts are nonsensical right after resume
fi
return 0
}
speedometer() { # output fan speed RPM
sed -n 's/^speed:[ \t]*//p' $IBM_ACPI/fan
}
setlevel() { # set fan speed level
$DRY_RUN || echo 0x2F $1 > $IBM_ACPI/ecdump
}
getlevel() { # get fan speed level
perl -e 'm/^EC 0x20: .* .(..)$/ and print $1 and exit 0 while <>; exit 1' < $IBM_ACPI/ecdump
}
log() {
$QUIET || echo "> $*"
! $SYSLOG || $LOGGER -t "`basename $0`[$$]" "$*"
}
cleanup() { # clean up after work
$AM_DAEMON && rm -f "$PID_FILE" 2> /dev/null
log "Shutting down, switching to automatic fan control"
$DRY_RUN || echo enable > $IBM_ACPI/fan
}
floor_div() {
echo $(( (($1)+1000*($2))/($2) - 1000 ))
}
init_state() {
IDX=0
NEW_IDX=0
START_TIME=0
MAX_IDX=$(( ${#LEVELS[@]} - 1 ))
SETTLE_LEFT=0
RESETTLE_LEFT=0
FIRST=true
RESTART=false
}
control_fan() {
# Enable the fan in default mode if anything goes wrong:
set -e -E -u
trap "cleanup; exit 2" HUP INT ABRT QUIT SEGV TERM
trap "cleanup" EXIT
trap "log 'Got SIGUSR1'; setlevel 0; RESTART=true; sleep $SUSPEND_TIME" USR1
init_state
log "Starting dynamic fan control"
# Control loop:
while true; do
TEMPS=`thermometer`
$QUIET || SPEED=`speedometer`
$QUIET || ECLEVEL=`getlevel`
NOW=`date +%s`
# Calculate new level index by placing temperatures into Z-regions:
# Z >= 2*I means "must be at index I or higher"
# Z = 2*I+1 is hysteresis: "don't step down if currently at I+1"
# hence the Z-regions are, for d=(MAX-MIN)/(2*MAX_IDX-1) :
# Z=0:{-infty..MIN-d) Z=1:{MIN-d..MIN) Z=2:{MIN..MIN+d} Z=3:{MIN+d..MIN+2d} ... Z=2*MAX_IDX:{MAX-d, MAX}
MAX_Z=$(( IDX>0 ? ( NOW>START_TIME+MIN_WAIT ? 2*(IDX-1) : 2*IDX ) : 0 ))
SENSOR=0
Z_STR="$MAX_Z+"
TEMP_STR="";
for TEMP in $TEMPS; do
[ $((2*SENSOR+2)) -le ${#THRESHOLDS[@]} ] ||
{ echo "Too many sensors, not enough values in THRESHOLDS" 2>&1; exit 1; }
if [ $TEMP == -128 ]; then
Z='_'; TEMP='_' # inactive sensor
else
MIN=$((THRESHOLDS[SENSOR*2] + MIN_THRESH_SHIFT));
MAX=$((THRESHOLDS[SENSOR*2+1]))
if (( TEMP < MIN - OFF_THRESH_DELTA )); then
Z=0
else
Z=$(( `floor_div $(( (TEMP-MIN)*(2*MAX_IDX-2) )) $((MAX-MIN))` + 2 ))
fi
[ $MAX_Z -gt $Z ] || MAX_Z=$Z
fi
Z_STR="${Z_STR}${Z}"
TEMP_STR="${TEMP_STR}${TEMP} "
(( ++SENSOR ))
done
[ $SENSOR -gt 0 ] || { echo "No temperatures read" >&2; exit 1; }
(( (MAX_Z == 2*IDX-1) && ++MAX_Z )) # hysteresis
NEW_IDX=$(( MAX_Z/2 ))
[ $NEW_IDX -le $MAX_IDX ] || NEW_IDX=$MAX_IDX
# Interrupted by a signal?
if $RESTART; then
init_state
log "Resetting state"
continue
fi
# Transition
$FIRST && OLDLEVEL='?' || OLDLEVEL=${LEVELS[$IDX]}
NEWLEVEL=${LEVELS[$NEW_IDX]}
$QUIET || echo "L=$OLDLEVEL->$NEWLEVEL EC=$ECLEVEL RPM=`printf %4s $SPEED` T=($TEMP_STR) Z=$Z_STR"
if [ "$OLDLEVEL" != "$NEWLEVEL" ]; then
START_TIME=$NOW
log "Changing fan level: $OLDLEVEL->$NEWLEVEL (temps: $TEMP_STR)"
fi
setlevel $NEWLEVEL
sleep $INTERVAL
# If needed, apply anti-pulsing hack after a settle-down period (and occasionally re-settle):
if [ ${ANTIPULSE[${NEW_IDX}]} == 1 ]; then
if [ $NEWLEVEL != $OLDLEVEL -o $RESETTLE_LEFT -le 0 ]; then # start settling?
SETTLE_LEFT=$SETTLE_TIME
RESETTLE_LEFT=$RESETTLE_TIME
fi
if [ $SETTLE_LEFT -ge 0 ]; then
SETTLE_LEFT=$((SETTLE_LEFT-INTERVAL))
else
setlevel 0x40 # disengage briefly to fool embedded controller
sleep 0.5
RESETTLE_LEFT=$((RESETTLE_LEFT-INTERVAL))
fi
fi
IDX=$NEW_IDX
FIRST=false
done
}
if $KILL_DAEMON || $SUSPEND_DAEMON; then
if [ -f "$PID_FILE" ]; then
set -e
DPID="`cat \"$PID_FILE\"`"
if $KILL_DAEMON; then
kill "$DPID"
rm "$PID_FILE"
$QUIET || echo "Killed process $DPID"
else # SUSPEND_DAEMON
kill -USR1 "$DPID"
$QUIET || echo "Sent SIGUSR1 to $DPID"
fi
else
$QUIET || echo "Daemon not running."
exit 1
fi
elif $DAEMONIZE ; then
if [ -e "$PID_FILE" ]; then
echo "$0: File $PID_FILE already exists, refusing to run."
exit 1
else
AM_DAEMON=true QUIET=true control_fan 0<&- 1>&- 2>&- &
echo $! > "$PID_FILE"
exit 0
fi
else
[ -e "$PID_FILE" ] && echo "WARNING: daemon already running"
control_fan
fi