Code/tp-fancontrol

From ThinkWiki
Revision as of 12:03, 20 September 2006 by Thinker (Talk | contribs) (bugfix)
Jump to: navigation, search
  1. !/bin/bash
  1. tp-fancontrol 0.2.16 (http://thinkwiki.org/wiki/ACPI_fan_control_script)
  2. Provided under the GNU General Public License version 2 or later or
  3. the GNU Free Documentation License version 1.2 or later, at your option.
  4. See http://www.gnu.org/copyleft/gpl.html for the Warranty Disclaimer.
  1. This script dynamically controls fan speed on some ThinkPad models
  2. according to user-defined temperature thresholds. It implements its
  3. own decision algorithm, overriding the ThinkPad embedded
  4. controller. It also implements a workaround for the fan noise pulse
  5. experienced every few seconds on some ThinkPads.
  6. Run 'tp-fancontrol --help' for options.
  7. For optimal fan behavior during suspend and resume, invoke
  8. "tp-fancontrol -u" during the suspend process.
  9. WARNING: This script relies on undocumented hardware features and
  10. overrides nominal hardware behavior. It may thus cause arbitrary
  11. damage to your laptop or data. Watch your temperatures!
  12. WARNING: The list of temperature ranges used below is much more liberal
  13. than the rules used by the embedded controller firmware, and is
  14. derived mostly from anecdotal evidence, hunches and wishful thinking.
  15. It is also model-specific (see http://thinkwiki.org/wiki/Thermal_sensors).
  1. Temperature ranges, per sensor:
  2. (min temperature: when to step up from 0-th fan level,
  3. max temperature: when to step up to maximum fan level)

THRESHOLDS=( # Sensor ThinkPad model

            #             R51     T41/2  Z60t   T43-26xx
  1. 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)
 40   50    #  EC 0x7D    n/a     n/a    n/a    UltraBay BAT
 37   47    #  EC 0x7E    BAT     BAT    BAT    Sys BAT (rear right of battery)
 37   47    #  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 MAX_THRESH_SHIFT=0 # increase max 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 the min temperature thresholds by N degrees
         (positive for quieter, negative for cooler).
         Max temperature thresholds are not affected.
  -S N   Shift up the max temperature thresholds by N degrees
         (positive for quieter, negative for cooler). DANGEROUS.
  -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:S:qtdlp:kuh' OPT; do

   case "$OPT" in
       s) # shift thresholds
           MIN_THRESH_SHIFT="$OPTARG"
           ;;
       S) # shift thresholds
           MAX_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

  1. 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 THERMAL < $IBM_ACPI/thermal
   read X Y1 Y2 Y3 Y4 Y5 Y6 Y7 Y8 Z1 Z2 Z3 JNK < <(echo "$THERMAL") 
   "$X" == "temperatures:"  || { echo "$0: Bad readout: \"$THERMAL\"" >&2;  exit 1; }
   echo -n "$Y1 $Y2 $Y3 $Y4 $Y5 $Y6 $Y7 $Y8 ";
   if -n "$Z1" && -n "$Z2" && -n "$Z3" ; then 
       # ibm_acpi provided extra sensors from at EC offsets 0xC0 to 0xC2?
       echo -n "$Z1 $Z2 $Z3 "
   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 ))

}

set_priority() {

   ! $DRY_RUN && renice -10 -p $$

}

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] + MAX_THRESH_SHIFT ))
               $MAX -le $MIN  && \
                   { echo 'Reversed temperature thresholds (shifted too much?)' 2>&1; exit 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
       set_priority
       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"
   set_priority
   control_fan

fi