Difference between revisions of "Code/tp-fancontrol"

From ThinkWiki
Jump to: navigation, search
 
(21 intermediate revisions by 7 users not shown)
Line 1: Line 1:
{{Code|
 
|filename=tp-fancontrol
 
|context=[[ACPI fan_control script#Comprehensive_bash_script_with_fine_control_over_fan_speed]]
 
|author=[[User:Thinker|Thinker]] and [[User:Spiney|Spiney]]
 
|license=GPL v2 or later; or GFDL v1.2 or later]
 
}}
 
 
<pre>
 
 
#!/bin/bash
 
#!/bin/bash
  
# tp-fancontrol 0.2.9 (http://thinkwiki.org/wiki/ACPI_fan_control_script)
+
# tp-fancontrol 0.3.02 (http://thinkwiki.org/wiki/ACPI_fan_control_script)
 
# Provided under the GNU General Public License version 2 or later or
 
# 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.
 
# the GNU Free Documentation License version 1.2 or later, at your option.
Line 38: Line 30:
 
#  max temperature: when to step up to maximum fan level)
 
#  max temperature: when to step up to maximum fan level)
 
THRESHOLDS=( #  Sensor    ThinkPad model
 
THRESHOLDS=( #  Sensor    ThinkPad model
             #            R51    T41/2  Z60t  43-26xx
+
             #            R51    T41/2  Z60t  T43-26xx
 
# min  max  #  ---------- ------- -----  -----  ---------------------------
 
# min  max  #  ---------- ------- -----  -----  ---------------------------
 
   50  70    #  EC 0x78    CPU    CPU    ?      CPU
 
   50  70    #  EC 0x78    CPU    CPU    ?      CPU
Line 45: Line 37:
 
   49  68    #  EC 0x7B    GPU    GPU    ?      GPU
 
   49  68    #  EC 0x7B    GPU    GPU    ?      GPU
 
   40  50    #  EC 0x7C    BAT    BAT    BAT    Sys BAT (front left of battery)
 
   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
+
   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 0x7E    BAT    BAT    BAT    Sys BAT (rear right of battery)
   45   55   #  EC 0x7F    n/a    n/a    n/a    UltraBay BAT
+
   37   47   #  EC 0x7F    n/a    n/a    n/a    UltraBay BAT
 +
 
 
   45  60    #  EC 0xC0    ?      n/a    ?      Between northbridge and DRAM
 
   45  60    #  EC 0xC0    ?      n/a    ?      Between northbridge and DRAM
 
   48  62    #  EC 0xC1    ?      n/a    ?      Southbridge (under miniPCI)
 
   48  62    #  EC 0xC1    ?      n/a    ?      Southbridge (under miniPCI)
 
   50  65    #  EC 0xC2    ?      n/a    ?      Power circuitry (under CDC)
 
   50  65    #  EC 0xC2    ?      n/a    ?      Power circuitry (under CDC)
   47  60   #  HDAPS     HDAPS   HDAPS HDAPS HDAPS readout (same as EC 0x79)
+
 
 +
   47  58   #  HDD        ->     ->    ->    Hard disk internal sensor
 +
  47   60    # HDAPS     ->      ->    ->    HDAPS readout (same as EC 0x79)
 
)
 
)
  
Line 61: Line 56:
 
OFF_THRESH_DELTA=3 # when gets this much cooler than 'min' above, may turn off fan
 
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_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
 
MIN_WAIT=180 # minimum time (seconds) to spend in a given level before stepping down
  
 
IBM_ACPI=/proc/acpi/ibm
 
IBM_ACPI=/proc/acpi/ibm
 
HDAPS_TEMP=/sys/bus/platform/drivers/hdaps/hdaps/temp1
 
HDAPS_TEMP=/sys/bus/platform/drivers/hdaps/hdaps/temp1
LOGGER=/usr/bin/logger  
+
PID_FILE=/var/run/tp-fancontrol.pid
 +
LOGGER=/usr/bin/logger
 
INTERVAL=3        # sample+refresh interval
 
INTERVAL=3        # sample+refresh interval
 
SETTLE_TIME=6    # wait this many seconds long before applying anti-pulsing
 
SETTLE_TIME=6    # wait this many seconds long before applying anti-pulsing
 
RESETTLE_TIME=600 # briefly disable anti-pulsing at every N seconds
 
RESETTLE_TIME=600 # briefly disable anti-pulsing at every N seconds
 
SUSPEND_TIME=5    # seconds to sleep when receiving SIGUSR1
 
SUSPEND_TIME=5    # seconds to sleep when receiving SIGUSR1
 +
DISK_POLL_PERIOD=15 # poll period in seconds for disk sensors (it changes slowly and is expensive to read)
 +
HITACHI_MODELS="^(HTS4212..H9AT00|HTS726060M9AT00|HTS5410..G9AT00|IC25[NT]0..ATCS0[45]|HTE541040G9AT00|HTS5416..J9(AT|SA)00)"
 +
SEP=','          # Separator char for display
 +
 +
WATCHDOG_DELAY=$(( 3 * INTERVAL ))
 +
HAVE_WATCHDOG=`grep -q watchdog $IBM_ACPI/fan && echo true || echo false`
 +
HAVE_LEVELCMD=`grep -q disengaged $IBM_ACPI/fan && echo true || echo false`
  
PID_FILE=/var/run/tp-fancontrol.pid
 
 
QUIET=false
 
QUIET=false
 
DRY_RUN=false
 
DRY_RUN=false
Line 79: Line 82:
 
SUSPEND_DAEMON=false
 
SUSPEND_DAEMON=false
 
SYSLOG=false
 
SYSLOG=false
 +
DISK_POLL_TIME=-$DISK_POLL_PERIOD
  
 
usage() {
 
usage() {
Line 85: Line 89:
  
 
Available options:
 
Available options:
   -s N  shift up temperature thresholds by N degrees
+
   -s N  Shift up the min temperature thresholds by N degrees
           (positive for quieter, negative for cooler)
+
          (positive for quieter, negative for cooler).
   -t    test mode
+
          Max temperature thresholds are not affected.
   -q    quiet mode
+
  -S N  Shift up the max temperature thresholds by N degrees
   -d    daemon mode, go into background (implies -q)
+
           (positive for quieter, negative for cooler). DANGEROUS.
   -l    log to syslog
+
   -t    Test mode
   -k    kill already-running daemon
+
   -q    Quiet mode
   -u    tell already-running daemon that the system is being suspended
+
   -d    Daemon mode, go into background (implies -q)
   -p    pid file location for daemon mode, default: $PID_FILE
+
   -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;
 
     exit 1;
 
}
 
}
  
while getopts 's:qtdlp:kuh' OPT; do
+
while getopts 's:S:qtdlp:kuh' OPT; do
 
     case "$OPT" in
 
     case "$OPT" in
 
         s) # shift thresholds
 
         s) # shift thresholds
 
             MIN_THRESH_SHIFT="$OPTARG"
 
             MIN_THRESH_SHIFT="$OPTARG"
 +
            ;;
 +
        S) # shift thresholds
 +
            MAX_THRESH_SHIFT="$OPTARG"
 
             ;;
 
             ;;
 
         t) # test mode
 
         t) # test mode
