Difference between revisions of "Talk:Fan control scripts"

From ThinkWiki
Jump to: navigation, search
(Sensor-specific variable-speed script)
(New sensor-specific variable-speed script)
Line 27: Line 27:
 
== Sensor-specific variable-speed script ==
 
== Sensor-specific variable-speed script ==
  
Here's a new variable-speed control script that lets you define the temperature range separately for each sensor. To keep things simple, it auto-computes the trip points (unlike the current script). Works well on a {{T43}}, and (just barely) keeps the fan off most of the time with CPU undervolting and  {{fglrx}} set to maximum power saving. Feedback on other machines would be appreciated.
+
Here's a new variable-speed control script that lets you define the temperature range separately for each sensor. To keep things simple, it auto-computes the trip points (unlike the current script). Works well on a {{T43}}, and (just barely) keeps the fan off most of the time with CPU undervolting and  [[fglrx]] set to maximum power saving. Feedback on other machines would be appreciated.
  
Any idea what the funny sensors (2nd and 3rd) are? On the T43, the second sensor seems to be the same as the HDAPS sensor (it never deviates by more than one degree from what the HDAPS sensor tells directly), but I don't know where it's located. The 3rd sensor is uncorrelated with disk temperature and activity.
+
Any idea what are the sensors at EC offsets 0x79, 0x7A, 0xC0, 0xC1, 0xC2? On the T43, 0x79 seems to be the same as the HDAPS sensor (it never deviates by more than one degree from what the HDAPS sensor tells directly), but I don't know where it's located. Sensor 0x7A is uncorrelated with disk temperature and activity, so it can't be HDD like reported ofor R52. Sensor 0xC1 seems to be under the palm-rest (see discussion in [[Talk:Problem_with_fan_noise]]).
  
 
<pre>
 
<pre>
 
#!/bin/bash
 
#!/bin/bash
 +
 +
# tp-fancontrol 0.2 (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
 
# This script dynamically controls fan speed on some ThinkPad models
Line 43: Line 48:
 
# overrides nominal hardware behavior. It may thus cause arbitrary
 
# overrides nominal hardware behavior. It may thus cause arbitrary
 
# damage to your laptop or data. Watch your temperatures!
 
# 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 evidene, hunches and wishful thinking.
 +
# It is also model-specific.
 
#
 
#
 
# This file is placed in the public domain and may be freely distributed.
 
# This file is placed in the public domain and may be freely distributed.
 +
 +
# 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      T43
 +
# min  max  #  ---------- -------  -------
 +
  50  70    #  EC 0x78    CPU      CPU
 +
  47  57    #  EC 0x79    miniPCI  HDAPS (through EC)
 +
  43  54    #  EC 0x7A    HDD      ???
 +
  49  68    #  EC 0x7B    GPU      GPU
 +
  37  52    #  EC 0x7C    BAT      BAT (SouthWest)
 +
  45  55    #  EC 0x7D    n/a      n/a
 +
  33  45    #  EC 0x7E    BAT      BAT (NorthEast)
 +
  45  55    #  EC 0x7F    n/a      n/a
 +
  45  55    #  EC 0xC0    ???      ???
 +
  48  60    #  EC 0xC1    ???      mini-PCI (?)
 +
  47  56    #  EC 0xC2    ???      RAM?
 +
  47  57    #  HDAPS      HDAPS    HDAPS (direct)
 +
)
  
 
LEVELS=(    0      2      4      7)  # Fan speed levels
 
LEVELS=(    0      2      4      7)  # Fan speed levels
 
ANTIPULSE=( 0      1      0      0)  # Prevent fan pulsing noise at this level
 
ANTIPULSE=( 0      1      0      0)  # Prevent fan pulsing noise at this level
                                     #   (this also prevents fan speed updates)
+
                                     # (reduces frequency of fan speed updates)
  
# Temperature ranges, per sensor:
+
OFF_THRESH_DELTA=2 # when gets this much cooler than 'min' above, may turn off fan
MIN_TEMPS=( 52  49    44  51  39    45  35    45  49  ) # step up from zero-th fan level at this temperature
+
MIN_THRESH_SHIFT=0 # increase min thresholds by this much
MAX_TEMPS=( 70  59    54  68  52    55  45    55  59  ) # step up to maximum fan at this temperature
+
MIN_WAIT=120 # minimum time (seconds) to spend in a given level before stepping down
          # CPU HDAPS ??? GPU BAT-SW n/a BAT-NE n/a HDAPS  (ThinkPad T43)
 
          # CPU m-PCI HDD GPU BAT-SW n/a BAT-NE n/a HDAPS  (ThinkPad R51)
 
  
 
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
 +
