|
|
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
| |