Line 142: Line 152:
 
     DAEMONIZE=false
 
     DAEMONIZE=false
 
fi
 
fi
 +
 +
# Read the temperature sensor on new Hitachi drivers without spinning up the
 +
# disk or unloading its head (this cannot be done using standard SMART).
 +
# Works only with drivers/ide or new libata. Equivalent to hdparm -H in >=6.7.
 +
read_hitachi_temp() { perl - "$@" <<'EOPERL'    # do it in Perl
 +
    #!/usr/bin/perl
 +
    $dev="$ARGV[0]" or die "No device given.\n";
 +
    $HDIO_DRIVE_CMD=0x031f;
 +
    $args=pack("cccc",0xf0,0,0x01,0);  # Sense Condition command
 +
    open(DEV,"<",$dev) or die "open(\"$dev\"): $!\n";
 +
    if (ioctl(DEV,$HDIO_DRIVE_CMD,$args)) {
 +
      $nsect=(unpack("cccc",$args))[2];
 +
      if ($nsect==0 || $nsect==0xff) {
 +
          die "Temperature over/underflow.\n";
 +
      } elsif ($nsect==0x01) {  # Linux<=2.6.18 doesn't return ATA registers
 +
          die "Old Linux kernel, readout not supported.\n";
 +
      } else {
 +
          printf "%d\n", $nsect/2-20;
 +
      }
 +
    } else {
 +
        die "ioctl(\"$dev\",HDIO_DRIVE_CMD,SENSE_CONDITION): $!\n"
 +
    }
 +
EOPERL
 +
}
 +
 +
