Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 316 lines (270 sloc) 9.69 KB
#!/usr/bin/perl
# vi: sw=3 ai sm:
# WARNING: This has only been tested on my own MacBook (mid-2012 model)
#
# This script polls the system temperatures at 1-second intervals, and
# adjusts the fan speed accordingly to prevent overheating. The script
# features aggressive ramp-ups and extremely conservative ramp-downs;
# in other words we are working on the assumption that a loud computer
# is way better than an unusable one in a triple-beep situation.
use strict;
use integer;
use Getopt::Long;
###############################################################################
# Configurable parameters
# List of temperature sensors - I have no idea what most of these sensors are.
# I just know to be afraid if they read high.
my @sensors = qw(
TA0P TB0T TB1T TB2T TCGC TCSA TCTD TCXC TC0E
TC0F TC0J TC0P TC1C TC2C TG1D TM0P TM0S TPCD
);
my $fan_speed_min = 2500;
my $fan_speed_max = 6200;
my $ambient_threshold = 33;
my $temp_setpoint = 60; # beyond this is our definition of "too hot"
my $time_delta = 2; # poll interval
my $ramp_down_countdown_initial = 45;
my $ramp_down_delta_min = 25;
my $ramp_up_delta_min = 50;
my $log_interval = 15;
###############################################################################
use vars qw( $verbose_p );
GetOptions(
'verbose|v' => sub { $verbose_p += 1 },
) || exit(1);
use vars qw( $log_forced );
sub force_logging () {
$log_forced = 1;
}
sub unforce_logging () {
$log_forced = 0;
}
sub logging ($@) {
my($flag, $fmt, @args) = @_;
my $t = time;
my $msg = sprintf $fmt, @args;
chomp $msg;
printf STDERR "%u%s\t%s\n", $t, $flag, $msg if $verbose_p || $log_forced;
}
sub log_info {
logging '#', @_;
}
sub log_read {
logging 'r', @_;
}
sub log_write {
logging 'w', @_;
}
sub handler {
my($sig) = @_;
log_info "Caught a SIG$sig, shutting down\n";
$fan_speed_max = 6200 unless defined $fan_speed_max;
target_fan_speed($fan_speed_max) if $sig eq 'TERM';
exit(1);
}
$SIG{'HUP'} = sub { exec { $0 } $0 };
$SIG{'HUP'} = \&handler;
$SIG{'INT'} = \&handler;
$SIG{'QUIT'} = \&handler;
$SIG{'TERM'} = \&handler;
sub read_sensor ($) {
my($key) = @_;
my $pid = open(SENSORS, '-|');
die "fork: $!\n" unless defined $pid;
if (!$pid) {
my $sensors = '/usr/local/sbin/smc';
my @cmd = ($sensors, '-k', $key, '-r');
log_info("@cmd") if $verbose_p > 1;
exec {$sensors} @cmd;
die "$sensors: exec: $!\n";
}
my $data = scalar <SENSORS>;
log_read($data) if $verbose_p > 1;
close SENSORS;
my $it;
my $key_re = quotemeta($key);
if ($data =~ /$key_re \[....\] (\d+) \(.*\)/) {
$it = $1;
} elsif ($data =~ /(?=T)$key \[sp78\] \(bytes (.*)\)/) {
my @bytes = map { hex($_) } split(/ /, $1);
no integer;
$it = $bytes[0] + $bytes[1]/256.0 if $bytes[0] && $bytes[0] != 0xff;
} elsif ($data =~ /no data/) {
$it = undef;
}
return $it;
}
sub write_sensor ($$$) {
my($key, $type, $intval) = @_;
my $pid = open(SENSORS, '|-');
die "fork: $!\n" unless defined $pid;
if (!$pid) {
my $sensors = '/usr/local/sbin/smc';
my $code;
if ($type eq 'ui16') {
$code = sprintf "%04x", $intval;
} elsif ($type eq 'sp78') {
$code = sprintf "%04x", ($intval << 2);
} else {
die;
}
my @cmd = ($sensors, '-k', $key, '-w', $code);
log_info("@cmd") if $verbose_p > 1;
exec {$sensors} @cmd;
die "$sensors: exec: $!\n";
}
close SENSORS;
}
sub target_fan_speed (;$) {
my($speed) = @_;
if (!defined $speed) {
$speed = read_sensor('F0Tg');
} else {
write_sensor('FS! ', 'ui16', 1) unless read_sensor('FS! ') == 1;
write_sensor('F0Tg', 'sp78', $speed) unless read_sensor('F0Tg') == $speed;
}
return $speed;
}
################################################################################
my $fan_speed_setpoint = target_fan_speed;
my $ramp_down_countdown;
my @temp_history_short;
my @temp_history_long;
force_logging;
log_info "Temperature setpoint %d deg C, current fan speed setpoint %d", $temp_setpoint, $fan_speed_setpoint;
target_fan_speed $fan_speed_setpoint; # in case FS! has not been set
for (my($last_temp_high, $last_setpoint, $last_wait);;) {
my @temp = map { read_sensor($_) } @sensors;
my $temp_high = (sort {$b <=> $a} @temp)[0];
my $temp_ambient = read_sensor 'TA0P';
my $fan_speed = read_sensor('F0Ac');
# Keep track of previous temperature readings
unshift @temp_history_short, $temp_high;
unshift @temp_history_long, $temp_high;
splice @temp_history_short, (900/$time_delta + 1);
splice @temp_history_long, (900/$time_delta + 1);
# Calculate averages
my($avg_1min, $avg_5min, $avg_15min);
do {
my @avg = (
[\@temp_history_short, 60, 0, 0],
[\@temp_history_short, 300, 0, 0],
[\@temp_history_short, 900, 0, 0]
);
for (my $j = 0; $j < @avg; $j += 1) {
for (my $i = 0; $i < scalar @{$avg[$j]->[0]}; $i += 1) {
my $temp = $avg[$j]->[0]->[$i];
my $t = $i * $time_delta;
no integer;
if ($t < $avg[$j]->[1]) {
$avg[$j]->[2] += 1;
$avg[$j]->[3] += $temp;
}
}
}
no integer;
$avg_1min = $avg[0]->[3] / $avg[0]->[2];
$avg_5min = $avg[1]->[3] / $avg[1]->[2];
$avg_15min = $avg[2]->[3] / $avg[2]->[2];
};
# Calculate penalty for ramp-down
my $penalty = ($temp_ambient - $ambient_threshold + 1)/2;
$penalty = 0 if $penalty < 0;
$penalty += 1 if $avg_1min > $temp_setpoint;
$penalty += 1 if $avg_5min > $temp_setpoint;
$penalty += 1 if $avg_15min > $temp_setpoint;
# Periodic logging
force_logging if (time % $log_interval) == 0 || $temp_high > $temp_setpoint;
my $fmt_periodic = 'Temp %.1f(+%d) amb %.1f avg %.1f, %.1f, %.1f';
$fmt_periodic .= ($verbose_p > 1)? ', %d rpm': '%0.0s';
$fmt_periodic .= ' @%d';
if ($verbose_p > 1 || ($temp_high != $last_temp_high)
|| ($fan_speed_setpoint != $last_setpoint)) {
log_info $fmt_periodic, $temp_high, $penalty, $temp_ambient,
$avg_1min, $avg_5min, $avg_15min,
$fan_speed, $fan_speed_setpoint;
$last_temp_high = $temp_high;
$last_setpoint = $fan_speed_setpoint;
}
# Make sure the fan speed has not been changed by someone else
my $actual_setpoint = target_fan_speed;
if ($actual_setpoint != $fan_speed_setpoint) {
force_logging;
log_info "Setpoint changed to %d by an external agent", $actual_setpoint;
$fan_speed_setpoint = $actual_setpoint;
}
# Stage one of temperature/fan speed check. Check the temperature against
# our set point to determine our target fan speed.
my $det = $temp_high + $penalty;
my $need_to_emit_stabilize_msg = 0;
my $target_fan_speed;
# Temperature higher than our setpoint. The machine is getting too hot.
# This is critical.
if ($det > $temp_setpoint) {
# If we got too hot, forget about ramping down - that's too dangerous
undef $ramp_down_countdown;
if ($fan_speed > $fan_speed_setpoint) { # set point too low
my $delta = ($fan_speed_max - $fan_speed_setpoint)/3;
$delta = $ramp_up_delta_min if $delta < $ramp_up_delta_min && $fan_speed_setpoint + $ramp_up_delta_min <= $fan_speed_max;
$delta = 1 if $delta < 1;
$target_fan_speed = $fan_speed_setpoint + $delta;
$target_fan_speed = $fan_speed_max if $target_fan_speed > $fan_speed_max;
} elsif ($fan_speed < $fan_speed_setpoint && $fan_speed_setpoint < $fan_speed_max) {
$need_to_emit_stabilize_msg = 1;
}
# Temperature lower than our setpoint, but might still be higher than our
# target temperature curve (if we can call it that)
} elsif ($det < $temp_setpoint) { # too cool? maybe? :-P
my $delta = 15*($temp_setpoint - $avg_1min)
+ 5*($temp_setpoint - $avg_5min)
+ 1*($temp_setpoint - $avg_15min)
+ 5*($temp_high - $avg_5min);
if ($delta < 1 || $delta < $ramp_down_delta_min) {
$delta = 1;
if ($fan_speed_setpoint - $ramp_down_delta_min >= $fan_speed_min) {
$delta = $ramp_down_delta_min;
}
}
$target_fan_speed = $fan_speed_setpoint - $delta;
# http://forums.macrumors.com/archive/index.php/index/t-1324246.html
$target_fan_speed = 5000 if $target_fan_speed < 5000 && $temp_high >= 50;
}
# Stage two of temperature/fan speed check. Check the fan speed against
# our target to determine if we are ramping up or down
if (!defined $target_fan_speed || $target_fan_speed == $fan_speed_setpoint) {
;
# Ramp up immediately if it looks like we need to
} elsif ($target_fan_speed > $fan_speed_setpoint) {
force_logging;
log_info "Ramping up to $target_fan_speed\n";
$fan_speed_setpoint = target_fan_speed($target_fan_speed);
undef $last_wait;
# Ramping down is kind of a lower priority since it's kind of dangerous.
} elsif ($target_fan_speed < $fan_speed_setpoint) {
if ($target_fan_speed >= $fan_speed_min) {
if (!defined $ramp_down_countdown) {
# Monitor the situation for a while before we believe we really
# are probably too cool and maybe we should ramp down the fan.
$ramp_down_countdown = $ramp_down_countdown_initial;
log_info "Setting ramp-down countdown to $ramp_down_countdown";
} else {
$ramp_down_countdown -= $time_delta;
if ($ramp_down_countdown <= 0) {
force_logging;
log_info "Ramping down to $target_fan_speed";
$fan_speed_setpoint = target_fan_speed($target_fan_speed);
@temp_history_short = ();
undef $last_wait;
undef $ramp_down_countdown;
}
}
}
}
if ($need_to_emit_stabilize_msg) {
log_info "Waiting for fan to stabilize at $fan_speed_setpoint\n"
unless defined $last_wait && $fan_speed_setpoint == $last_wait;
$last_wait = $fan_speed_setpoint;
}
unforce_logging;
sleep($time_delta);
}