#!/usr/bin/perl
##########################################################################
# Thinkpad ACPI Battery Control
# Copyright 2011 Elliot Wolk
##########################################################################
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
##########################################################################
# Exposes inhibit charge, start/stop charge threshold, and force discharge
# through ACPI as an alternative to SMAPI, which is broken in W520, etc.
#
# Makes ACPI calls using the acpi_call kernel module, which is REQUIRED.
#
# Supports 2011-released thinkpads, and possibly later ones.
# Tested and works on: W520, T420, X220
# Tested and does NOT work on: T400, X201S
#
# Limitations/Known Issues:
# 1) you cant force discharge on battery, so balancing is still impossible
# 2) sometimes you cant convince a slice battery to charge before the main
# ANYONE who figures out this second issue gets a cookie!
# if my main is above 80, and my slice is at 80, my main charges.
# i can inhibit the main, but i CANNOT trick the slice into charging
# 3) you can only inhibit for 720 minutes instead of 1440 minutes
# this seems like a BIOS error
##########################################################################
use strict;
use warnings;
use File::Basename;
my $acpiCallDev = '/proc/acpi/call';
my @aslBases = (
'\_SB.PCI0.LPC.EC.HKEY',
'\_SB.PCI0.LPC0.EC0.HKEY',
'\_SB.PCI0.LPCB.EC.HKEY',
'\_SB.PCI0.LPCB.EC0.HKEY',
'\_SB.PCI0.LPCB.H_EC.HKEY',
'\_SB.PCI0.LPC0.EC.HKEY',
);
sub getMethod($$);
sub setMethod($$@);
sub readPeakShiftState($);
sub readInhibitCharge($);
sub readStartChargeThreshold($);
sub readStopChargeThreshold($);
sub readForceDischarge($);
sub writePeakShiftState($);
sub writeInhibitCharge($);
sub writeStartChargeThreshold($);
sub writeStopChargeThreshold($);
sub writeForceDischarge($);
sub getASLBase();
sub acpiCall($);
sub runAcpiCall($$);
sub acpiCallGet($$);
sub acpiCallSet($$);
sub revpadzero($$);
sub bitRangeToDec(\@$$);
sub parseArgDecToBin($);
sub parseStatusHexToBitArray($);
sub decToBin($);
sub binToDec($);
sub hexToBin($);
sub binToHex($);
sub synList(@);
our $verbose = 0;
my ($methodST, $methodSP, $methodIC, $methodFD, $methodPS) =
("ST", "SP", "IC", "FD", "PS");
my $methodSyns = {
$methodST => synList ("st", "startThreshold", "start"),
$methodSP => synList ("sp", "stopThreshold", "stop"),
$methodIC => synList ("ic", "inhibitCharge", "inhibit"),
$methodFD => synList ("fd", "forceDischarge"),
$methodPS => synList ("ps", "peakShiftState"),
};
my $name = File::Basename::basename $0;
my $usage = "Usage:
Show this message:
$name [-h|--help]
Get charge thresholds / inhibit charge / force discharge:
$name [-v] -g $methodST
$name [-v] -g $methodSP
$name [-v] -g $methodIC
$name [-v] -g $methodFD
Set charge thresholds / inhibit charge / force discharge:
$name [-v] -s $methodST
$name [-v] -s $methodSP
$name [-v] -s $methodIC []
$name [-v] -s $methodFD []
Set peak shift state, which is mysterious and inhibits charge:
$name [-v] -s $methodPS []
Synonyms:
$methodST -> $$methodSyns{$methodST}
$methodSP -> $$methodSyns{$methodSP}
$methodIC -> $$methodSyns{$methodIC}
$methodFD -> $$methodSyns{$methodFD}
$methodPS -> $$methodSyns{$methodPS}
Options:
-v show ASL call and response
1 for main, 2 for secondary, 0 for either/both
number of minutes, or 0 for never, or 65535 for forever
0 for default, 1-99 for percentage
1 for inhibit charge, 0 for stop inhibiting charge
1 for force discharge, 0 for stop forcing discharge
1 for stop forcing when AC is detached, 0 for do not
[] means optional: sets value to 0
";
my $noReadMethods = join "|", ($methodPS);
my $noBothReadMethods = join "|", ($methodST, $methodSP, $methodFD);
my $noBothWriteMethods = join "|", ($methodFD);
sub main(@){
if(@_ == 1 and $_[0] =~ /^(-h|--help)$/){
print $usage;
exit 0;
}
if(@_ > 0 and $_[0] eq '-v'){
$verbose = 1;
shift;
}
my $cmd = shift() || '';
my $method;
my $methodSyn = shift() || '';
for my $m(keys %$methodSyns){
$method = $m if $methodSyn eq $m or $methodSyn =~ /^($$methodSyns{$m})$/;
}
die $usage if not defined $method or $cmd !~ /^(-g|-s)$/;
my $bat;
if($method eq $methodPS){
$bat = 0;
}else{
$bat = shift;
}
die " missing or incorrect\n" if not defined $bat or $bat !~ /^0|1|2$/;
if($cmd eq '-g' and @_ == 0){
print getMethod($method, $bat) . "\n";
}elsif($cmd eq '-s'){
print setMethod($method, $bat, @_);
}else{
die $usage;
}
}
sub getMethod($$){
my $method = shift;
my $bat = shift;
if($method =~ /^($noReadMethods)$/){
die "Cannot read $method\n";
}
if($bat == 0 and $method =~ /^($noBothReadMethods)$/){
die "Cannot specify 'either/both' for reading $method\n";
}
$bat = parseArgDecToBin $bat;
if($method eq $methodST){
return readStartChargeThreshold(acpiCallGet 'BCTG', $bat);
}elsif($method eq $methodSP){
return readStopChargeThreshold(acpiCallGet 'BCSG', $bat);
}elsif($method eq $methodIC){
#this is actually reading peak shift state
return readInhibitCharge(acpiCallGet 'PSSG', $bat);
}elsif($method eq $methodFD){
return readForceDischarge(acpiCallGet 'BDSG', $bat);
}else{
die $usage;
}
}
sub setMethod($$@){
my $method = shift;
my $bat = shift;
if($bat == 0 and $method =~ /^($noBothWriteMethods)$/){
die "Cannot specify 'either/both' for writing $method\n";
}
my %info;
$info{bat} = $bat;
if($method =~ /^($methodIC|$methodPS)$/){
$info{inhibit} = shift @_;
if(not defined $info{inhibit} or $info{inhibit} !~ /^0|1$/){
die "missing or invalid value for \n";
}
$info{min} = shift @_;
$info{min} = 0 if not defined $info{min};
############################################################
#they are shifting a bit somewhere; the limit should be 1440
#the same range in peak-shift-state is used, except shifted to the left
#the value returned by peak-shift-state is the REAL duration, though
$info{min} *= 2 if $method eq $methodIC;
############################################################
if($info{min} !~ /^\d+$/ or ($info{min} > 1440 and $info{min} != 65535)){
die "invalid value for \n";
}
}elsif($method =~ /^($methodFD)$/){
$info{discharge} = shift @_;
if(not defined $info{discharge} or $info{discharge} !~ /^0|1$/){
die "missing or invalid value for \n";
}
$info{acbreak} = shift @_;
$info{acbreak} = 0 if not defined $info{acbreak};
if($info{acbreak} !~ /^0|1$/){
die "invalid value for \n";
}
}elsif($method =~ /^($methodST|$methodSP)$/){
$info{percent} = shift @_;
if(not defined $info{percent} or $info{percent} !~ /^\d+$/ or $info{percent} > 99){
die "missing or invalid value for \n";
}
}
die $usage if @_ > 0;
%info = map {$_ => parseArgDecToBin $info{$_}} keys %info;
if($method eq $methodST){
acpiCallSet 'BCCS', writeStartChargeThreshold(\%info);
}elsif($method eq $methodSP){
acpiCallSet 'BCSS', writeStopChargeThreshold(\%info);
}elsif($method eq $methodIC){
acpiCallSet 'BICS', writeInhibitCharge(\%info);
}elsif($method eq $methodFD){
acpiCallSet 'BDSS', writeForceDischarge(\%info);
}elsif($method eq $methodPS){
acpiCallSet 'PSSS', writePeakShiftState(\%info);
}else{
die $usage;
}
}
sub readInhibitCharge($){
my @bits = parseStatusHexToBitArray $_[0];
if($bits[5] != 1){
die "\n";
}
my $val;
if($bits[0] == 1){
$val = "yes";
my $min = bitRangeToDec @bits, 8, 23;
if($min == 0){
$val .= " (unspecified min)";
}elsif($min == 65535){
$val .= " (forever)";
}else{
$val .= " ($min min)";
}
}else{
$val = "no";
}
return $val;
}
sub readStartChargeThreshold($){
my @bits = parseStatusHexToBitArray $_[0];
if($bits[8] != 1 and $bits[9] != 1){
die "\n";
}
my $val = bitRangeToDec @bits, 0, 7;
if($val == 0){
$val .= " (default)";
}elsif($val > 0 and $val < 100){
$val .= " (relative percent)";
}else{
$val .= " (unknown)";
}
return $val;
}
sub readStopChargeThreshold($){
my @bits = parseStatusHexToBitArray $_[0];
if($bits[8] != 1 and $bits[9] != 1){
die "\n";
}
my $val = bitRangeToDec @bits, 0, 7;
if($val == 0){
$val .= " (default)";
}elsif($val > 0 and $val < 100){
$val .= " (relative percent)";
}else{
$val .= " (unknown)";
}
return $val;
}
sub readForceDischarge($){
my @bits = parseStatusHexToBitArray $_[0];
if($bits[8] != 1 and $bits[9] != 1){
die "\n";
}
my $val;
if($bits[0] == 1){
$val = 'yes';
}else{
$val = 'no';
}
if($bits[1] == 1){
$val .= ' (break on AC detach)';
}
return $val;
}
sub writePeakShiftState($){
my $info = shift;
return reverse ''
. revpadzero( 1, $$info{inhibit})
. revpadzero( 3, 0)
. revpadzero( 4, 0)
. revpadzero(16, $$info{min})
. revpadzero( 8, 0)
;
}
sub writeInhibitCharge($){
my $info = shift;
return reverse ''
. revpadzero( 1, $$info{inhibit})
. revpadzero( 3, 0)
. revpadzero( 2, $$info{bat})
. revpadzero( 2, 0)
. revpadzero(16, $$info{min})
. revpadzero( 8, 0)
;
}
sub writeStartChargeThreshold($){
my $info = shift;
return reverse ''
. revpadzero( 8, $$info{percent})
. revpadzero( 2, $$info{bat})
. revpadzero(22, 0)
;
}
sub writeStopChargeThreshold($){
my $info = shift;
return reverse ''
. revpadzero( 8, $$info{percent})
. revpadzero( 2, $$info{bat})
. revpadzero(22, 0)
;
}
sub writeForceDischarge($){
my $info = shift;
return reverse ''
. revpadzero( 1, $$info{discharge})
. revpadzero( 1, $$info{acbreak})
. revpadzero( 6, 0)
. revpadzero( 2, $$info{bat})
. revpadzero(22, 0)
;
}
sub acpiCall($){
my $call = shift;
if(not -e $acpiCallDev){
$ENV{'PATH'} = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
system "modprobe acpi_call";
}
if(not -e $acpiCallDev){
die "Could not find $acpiCallDev. Is module acpi_call loaded?\n";
}
my $val;
for my $aslBase(@aslBases) {
$val = runAcpiCall $call, $aslBase;
if($val !~ /Error: AE_NOT_FOUND/){
print "Call : $aslBase.$call\n" if $verbose;
print "Response: $val\n" if $verbose;
last;
}else{
$val = undef;
}
}
die "Error: no ASL base found\n" if not defined $val;
return $val;
}
sub runAcpiCall($$){
my ($call, $aslBase) = @_;
open FH, "> $acpiCallDev" or die "Cannot write to $acpiCallDev: $!";
print FH "$aslBase.$call\n";
close FH;
open FH, "< $acpiCallDev" or die "Cannot read $acpiCallDev: $!";
my $val = ;
close FH;
return $val;
}
sub acpiCallGet($$){
my ($method, $bits) = @_;
my $call = "$method 0x" . binToHex($bits);
my $val = acpiCall $call;
if($val eq '0x80000000'){
die "Call failure status returned: $val";
}
return $val;
}
sub acpiCallSet($$){
my ($method, $bits) = @_;
my $call = "$method 0x" . binToHex($bits);
my $val = acpiCall $call;
if($val eq '0x80000000'){
die "Call failure status returned: $val";
}
}
sub revpadzero($$){
return reverse ('0' x ($_[0] - length $_[1]) . $_[1]);
}
sub bitRangeToDec(\@$$){
my @bits = @{shift()};
my $start = shift;
my $end = shift;
my $bin = reverse(join '', @bits[$start .. $end]);
return binToDec $bin;
}
sub parseArgDecToBin($){
my $dec = shift;
die "not a positive integer: " . $dec . "\n\n$usage" if $dec !~ /^\d+$/;
return decToBin $dec;
}
sub parseStatusHexToBitArray($){
my $hex = shift;
if($hex !~ /0x([0-9a-f]+)/i){
my $msg = "Bad status returned: $hex\n";
if($hex =~ /Error: AE_NOT_FOUND/){
$msg .= ''
. "ASL base not found for this machine\n"
. " {perhaps it does not have the ThinkPad ACPI interface}\n"
. "ASL bases checked:\n "
. (join "\n ", @aslBases)
. "\n"
;
}
die $msg;
}
return split //, revpadzero 32, hexToBin($1);
}
sub decToBin($){
my $bits = unpack("B32", pack("N", $_[0]));
$bits =~ s/^0*//;
return $bits;
}
sub binToDec($){
return oct "0b$_[0]";
}
sub hexToBin($){
return decToBin(oct "0x$_[0]");
}
sub binToHex($){
return sprintf("%x", binToDec $_[0]);
}
sub synList(@){
return join "|", ((map {"--$_"} @_), @_);
}
&main(@ARGV);