update_disk_temp() {
 +
    if (( SECONDS >= DISK_POLL_TIME + DISK_POLL_PERIOD )); then
 +
        LAST_DISK_TEMP="-128"
 +
        for DEV in {sda,hda}; do
 +
            if [[ -b "/dev/$DEV" ]]; then
 +
                local MODEL=`cat /sys/block/$DEV/device/model`
 +
                if [[ "$MODEL" =~ "$HITACHI_MODELS" ]]; then
 +
                    if HTEMP=`read_hitachi_temp "/dev/$DEV" 2>/dev/null`; then
 +
                        LAST_DISK_TEMP="$HTEMP"
 +
                        break
 +
                    fi
 +
                fi
 +
            fi
 +
        done
 +
        DISK_POLL_TIME=$SECONDS
 +
    fi
 +
}
  
 
thermometer() { # output list of temperatures
 
thermometer() { # output list of temperatures
     # Base temperatures from ibm-acpi:
+
     # 8 basic temperatures from ibm-acpi:
     [ -r $IBM_ACPI/thermal ] || { echo "$0: Cannot read $IBM_ACPI/thermal" 2>&1 ; exit 1; }
+
     [[ -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
+
     read THERMAL < $IBM_ACPI/thermal
     Y="$Y1 $Y2 $Y3 $Y4 $Y5 $Y6 $Y7 $Y8"
+
     read X Y1 Y2 Y3 Y4 Y5 Y6 Y7 Y8 Z1 Z2 Z3 JNK < <(echo "$THERMAL")
     [ "$X" == "temperatures:" ] || { echo "$0: Bad temperatures: $X $Y $Z" >&2;  exit 1; }
+
     [[ "$X" == "temperatures:" ]] || { echo "$0: Bad readout: \"$THERMAL\"" >&2;  exit 1; }
     echo -n "$Y ";
+
     echo -n "$Y1 $Y2 $Y3 $Y4 $Y5 $Y6 $Y7 $Y8 ";
     if [[ "$Z" == *\ *\ * ]]; then # ibm_acpi provided the 3 extra sensors from at EC offsets 0xC0 to 0xC2?
+
    # 3 extra temperatures from ibm_acpi:
         echo -n "$Z "
+
     if [[ -n "$Z1" && -n "$Z2" && -n "$Z3" ]]; then  
 +
        # ibm_acpi provided extra sensors from at EC offsets 0xC0 to 0xC2?
 +
         echo -n "$SEP $Z1 $Z2 $Z3 "
 
     else  
 
     else  
 
         [ -r $IBM_ACPI/ecdump ] || { echo "$0: Cannot read $IBM_ACPI/ecdump" 2>&1; exit 1; }
 
         [ -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
 
         perl -e 'm/^EC 0xc0: .(..) .(..) .(..) / and print hex($1)." ".hex($2)." ".hex($3)." " and exit 0 while <>; exit 1' < $IBM_ACPI/ecdump
 
     fi
 
     fi
     # HDAPS temperature (optional):
+
     # 1 Disk drive temperatures:
 +
    echo -n "$SEP $LAST_DISK_TEMP "
 +
    # 1 HDAPS temperature (optional):
 
     if [ -r $HDAPS_TEMP ]; then
 
     if [ -r $HDAPS_TEMP ]; then
 
         Y="`cat $HDAPS_TEMP`"
 
         Y="`cat $HDAPS_TEMP`"
Line 169: Line 225:
  
 
setlevel() { # set fan speed level
 
setlevel() { # set fan speed level
     $DRY_RUN || echo 0x2F $1 > $IBM_ACPI/ecdump
+
     local LEVEL=$1
 +
    if ! $DRY_RUN; then
 +
        if $HAVE_LEVELCMD; then
 +
        echo "level $LEVEL" > $IBM_ACPI/fan
 +
else
 +
case "$LEVEL" in
 +
(auto)        LEVEL=0x80 ;;
 +
(disengaged)  LEVEL=0x40 ;;
 +
esac
 +
        echo 0x2F $LEVEL > $IBM_ACPI/ecdump
 +
fi
 +
    fi
 
}
 
}
  
Line 184: Line 251:
 
     $AM_DAEMON && rm -f "$PID_FILE" 2> /dev/null
 
     $AM_DAEMON && rm -f "$PID_FILE" 2> /dev/null
 
     log "Shutting down, switching to automatic fan control"
 
     log "Shutting down, switching to automatic fan control"
     $DRY_RUN || echo enable > $IBM_ACPI/fan
+
     if ! $DRY_RUN; then
 +
        if $HAVE_LEVELCMD; then
 +
            echo enable > $IBM_ACPI/fan
 +
        else
 +
            echo 0x2F 0x80 > $IBM_ACPI/ecdump
 +
        fi
 +
        if $HAVE_WATCHDOG; then
 +
            echo watchdog 0 > $IBM_ACPI/fan  # disable watchdog
 +
        fi
 +
    fi
 
}
 
}
  
 
floor_div() {
 
floor_div() {
 
     echo $(( (($1)+1000*($2))/($2) - 1000 ))
 
     echo $(( (($1)+1000*($2))/($2) - 1000 ))
 +
}
 +
 +
set_priority() {
 +
    ! $DRY_RUN && renice -10 -p $$
 
}
 
}
  
Line 208: Line 288:
 
     trap "cleanup" EXIT
 
     trap "cleanup" EXIT
 
     trap "log 'Got SIGUSR1'; setlevel 0; RESTART=true; sleep $SUSPEND_TIME" USR1
 
     trap "log 'Got SIGUSR1'; setlevel 0; RESTART=true; sleep $SUSPEND_TIME" USR1
 +
    if ! $DRY_RUN && $HAVE_WATCHDOG; then
 +
        log "Activating watchdog with delay $WATCHDOG_DELAY sec"
 +
        echo "watchdog $WATCHDOG_DELAY" > $IBM_ACPI/fan
 +
    fi
  
 
     init_state
 
     init_state
Line 214: Line 298:
 
     # Control loop:
 
     # Control loop:
 
     while true; do
 
     while true; do
 +
        # Get readouts
 +
        update_disk_temp  # don't do this in a subshell, it's stateful
 
         TEMPS=`thermometer`
 
         TEMPS=`thermometer`
 
         $QUIET || SPEED=`speedometer`
 
         $QUIET || SPEED=`speedometer`
 
         $QUIET || ECLEVEL=`getlevel`
 
         $QUIET || ECLEVEL=`getlevel`
 
         NOW=`date +%s`
 
         NOW=`date +%s`
 +
        if echo "$TEMPS" | grep -q "[^ 0-9$SEP\n-]"; then
 +
            echo "Invalid character in temperatures: $TEMPS" >&2; exit 1;
 +
        fi
  
         # Calculate new level index by placing temperatures into Z-regions:
+
         # Calculate new level index by placing temperatures into regions of "Z" values:
 
         # Z >= 2*I means "must be at index I or higher"
 
         # 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"
 
         # 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) :
+
         # The set of temperatures for each Z value are as follows, denoting 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}
+
         #  Z=0:         {-infty..MIN-OFF_THRESH_DELTA)     Z=1:   {MIN-OFF_THRESH_DELTA..MIN}
 +
        #  Z=2:         {MIN..MIN+d}         Z=3:   {MIN+d..MIN+2d}
 +
        #  Z=4:        {MIN+2d..MIN+3d}    Z=5:  {MIN+3d..MIN+4d}  ...
 +
        #  Z=2*MAX_IDX: {MAX..infty}
  
 +
        # Enforce minimum time in this level before stepping down:
 
         MAX_Z=$(( IDX>0 ? ( NOW>START_TIME+MIN_WAIT ? 2*(IDX-1) : 2*IDX ) : 0 ))
 
         MAX_Z=$(( IDX>0 ? ( NOW>START_TIME+MIN_WAIT ? 2*(IDX-1) : 2*IDX ) : 0 ))
 +
 +
        # Go over all sensors and compute the Z value; compute the maximum Z and a pretty-printed string:
 
         SENSOR=0
 
         SENSOR=0
 
         Z_STR="$MAX_Z+"
 
         Z_STR="$MAX_Z+"
 
         TEMP_STR="";
 
         TEMP_STR="";
 
         for TEMP in $TEMPS; do
 
         for TEMP in $TEMPS; do
 +
            if [[ "$TEMP" == "$SEP" ]]; then  # ignore this (a separator for visual aid)
 +
                Z_STR="${Z_STR}$SEP"
 +
                TEMP_STR="${TEMP_STR}$SEP "
 +
                continue
 +
            fi
 
             [ $((2*SENSOR+2)) -le ${#THRESHOLDS[@]} ] ||
 
             [ $((2*SENSOR+2)) -le ${#THRESHOLDS[@]} ] ||
 
                 { echo "Too many sensors, not enough values in THRESHOLDS" 2>&1; exit 1; }
 
                 { echo "Too many sensors, not enough values in THRESHOLDS" 2>&1; exit 1; }
             if [ $TEMP == -128 ]; then
+
             if [[ $TEMP == -128 || $TEMP == 128 ]]; then
 
                 Z='_'; TEMP='_' # inactive sensor
 
                 Z='_'; TEMP='_' # inactive sensor
 
             else
 
             else
                 MIN=$((THRESHOLDS[SENSOR*2] + MIN_THRESH_SHIFT));
+
                 MIN=$((THRESHOLDS[SENSOR*2] + MIN_THRESH_SHIFT))
                 MAX=$((THRESHOLDS[SENSOR*2+1]))
+
                 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
 
                 if (( TEMP < MIN - OFF_THRESH_DELTA )); then
 
                     Z=0
 
                     Z=0
                 else
+
                 else # compute Z value for this sensor (see above):
                     Z=$(( `floor_div $(( (TEMP-MIN)*(2*MAX_IDX-2) )) $((MAX-MIN))` + 2 ))
+
                     Z=$(( `floor_div $(( 2*(TEMP-MIN)*(MAX_IDX-1) )) $((MAX-MIN))` + 2 ))
 +
                    [ $Z -ge 1 ] || Z=1
 +
                    [ $Z -le $((2*MAX_IDX)) ] || Z=$((2*MAX_IDX))
 
                 fi
 
                 fi
 
                 [ $MAX_Z -gt $Z ] || MAX_Z=$Z
 
                 [ $MAX_Z -gt $Z ] || MAX_Z=$Z
Line 250: Line 354:
 
         [ $SENSOR -gt 0 ] || { echo "No temperatures read" >&2; exit 1; }
 
         [ $SENSOR -gt 0 ] || { echo "No temperatures read" >&2; exit 1; }
  
         (( (MAX_Z == 2*IDX-1) && ++MAX_Z )) # hysteresis
+
         HYS=$(( (MAX_Z == 2*IDX-1) && ++MAX_Z )) # hysteresis
 
         NEW_IDX=$(( MAX_Z/2 ))
 
         NEW_IDX=$(( MAX_Z/2 ))
        [ $NEW_IDX -le $MAX_IDX ] || NEW_IDX=$MAX_IDX
 
  
# Interrupted by a signal?
+
        # Interrupted by a signal?
if $RESTART; then
+
        if $RESTART; then
init_state
+
            init_state
log "Resetting state"
+
            log "Resetting state"
continue
+
            continue
fi
+
        fi
  
 
         # Transition
 
         # Transition
Line 283: Line 386:
 
                 SETTLE_LEFT=$((SETTLE_LEFT-INTERVAL))
 
                 SETTLE_LEFT=$((SETTLE_LEFT-INTERVAL))
 
             else
 
             else
                 setlevel 0x40 # disengage briefly to fool embedded controller
+
                 setlevel disengaged # disengage briefly to fool embedded controller
 
                 sleep 0.5
 
                 sleep 0.5
 
                 RESETTLE_LEFT=$((RESETTLE_LEFT-INTERVAL))
 
                 RESETTLE_LEFT=$((RESETTLE_LEFT-INTERVAL))
Line 315: Line 418:
 
         exit 1
 
         exit 1
 
     else
 
     else
AM_DAEMON=true QUIET=true control_fan 0<&- 1>&- 2>&- &
+
        set_priority
 +
        AM_DAEMON=true QUIET=true control_fan 0<&- 1>&- 2>&- &
 
         echo $! > "$PID_FILE"
 
         echo $! > "$PID_FILE"
 
         exit 0
 
         exit 0
Line 321: Line 425:
 
else
 
else
 
     [ -e "$PID_FILE" ] && echo "WARNING: daemon already running"
 
     [ -e "$PID_FILE" ] && echo "WARNING: daemon already running"
 +
    set_priority
 
     control_fan
 
     control_fan
 
fi
 
fi
</pre>
 

Latest revision as of 15:50, 7 June 2011

  1. !/bin/bash
  1. tp-fancontrol 0.3.02 (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   58    #  HDD        ->      ->     ->     Hard disk internal sensor
 47   60    #  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 PID_FILE=/var/run/tp-fancontrol.pid 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 DISK_POLL_PERIOD=15 # poll period in seconds for disk sensors (it changes slowly and is expensive to read) HITACHI_MODELS="^(HTS4212..H9AT00|HTS726060M9AT00|HTS5410..G9AT00|IC25[NT]0..ATCS0[45]|HTE541040G9AT00|HTS5416..J9(AT|SA)00)" SEP=',' # Separator char for display

WATCHDOG_DELAY=$(( 3 * INTERVAL )) HAVE_WATCHDOG=`grep -q watchdog $IBM_ACPI/fan && echo true || echo false` HAVE_LEVELCMD=`grep -q disengaged $IBM_ACPI/fan && echo true || echo false`

QUIET=false DRY_RUN=false DAEMONIZE=false AM_DAEMON=false KILL_DAEMON=false SUSPEND_DAEMON=false SYSLOG=false DISK_POLL_TIME=-$DISK_POLL_PERIOD

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

  1. Read the temperature sensor on new Hitachi drivers without spinning up the
  2. disk or unloading its head (this cannot be done using standard SMART).
  3. Works only with drivers/ide or new libata. Equivalent to hdparm -H in >=6.7.

read_hitachi_temp() { perl - "$@" <<'EOPERL' # do it in Perl

   #!/usr/bin/perl
   $dev="$ARGV[0]" or die "No device given.\n";
   $HDIO_DRIVE_CMD=0x031f;
   $args=pack("cccc",0xf0,0,0x01,0);   # Sense Condition command
   open(DEV,"<",$dev) or die "open(\"$dev\"): $!\n";
   if (ioctl(DEV,$HDIO_DRIVE_CMD,$args)) {
      $nsect=(unpack("cccc",$args))[2];
      if ($nsect==0 || $nsect==0xff) {
          die "Temperature over/underflow.\n";
      } elsif ($nsect==0x01) {  # Linux<=2.6.18 doesn't return ATA registers
          die "Old Linux kernel, readout not supported.\n";
      } else {
          printf "%d\n", $nsect/2-20;
      }
   } else {
       die "ioctl(\"$dev\",HDIO_DRIVE_CMD,SENSE_CONDITION): $!\n"
   }

EOPERL }

update_disk_temp() {

   if (( SECONDS >= DISK_POLL_TIME + DISK_POLL_PERIOD )); then
       LAST_DISK_TEMP="-128"
       for DEV in {sda,hda}; do
           if -b "/dev/$DEV" ; then
               local MODEL=`cat /sys/block/$DEV/device/model`
               if "$MODEL" =~ "$HITACHI_MODELS" ; then
                   if HTEMP=`read_hitachi_temp "/dev/$DEV" 2>/dev/null`; then
                       LAST_DISK_TEMP="$HTEMP"
                       break
                   fi
               fi
           fi
       done
       DISK_POLL_TIME=$SECONDS
   fi

}

thermometer() { # output list of temperatures

   # 8 basic 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 ";
   # 3 extra temperatures from ibm_acpi:
   if -n "$Z1" && -n "$Z2" && -n "$Z3" ; then 
       # ibm_acpi provided extra sensors from at EC offsets 0xC0 to 0xC2?
       echo -n "$SEP $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
   # 1 Disk drive temperatures:
   echo -n "$SEP $LAST_DISK_TEMP "
   # 1 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

   local LEVEL=$1
   if ! $DRY_RUN; then
       if $HAVE_LEVELCMD; then
       	echo "level $LEVEL" > $IBM_ACPI/fan

else case "$LEVEL" in (auto) LEVEL=0x80 ;; (disengaged) LEVEL=0x40 ;; esac

       	echo 0x2F $LEVEL > $IBM_ACPI/ecdump

fi

   fi

}

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"
   if ! $DRY_RUN; then
       if $HAVE_LEVELCMD; then
           echo enable > $IBM_ACPI/fan
       else
           echo 0x2F 0x80 > $IBM_ACPI/ecdump
       fi
       if $HAVE_WATCHDOG; then
           echo watchdog 0 > $IBM_ACPI/fan  # disable watchdog
       fi
   fi

}

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
   if ! $DRY_RUN && $HAVE_WATCHDOG; then
       log "Activating watchdog with delay $WATCHDOG_DELAY sec"
       echo "watchdog $WATCHDOG_DELAY" > $IBM_ACPI/fan
   fi
   init_state
   log "Starting dynamic fan control"
   # Control loop:
   while true; do
       # Get readouts
       update_disk_temp  # don't do this in a subshell, it's stateful
       TEMPS=`thermometer`
       $QUIET || SPEED=`speedometer`
       $QUIET || ECLEVEL=`getlevel`
       NOW=`date +%s`
       if echo "$TEMPS" | grep -q "[^ 0-9$SEP\n-]"; then
           echo "Invalid character in temperatures: $TEMPS" >&2; exit 1;
       fi
       # Calculate new level index by placing temperatures into regions of "Z" values:
       # 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"
       # The set of temperatures for each Z value are as follows, denoting d=(MAX-MIN)/(2*(MAX_IDX-1)) :
       #   Z=0:         {-infty..MIN-OFF_THRESH_DELTA)     Z=1:   {MIN-OFF_THRESH_DELTA..MIN}
       #   Z=2:         {MIN..MIN+d}         Z=3:   {MIN+d..MIN+2d}
       #   Z=4:         {MIN+2d..MIN+3d}     Z=5:   {MIN+3d..MIN+4d}   ...
       #   Z=2*MAX_IDX: {MAX..infty}
       # Enforce minimum time in this level before stepping down:
       MAX_Z=$(( IDX>0 ? ( NOW>START_TIME+MIN_WAIT ? 2*(IDX-1) : 2*IDX ) : 0 ))
       # Go over all sensors and compute the Z value; compute the maximum Z and a pretty-printed string:
       SENSOR=0
       Z_STR="$MAX_Z+"
       TEMP_STR="";
       for TEMP in $TEMPS; do
           if "$TEMP" == "$SEP" ; then   # ignore this (a separator for visual aid)
               Z_STR="${Z_STR}$SEP"
               TEMP_STR="${TEMP_STR}$SEP "
               continue
           fi
           [ $((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  # compute Z value for this sensor (see above):
                   Z=$(( `floor_div $(( 2*(TEMP-MIN)*(MAX_IDX-1) )) $((MAX-MIN))` + 2 ))
                   [ $Z -ge 1 ] || Z=1
                   [ $Z -le $((2*MAX_IDX)) ] || Z=$((2*MAX_IDX))
               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; }
       HYS=$(( (MAX_Z == 2*IDX-1) && ++MAX_Z )) # hysteresis
       NEW_IDX=$(( MAX_Z/2 ))
       # 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 disengaged # 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