INTERVAL=3
 +
SETTLE_TIME=6
 +
RESETTLE_TIME=300
 +
 
PID_FILE=/var/run/tp-fancontrol.pid
 
PID_FILE=/var/run/tp-fancontrol.pid
INTERVAL=3
 
 
VERBOSE=true
 
VERBOSE=true
 
DRY_RUN=false
 
DRY_RUN=false
Line 66: Line 98:
 
KILL_DAEMON=false
 
KILL_DAEMON=false
 
SYSLOG=false
 
SYSLOG=false
LOGGER=/usr/bin/logger
 
  
 
usage() {
 
usage() {
     echo "Usage: $0 [OPTION]..."
+
     echo "
    echo
+
Usage: $0 [OPTION]...
    echo "Available options:"
+
 
    echo "   -t    test mode"
+
Available options:
    echo "  -q    quiet mode"
+
  -s N   shift up temperature thresholds by N degrees
    echo "  -d    daemon mode, go into background (implies -q)"
+
  -t    test mode
    echo "  -l    log to syslog"
+
  -q    quiet mode
     echo "  -p    pid file location for daemon mode, default: $PID_FILE"
+
  -d    daemon mode, go into background (implies -q)
    echo "  -k    kill daemon (ignores all but -p)"
+
  -l    log to syslog
     exit 1
+
  -k     kill daemon (ignores all but -p)
 +
  -p    pid file location for daemon mode, default: $PID_FILE
 +
"
 +
     exit 1;
 
}
 
}
  
while getopts 'qtdlp:kh' OPT; do
+
while getopts 's:qtdlp:kh' OPT; do
 
     case "$OPT" in
 
     case "$OPT" in
 +
        s) # shift thresholds
 +
            MIN_THRESH_SHIFT="$OPTARG"
 +
            ;;
 
         t) # test mode
 
         t) # test mode
 
             DRY_RUN=true
 
             DRY_RUN=true
