Difference between revisions of "Code/tp-fancontrol"

From ThinkWiki
Jump to: navigation, search
(v0.3.02 - just some code comments)
m
Line 63: Line 63:
 
PID_FILE=/var/run/tp-fancontrol.pid
 
PID_FILE=/var/run/tp-fancontrol.pid
 
LOGGER=/usr/bin/logger
 
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
Line 72: Line 72:
  
 
WATCHDOG_DELAY=$(( 3 * INTERVAL ))
 
WATCHDOG_DELAY=$(( 3 * INTERVAL ))
−
HAVE_WATCHDOG=`grep -q watchdog $IBM_ACPI/fan && echo true || echo false`
+
HAVE_WATCHDOG=`grep -q watchdog $IBM_ACPI/fan
−
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
 
−
 
 
−
# 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
 
−
 
 
−
# 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
 
−
    # 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
 
−
        echo enable > $IBM_ACPI/fan
 
−
        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 || $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; }
 
−
 
 
−
        (( (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
 

Revision as of 03:44, 13 April 2007

  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="^(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