Line 121: Line 158:
  
 
thermometer() { # output list of temperatures
 
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 Y < $IBM_ACPI/thermal
 
     read X Y < $IBM_ACPI/thermal
     if ! [ "$X" == "temperatures:" ]; then
+
     [ "$X" == "temperatures:" ] || { echo "$0: Bad temperatures: $X $Y" >&2exit 1; }
        echo "$0: Bad temperatures: $X $Y" >&2
 
        exit 1
 
    fi
 
 
     echo -n "$Y ";
 
     echo -n "$Y ";
 +
    # Extended temperatures at EC offsets 0xC0 to 0xC2:
 +
    [ -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
 +
    # HDAPS temperature (optional):
 
     [ -r $HDAPS_TEMP ] && echo -n "`cat $HDAPS_TEMP` "
 
     [ -r $HDAPS_TEMP ] && echo -n "`cat $HDAPS_TEMP` "
 
     return 0
 
     return 0
Line 157: Line 197:
  
 
     IDX=0
 
     IDX=0
 +
    START_TIME=0
 
     MAX_IDX=$(( ${#LEVELS[@]} - 1 ))
 
     MAX_IDX=$(( ${#LEVELS[@]} - 1 ))
     SETTLE=0
+
     SETTLE_LEFT=0
 +
    RESETTLE_LEFT=0
 
     FIRST=true
 
     FIRST=true
 
     $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Starting dynamic fan control"
 
     $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Starting dynamic fan control"
Line 166: Line 208:
 
         TEMPS=`thermometer`
 
         TEMPS=`thermometer`
 
         $VERBOSE && SPEED=`speedometer`
 
         $VERBOSE && SPEED=`speedometer`
 +
        NOW=`date +%s`
 +
 
         # Calculate new level index by placing temperatures into Z-regions:
 
         # Calculate new level index by placing temperatures into Z-regions:
 
         # Z >= 2*I means "must be at index I or higher"
 
         # Z >= 2*I means "must be at index I or higher"
Line 172: Line 216:
 
         #  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-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 ? 2*(IDX-1) : 0 ))
+
        MAX_Z=$(( IDX>0 ? ( NOW>START_TIME+MIN_WAIT ? 2*(IDX-1) : 2*IDX ) : 0 ))
SENSOR=0
+
        SENSOR=0
Z_LIST=''
+
        Z_LIST="$MAX_Z+"
 
         for TEMP in $TEMPS; do
 
         for TEMP in $TEMPS; do
    [ $SENSOR -lt ${#MIN_TEMPS[@]} -a $SENSOR -lt ${#MAX_TEMPS[@]} ] ||
+
            [ $((2*SENSOR+2)) -le ${#THRESHOLDS[@]} ] ||
        { echo "Too many sensors, not enough values in MIN_TEMPS and MAX_TEMPS" 2>&1; exit 1; }
+
                { echo "Too many sensors, not enough values in THRESHOLDS" 2>&1; exit 1; }
             MIN=${MIN_TEMPS[$SENSOR]}; MAX=${MAX_TEMPS[$SENSOR]}
+
             MIN=$((THRESHOLDS[SENSOR*2] + MIN_THRESH_SHIFT));
    Z=$(( `floor_div $(( (TEMP-MIN)*(2*MAX_IDX-2) )) $((MAX-MIN))` + 2 ))
+
            MAX=$((THRESHOLDS[SENSOR*2+1]))
    Z_LIST="$Z_LIST $Z"
+
            if (( TEMP < MIN - OFF_THRESH_DELTA )); then
 +
                Z=0
 +
            else
 +
                Z=$(( `floor_div $(( (TEMP-MIN)*(2*MAX_IDX-2) )) $((MAX-MIN))` + 2 ))
 +
            fi
 +
            Z_LIST="${Z_LIST}${Z}"
 
             [ $MAX_Z -gt $Z ] || MAX_Z=$Z
 
             [ $MAX_Z -gt $Z ] || MAX_Z=$Z
    (( ++SENSOR ))
+
            (( ++SENSOR ))
 
         done
 
         done
[ $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
+
        (( (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
+
        [ $NEW_IDX -le $MAX_IDX ] || NEW_IDX=$MAX_IDX
  
 
         # Transition
 
         # Transition
         $FIRST && OLDLEVEL=unknown || OLDLEVEL=${LEVELS[$IDX]}
+
         $FIRST && OLDLEVEL='?' || OLDLEVEL=${LEVELS[$IDX]}
 
         NEWLEVEL=${LEVELS[$NEW_IDX]}
 
         NEWLEVEL=${LEVELS[$NEW_IDX]}
         $VERBOSE && echo "Level: $OLDLEVEL->$NEWLEVEL  Fan: $SPEED  Temps: $TEMPS" #"  Z:$Z_LIST"
+
         $VERBOSE && echo "L:$OLDLEVEL->$NEWLEVEL  Fan:`printf %4s $SPEED` T:($TEMPS) Z:$Z_LIST"
         $SYSLOG && [ $OLDLEVEL != $NEWLEVEL ] &&  
+
         if [ $OLDLEVEL != $NEWLEVEL ]; then
                $LOGGER -t "`basename $0`[$$]" "Changing fan level: $OLDLEVEL->$NEWLEVEL"
+
            START_TIME=$NOW
 +
            $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Changing fan level: $OLDLEVEL->$NEWLEVEL"
 +
        fi
  
 
         setlevel $NEWLEVEL
 
         setlevel $NEWLEVEL
   
+
 
 
         sleep $INTERVAL
 
         sleep $INTERVAL
   
+
 
         # If needed, apply anti-pulsing hack after a settle-down period:
+
         # If needed, apply anti-pulsing hack after a settle-down period (and occasionally re-settle):
         if [ ${ANTIPULSE[${NEW_IDX}]} == 1 ]; then
+
         if [ ${ANTIPULSE[${NEW_IDX}]} == 1 ]; then  
             if [ $NEWLEVEL == $OLDLEVEL ]; then
+
             if [ $NEWLEVEL != $OLDLEVEL -o $RESETTLE_LEFT -le 0 ]; then # start settling?
                 if [ $SETTLE -ge 0 ]; then
+
                SETTLE_LEFT=$SETTLE_TIME
                    (( SETTLE -= INTERVAL ))
+
                 RESETTLE_LEFT=$RESETTLE_TIME
                else
+
            fi
                    setlevel 0x40 # disengaged
+
            if [ $SETTLE_LEFT -ge 0 ]; then
                    sleep 0.5
+
                SETTLE_LEFT=$((SETTLE_LEFT-INTERVAL))
                fi
 
 
             else
 
             else
                 SETTLE=6
+
                 setlevel 0x40 # disengage briefly to fool embedded controller
 +
                sleep 0.5
 +
                RESETTLE_LEFT=$((RESETTLE_LEFT-INTERVAL))
 
             fi
 
             fi
 
         fi
 
         fi
   
+
 
 
         IDX=$NEW_IDX
 
         IDX=$NEW_IDX
FIRST=false
+
        FIRST=false
 
     done
 
     done
 
}
 
}
Line 245: Line 297:
 
fi
 
fi
 
</pre>
 
</pre>
 +
 +
Feedback very much welcome.
 +
 +
Spiney, I made the license more restrictive than prior versions (GPL+GFDL instead of public domains), is this OK with you?
 +
 +
--[[User:Thinker|Thinker]] 23:08, 27 Nov 2005 (CET)
 +
----

Revision as of 00:08, 28 November 2005

Wyrfel, are you sure the recent (19:54, 27 Oct 2005) cosmetic change was a good idea? The extensive chunks of code make it hard to grok the structure of the article in the absense of separator lines (which "===" doesn't have). --Thinker 22:10, 27 Oct 2005 (CEST)


We can discuss this. From my point of view, the chunks of code distinguish themselves from each other quite well, because they are each in one code block.

I do not like the = section level - and so far we avoided them on all pages - because

  • it generates H1 headings, which is the same as the page heading,
  • having more than one level with the hbars is confusing/less readable, because they are not very well distinguishable. This way i.e. the "Other" section looked like a separate empty secion.

I think the way it's now, the separator lines make it possible to easily distinguish the different main sections, while when you have both levels with separator lines, an additional task of distinguishing H1 and H2 separators is necessary.

However, i see your point as well and would like to hear more opinions/arguments.

Wyrfel 22:31, 27 Oct 2005 (CEST)


bash script with fine control over fan speed (for unpatched kernels)

Moved to the article page, after joint development by Spiney and Thinker.


Note that the fan levels, thresholds and anti-pulsing hacks are system-specific, so you may need to adjust them.

I think it'd probably be nice to have a table of the suggested values here. Those in the "unpatched kernels" script seems to work fine on my R52, but the other scripts all have different values.

--Micampe 08:19, 12 Nov 2005 (CET)

Sensor-specific variable-speed script

Here's a new variable-speed control script that lets you define the temperature range separately for each sensor. To keep things simple, it auto-computes the trip points (unlike the current script). Works well on a T43, and (just barely) keeps the fan off most of the time with CPU undervolting and fglrx set to maximum power saving. Feedback on other machines would be appreciated.

Any idea what are the sensors at EC offsets 0x79, 0x7A, 0xC0, 0xC1, 0xC2? On the T43, 0x79 seems to be the same as the HDAPS sensor (it never deviates by more than one degree from what the HDAPS sensor tells directly), but I don't know where it's located. Sensor 0x7A is uncorrelated with disk temperature and activity, so it can't be HDD like reported ofor R52. Sensor 0xC1 seems to be under the palm-rest (see discussion in Talk:Problem_with_fan_noise).

#!/bin/bash

# tp-fancontrol 0.2 (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.
#
# 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 evidene, hunches and wishful thinking.
# It is also model-specific.
#
# This file is placed in the public domain and may be freely distributed.

# 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      T43
# min  max   #  ---------- -------  -------
  50   70    #  EC 0x78    CPU      CPU
  47   57    #  EC 0x79    miniPCI  HDAPS (through EC)
  43   54    #  EC 0x7A    HDD      ???
  49   68    #  EC 0x7B    GPU      GPU
  37   52    #  EC 0x7C    BAT      BAT (SouthWest)
  45   55    #  EC 0x7D    n/a      n/a
  33   45    #  EC 0x7E    BAT      BAT (NorthEast)
  45   55    #  EC 0x7F    n/a      n/a
  45   55    #  EC 0xC0    ???      ???
  48   60    #  EC 0xC1    ???      mini-PCI (?)
  47   56    #  EC 0xC2    ???      RAM?
  47   57    #  HDAPS      HDAPS    HDAPS (direct)
)

LEVELS=(    0      2      4      7)  # Fan speed levels
ANTIPULSE=( 0      1      0      0)  # Prevent fan pulsing noise at this level
                                     # (reduces frequency of fan speed updates)

OFF_THRESH_DELTA=2 # when gets this much cooler than 'min' above, may turn off fan
MIN_THRESH_SHIFT=0 # increase min thresholds by this much
MIN_WAIT=120 # 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
SETTLE_TIME=6
RESETTLE_TIME=300

PID_FILE=/var/run/tp-fancontrol.pid
VERBOSE=true
DRY_RUN=false
DAEMONIZE=false
AM_DAEMON=false
KILL_DAEMON=false
SYSLOG=false

usage() {
    echo "
Usage: $0 [OPTION]...

Available options:
   -s N   shift up temperature thresholds by N degrees
   -t     test mode
   -q     quiet mode
   -d     daemon mode, go into background (implies -q)
   -l     log to syslog
   -k     kill daemon (ignores all but -p)
   -p     pid file location for daemon mode, default: $PID_FILE
"
    exit 1;
}

while getopts 's:qtdlp:kh' OPT; do
    case "$OPT" in
        s) # shift thresholds
            MIN_THRESH_SHIFT="$OPTARG"
            ;;
        t) # test mode
            DRY_RUN=true
            ;;
        q) # quiet mode
            VERBOSE=false
            ;;
        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
            ;;
        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."
    VERBOSE=true
    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 Y < $IBM_ACPI/thermal
    [ "$X" == "temperatures:" ] || { echo "$0: Bad temperatures: $X $Y" >&2;  exit 1; }
    echo -n "$Y ";
    # Extended temperatures at EC offsets 0xC0 to 0xC2:
    [ -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
    # HDAPS temperature (optional):
    [ -r $HDAPS_TEMP ] && echo -n "`cat $HDAPS_TEMP` "
    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
}

cleanup() { # clean up after work
    $AM_DAEMON && rm -f $PID_FILE 2> /dev/null
    $SYSLOG && $LOGGER -t "`basename $0`[$$]" \
               "Shutting down, switching to automatic fan control"
    $DRY_RUN || echo enable > $IBM_ACPI/fan
}

floor_div() {
    echo $(( (($1)+1000*($2))/($2) - 1000 ))
}

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

    IDX=0
    START_TIME=0
    MAX_IDX=$(( ${#LEVELS[@]} - 1 ))
    SETTLE_LEFT=0
    RESETTLE_LEFT=0
    FIRST=true
    $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Starting dynamic fan control"

    # Control loop:
    while true; do
        TEMPS=`thermometer`
        $VERBOSE && SPEED=`speedometer`
        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_LIST="$MAX_Z+"
        for TEMP in $TEMPS; do
            [ $((2*SENSOR+2)) -le ${#THRESHOLDS[@]} ] ||
                { echo "Too many sensors, not enough values in THRESHOLDS" 2>&1; exit 1; }
            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
            Z_LIST="${Z_LIST}${Z}"
            [ $MAX_Z -gt $Z ] || MAX_Z=$Z
            (( ++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

        # Transition
        $FIRST && OLDLEVEL='?' || OLDLEVEL=${LEVELS[$IDX]}
        NEWLEVEL=${LEVELS[$NEW_IDX]}
        $VERBOSE && echo "L:$OLDLEVEL->$NEWLEVEL  Fan:`printf %4s $SPEED`  T:($TEMPS) Z:$Z_LIST"
        if [ $OLDLEVEL != $NEWLEVEL ]; then
            START_TIME=$NOW
            $SYSLOG && $LOGGER -t "`basename $0`[$$]" "Changing fan level: $OLDLEVEL->$NEWLEVEL"
        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 ; then 
    if [ -f $PID_FILE ]; then
	set -e
	DPID="`cat $PID_FILE`" 
        kill "$DPID"
	rm "$PID_FILE"
	$VERBOSE && echo "Killed process $DPID"
    else
        $VERBOSE && 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 VERBOSE=false control_fan 0<&- 1>&- 2>&- &
        echo $! > "$PID_FILE"
        exit 0
    fi
else
    [ -e "$PID_FILE" ] && echo "WARNING: daemon already running"
    control_fan
fi

Feedback very much welcome.

Spiney, I made the license more restrictive than prior versions (GPL+GFDL instead of public domains), is this OK with you?

--Thinker 23:08, 27 Nov 2005 (CET)