Difference between revisions of "Script for theft alarm using HDAPS"

From ThinkWiki
Jump to: navigation, search
(Yet another script (python/gtk based))
(The script: Moved to Code/)
Line 27: Line 27:
  
 
===The script===
 
===The script===
<pre>
+
{{CodeRef|tp-theft}}
#!/usr/bin/perl
 
#
 
# tp-theft v0.4.0
 
# (http://thinkwiki.org/wiki/Script_for_theft_alarm_using_HDAPS)
 
 
 
# 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 uses the HDAPS accelerometer found on recent ThinkPad models
 
# to emit an audio alarm when the laptop is tilted. In sufficiently
 
# populated environments, it can be used as a laptop theft deterrent.
 
# Uses a state machine and some heuristics to reduce false alarms.
 
#
 
# By default the alarm will be activated only when the KDE screen saver is
 
# locked. If you you open the laptop lid (or press the lid button) shortly
 
# before or after the beginning of movement, the alarm will be suspended
 
# (except for a brief warning) and you will get a few seconds of grace to
 
# unlock the screen saver. You can disable this functionality by passing
 
# the "--arm" parameter, or by setting  $use_kde=0  and  $use_lid=0 .
 
# To control the sound and blinkenlights, see the variables below.
 
 
 
use strict;
 
use warnings;
 
use FileHandle;
 
use Time::HiRes qw(sleep time);
 
 
 
##############################
 
# Siren volume and content
 
 
 
# Alarm audio volume (0..100)
 
my $alarm_volume = 70;
 
# Alarm command (default: synthesize a siren for 1.0 seconds):
 
my $alarm_cmd = "sox -t nul /dev/null -t wav -s -w -c2 -r48000 -t raw - synth 1.0 sine 2000-4000 sine 4000-2000 | aplay -q -fS16_LE -c2 -r48000";
 
# my $alarm_cmd = "aplay keep_your_hands_off_me.wav";
 
 
 
# Warning audio volume (0..100)
 
my $warn_volume = 45;
 
# Alarm command (default: synthesize a biref siren):
 
my $warn_cmd = "sox -t nul /dev/null -t wav -s -w -c2 -r48000 -t raw - synth 0.05 sine 2000-4000 sine 4000-2000 | aplay -q -fS16_LE -c2 -r48000";
 
# my $warn_cmd = "aplay warning.wav";
 
 
 
# Set ibm_acpi volume (0..15), if ibm_acpi is loaded with "experimental=1".
 
# Combining $acpi_volume=15 and $alarm_volume=100 makes the alarm
 
# dangerously loud.
 
my $acpi_volume = 10;
 
 
 
# Blink system LEDs when alarm activated?
 
my $use_led = 'safe';  # 0=off, 'safe'=only LEDs whose state you can recover, 'all'=pretty blinkenlights!
 
 
 
# Blink ThinkLight when alarm activated?
 
my $use_light = 0;  # 0=off, 1=on
 
 
 
##############################
 
# Activation control
 
 
 
# Tilt threshold (increase value to decrease sensitivity):
 
my $thresh = 0.20;
 
# Minimum movement duration between warning and alarm:
 
my $min_hold = 1.3;
 
# When armed, any movement triggers alarm. How long should it remain armed?
 
my $arm_persist = 6;
 
 
 
# Activate according to KDE screen saver? Otherwise, always active:
 
my $use_kde = 1;
 
# When screen saver locked, wait this long before activation:
 
my $kde_lock_delay =  8;
 
 
 
# Provide grace period if laptop lid is opened?
 
my $use_lid = 1;
 
# Opening a lid will grant this many seconds of grace (once):
 
my $lid_grace = 7;
 
# Lid must to be opened within this time to hold/pause alarm:
 
my $lid_grace_window = 8;
 
# Alarm will hold off this long when grace is available:
 
my $lid_hold = 3;
 
# After this many seconds of no movement, will allow grace again:
 
my $grace_relax = 15;
 
 
 
 
 
##############################
 
# Other setup vars
 
 
 
my $interval = 0.1;  # sampling intervalm in seconds
 
my $depth = 10;      # number of recent samples to analyze
 
my $verbose = 2;    # 0=nothing, 1=alarms, 2=state transitions, 3=everything
 
my $kde_check_interval = 1.5; # KDE screen saver check is expensive
 
 
 
my $pos_file = '/sys/devices/platform/hdaps/position';
 
my $lid_file = '/proc/acpi/button/lid/LID/state';
 
my $led_file = '/proc/acpi/ibm/led';
 
my $light_file = '/proc/acpi/ibm/light';
 
my $bay_file = '/proc/acpi/ibm/bay';
 
my $volume_file = '/proc/acpi/ibm/volume'; # load ibm_acpi with experimental=1
 
 
 
my $alsactl = '/usr/sbin/alsactl';
 
my $amixer = 'amixer';
 
my $kdesktop_lock = '/usr/bin/kdesktop_lock';
 
 
 
##############################
 
# Utility functions
 
 
 
sub say {
 
    my ($verb, $what) = @_;
 
    print(gmtime().": $what\n") if $verb<=$verbose;
 
}
 
 
 
sub slurp { # read whole file
 
    my ($filename) = @_;
 
    local $/;
 
    my $fh = new FileHandle($filename,"<") or return;
 
    return <$fh>;
 
}
 
 
 
sub stddev { # standard deviation of list
 
    my $sum=0;
 
    my $sumsq=0;
 
    my $n=$#_+1;
 
    for my $v (@_) {
 
        $sum += $v;
 
        $sumsq += $v*$v;
 
    }
 
    return sqrt($n*$sumsq - $sum*$sum)/($n*($n-1));
 
}
 
 
 
my $alarm_file; # flags ongoing alarm (and also stores saved mixer settings)
 
 
 
sub sound_alarm {
 
    # Sound alarm. Forks bash code which sets given volumes, runs the given
 
    # command, and then restores the given volumes to their saved values.
 
    my ($name, $volume, $acpi_volume, $cmd) = @_;
 
    return if (defined($alarm_file) && -f $alarm_file);
 
    say(1,$name);
 
    $alarm_file = `mktemp /tmp/tp-theft-tmp.XXXXXXXX` or die "mktemp: $?";
 
    chomp($alarm_file);
 
    my ($acpi_vol_file, $acpi_vol_set, $acpi_vol_restore);
 
    if ($_=slurp($volume_file) and m/^level:\s+(\d+)\n/) {
 
        $acpi_vol_file = $volume_file;
 
        $acpi_vol_set = "level $acpi_volume";
 
        $acpi_vol_restore = "level $1";
 
        if (m/^mute:\s+on$/m) {
 
          $acpi_vol_set = "up,".$acpi_vol_set; # unmute first
 
          $acpi_vol_restore .= ",mute";        # mute last
 
        }
 
    } else {
 
        $acpi_vol_file='/dev/null'; $acpi_vol_set=''; $acpi_vol_restore='';
 
    }
 
    system('/bin/bash', '-c', <<"EOF")==0 or die "Failed: $?";
 
( trap \"echo '$acpi_vol_restore' > $acpi_vol_file; sleep 0.1;
 
        $alsactl -f $alarm_file restore;
 
        rm -f $alarm_file
 
      \" EXIT HUP QUIT TERM
 
  $alsactl -f $alarm_file store &&                        # store ALSA
 
  echo '$acpi_vol_set' > $acpi_vol_file && sleep 0.1 &&    # set ACPI
 
  $amixer -q set Master $volume% unmute &&                # set ALSA Master
 
  $amixer -q set PCM 100% unmute &&                        # set alsa PCM
 
  $cmd ) &                                                # invoke command
 
EOF
 
}
 
 
 
 
 
##############################
 
# KDE screen saver lock check
 
 
 
if ($use_kde) { # Basic sanity check
 
    `/sbin/pidof kdesktop`; $?==0 or die "Can't use KDE, it's not running.\n";
 
}
 
 
 
sub kdesktop_lock_status {
 
    # See if kdesktop_lock is running and check its cmdline and automatic lock delay
 
    my $bin = $kdesktop_lock;
 
    my $pids = `/sbin/pidof $bin`;
 
    return 'off' unless $?==0;
 
    for my $pid (split(/\s+/,$pids)) {
 
        next unless $pid =~ m/^\d+$/;
 
        # Attached to display ":0" or "localhost:0"?
 
        my $environ = slurp("/proc/$pid/environ") or next;
 
        my $good=0; my $home;
 
        for (split(/\x00/,$environ)) {
 
            $good=1 if m/^DISPLAY=(localhost)?:0$/;
 
            $home=$1 if m/^HOME=(.+)$/;  # also remember its $HOME
 
        }
 
        next unless $good;
 
        # Check command line
 
        my $cmdline = slurp("/proc/$pid/cmdline") or next;
 
        $cmdline =~ m/^[^\x00]+\x00(?:([^\x00]+)\x00)?/ or die "Cannot parse $bin command line\n";
 
        if (!defined($1)) {
 
            # Read KDE screensaver lock time
 
            defined($home) or die "Cannot find HOME in environment of $bin process";
 
            my $rc_path = "$home/.kde/share/config/kdesktoprc";
 
            my $rc = new FileHandle($rc_path,"<") or die "Error opening $rc_path: $!";
 
            while (<$rc>) { m/^LockGrace=(\d+)$/ and return ('auto', $1/1000.0); };
 
            die "Cannot parse $rc_path";
 
        } elsif ($1 eq '--forcelock') {
 
            return "force";
 
        }
 
    }
 
    return 'off';
 
}
 
 
 
my $last_kls_update = 0; # time of last update
 
my $last_kls = 'init';  # last state seen
 
my $last_kls_start;      # when that state started
 
 
 
sub check_kde_lock {
 
    # De/activate according to KDE screen saver:
 
    my $now=time();
 
    return if $now < $last_kls_update + $kde_check_interval;
 
    my ($kls, $auto_delay) = kdesktop_lock_status();
 
    $last_kls_update = time();
 
    if ($kls ne $last_kls) {
 
        $last_kls = $kls;
 
        $last_kls_start = $now;
 
    }
 
    if ($kls eq 'off') { # no screen saver
 
        return(0, 'KDE screen saver not locked');
 
    } elsif ($kls eq 'auto') { # screen saver with automatic lock
 
        if ($now >= $last_kls_start + $auto_delay + $kde_lock_delay) {
 
            return(1, 'KDE screen saver is auto-locked');
 
        }
 
    } elsif ($kls eq 'force') { # screen saver with forced lock
 
        if ($now >= $last_kls_start + $kde_lock_delay) {
 
            return(1, 'KDE screen saver is forced-locked');
 
        }
 
    }
 
}
 
 
 
 
 
##############################
 
# Lid checking
 
 
 
if ($use_lid) { # sanity check
 
    slurp($lid_file) or die "Can't use lid via $lid_file: $!";
 
}
 
 
 
my $last_lid_status = 'open';
 
my $last_lid_open = 0;
 
 
 
sub check_lid {
 
    my $lid = slurp($lid_file) or return;
 
    if ($lid =~ m/state: *open$/) {
 
        $last_lid_open = time() if ($last_lid_status eq 'closed');
 
        $last_lid_status = 'open';
 
    } else {
 
        $last_lid_status = 'closed';
 
    }
 
    return $last_lid_status;
 
}
 
 
 
 
 
##############################
 
# LED blinking
 
 
 
sub frac {
 
    my ($x) = @_;
 
    return $x-int($x);
 
}
 
 
 
sub led_activate {
 
    return if $use_led eq '0';
 
    my $ledf = new FileHandle($led_file,">");
 
    if (!defined($ledf)) {
 
        print "Cannot open $led_file, disabling LED indicator: $!\n";
 
        $use_led = '0';
 
        return;
 
    }
 
    $ledf->autoflush(1);
 
    my $base = time()*2.5;
 
    print $ledf '0 '.((frac($base)>0.7)?'on':'off')."\n"; # power
 
    if ($use_led eq 'all') { # battery -- we can't recover these
 
        print $ledf '1 '.((frac($base+0.50)>0.7)?'on':'off')."\n"; # battery, orange
 
        print $ledf '2 '.((frac($base+0.25)>0.7)?'on':'off')."\n"; # battery, yellow
 
    }
 
    print $ledf '4 '.((frac($base)>0.7)?'on':'off')."\n"; # bay
 
    print $ledf '7 '.((frac($base+0.725)>0.7)?'on':'off')."\n"; # standby
 
}
 
 
 
sub led_restore {
 
    return if $use_led eq '0';
 
    my $ledf = new FileHandle($led_file,">") or die "Cannot open $led_file: $!\n";
 
    $ledf->autoflush(1);
 
    print $ledf "0 on\n"; # power=on
 
    if ($use_led eq 'all') { # battery -- we can't recover these
 
        print $ledf "1 on\n";
 
        print $ledf "2 on\n";
 
    }
 
    print $ledf "7 off\n"; # power=off
 
    my $baydata = slurp($bay_file) or die "Cannot open $bay_file: $!\n";
 
    my $is_bay = ($baydata =~ m/^status:\s*occupied$/m)?'on':'off';
 
    print $ledf "4 $is_bay\n"; # bay
 
}
 
 
 
 
 
##############################
 
# ThinkLight blinking
 
 
 
sub light_activate {
 
    return if $use_light eq '0';
 
    my $lightf = new FileHandle($light_file,">");
 
    if (!defined($lightf)) {
 
        print "Cannot open $light_file, disabling ThinkLight indicator: $!\n";
 
        $use_light = '0';
 
        return;
 
    }
 
    my $base = time()/2;
 
    print $lightf (check_lid() eq 'open' && ((frac($base)<0.1)?'on':'off'))."\n";
 
}
 
 
 
sub light_restore {
 
    return if $use_light eq '0';
 
    my $lightf = new FileHandle($light_file,">") or die "Cannot open $light_file: $!\n";
 
    print $lightf "off\n"; # ThinkLight
 
}
 
 
 
##############################
 
# Main code
 
 
 
my $state;
 
my %state_names=(0  =>'disabled    ',
 
                0.5=>'activating  ',
 
                1  =>'active+grace',
 
                2  =>'active      ',
 
                3  =>'hold+grace  ',
 
                4  =>'armed+grace ',
 
                5  =>'hold        ',
 
                6  =>'armed      ',
 
                7  =>'armed-force '  );
 
my $state_end = 0;
 
my $last_tilt = 0;
 
my $arm_forced = 0;
 
my (@XHIST, @YHIST); # sensor history
 
 
 
sub set_state {
 
    my ($st,  $why) = @_;
 
    say(2, "state=[".$state_names{$st}."]  ($why)");
 
    (@XHIST, @YHIST) = () if $st==0.5;
 
    led_restore() and light_restore() if defined($state) && $st==0;
 
    $state = $st;
 
}
 
 
 
sub get_pos {
 
    my $pos = slurp($pos_file) or die "Can't open HDAPS file $pos_file: $!\n";
 
    $pos =~ m/^\((-?\d+),(-?\d+)\)$/ or die "Can't parse $pos_file content\n";
 
    return ($1,$2);
 
}
 
 
 
 
 
for (@ARGV) {
 
    m/^--arm/ && do { $arm_forced=1; $use_lid=0; $use_kde=0; last; };
 
    die "Unknown parameter\n";
 
}
 
 
 
set_state($use_kde?0:0.5, "init");
 
 
 
eval {
 
 
 
$SIG{'HUP'}=$SIG{'INT'}=$SIG{'ABRT'}=$SIG{'QUIT'}=$SIG{'SEGV'}=$SIG{'TERM'} = sub { die "signal\n" };
 
 
 
while (1) {
 
    sleep(($state==0 && $use_kde) ? $kde_check_interval : $interval);
 
 
 
    # Check screensaver and lid:
 
    check_lid() if $use_lid;
 
    if ($use_kde && (my ($op, $why) = check_kde_lock())) {
 
        set_state(0.5, $why) if $op==1 && $state==0;
 
        set_state(0, $why) if $op==0 && $state>0;
 
    }
 
    next unless $state>0;
 
 
 
    # Collect and analyze sensor data:
 
    my $now = time();
 
    my $tilted;
 
    my ($x,$y) = get_pos;
 
    push(@XHIST,$x); push(@YHIST,$y);
 
    if ($state>0.5) {
 
        shift(@XHIST); shift(@YHIST);
 
        my $xdev = stddev(@XHIST);
 
        my $ydev = stddev(@YHIST);
 
        say(3,"X: v=$xdev (".join(',',@XHIST).")  Y: v=$ydev (".join(",",@YHIST).")");
 
        $tilted = ($xdev>$thresh || $ydev>$thresh);
 
        $last_tilt = $now if $tilted;
 
    }
 
 
 
    # Decide: state machine transitions
 
    if ($state==0.5) { # activating
 
        if ($#XHIST >= $depth && $#YHIST >= $depth) {
 
            set_state($arm_forced?7:$use_lid?1:2, "finished data collection");
 
        }
 
    } elsif ($state==1) { # active+grace
 
        if ($tilted) {
 
            set_state(3, "motion detected, holding for $lid_hold seconds, open lid for grace");
 
            $state_end = $now + $lid_hold;
 
            sound_alarm("WARNING", $warn_volume, $acpi_volume, $warn_cmd);
 
        }
 
    } elsif ($state==2) { # active
 
        if ($tilted) {
 
            set_state(5, "motion detected, holding for $min_hold seconds");
 
            $state_end = $now + $min_hold;
 
            sound_alarm("WARNING", $warn_volume, $acpi_volume, $warn_cmd);
 
        } else {
 
            if ($use_lid && ($now > $last_tilt + $grace_relax )) {
 
                set_state(1, "$grace_relax seconds since last motion, so allowing grace again");
 
            }
 
        }
 
    } elsif ($state==3) { # hold+grace
 
        if ($now < $last_lid_open + $lid_grace) {
 
            set_state(5, "lid opened, holding for $lid_grace seconds grace period");
 
            $state_end = $now + $lid_grace;
 
        } elsif ($now >= $state_end) {
 
            my $delta = $lid_grace_window - $lid_hold;
 
            $state_end = $now + $delta;
 
            set_state(4, "hold ended, arming but allowing grace for $delta more seconds");
 
        }
 
    } elsif ($state==4) { # armed+grace
 
        if ($now < $last_lid_open + $lid_grace) {
 
            set_state(5, "lid opened, holding for $lid_grace seconds grace period");
 
            $state_end = $now + $lid_grace;
 
        } elsif ($now >= $state_end) {
 
            set_state(6, "grace window ended");
 
        }
 
    } elsif ($state==5) { # hold
 
        if ($now >= $state_end) {
 
            set_state(6, "hold ended, arming");
 
        }
 
    } elsif ($state==6) { # armed
 
        if ($now > $last_tilt + $arm_persist) {
 
            set_state(2, "no motion for $arm_persist seconds, unarming");
 
        }
 
    }
 
 
 
    # LEDs:
 
    if ($state>0) {
 
        led_activate(); light_activate();
 
    }
 
 
 
    # Alarm:
 
    if (($state==4 || $state==6 || $state==7) && $tilted) {
 
        sound_alarm("ALARM", $alarm_volume, $acpi_volume, $alarm_cmd);
 
    }
 
}
 
 
 
};
 
 
 
print "Shutting down.\n" if $verbose>1;
 
led_restore() and light_restore() if ($state>0);
 
die "$@" if $@;
 
</pre>
 
 
 
The [[User:Thinker|author]] of the script makes it available the terms of both the [http://www.gnu.org/copyleft/gpl.html GPL] version 2 or later, or at your option, the [http://www.gnu.org/copyleft/fdl.html GFDL].
 
  
 
==A basic script==
 
==A basic script==

Revision as of 23:37, 26 July 2006

General

Recent ThinkPad models include a built-in two-axis accelerometer, as part of the HDAPS feature. This accelerometer can be put to another use: as a laptop theft deterrent. The following script detects when the laptop is moved and emits a loud audio alarm. Against a casual laptop-snatcher in a populated environment (e.g., typical office space) this can be an effective deterrent.

Note that the alarm cannot work when the laptop is suspended or powered off. You can buy external (hardware) motion detector alarms for those cases.

ATTENTION!
The audio alarm is played at a very high volume. Never enable the alarm while wearing headphones connected to the laptop's speaker output or when the laptop is connected to a high-power amplifier.

A comprehensive script

This Perl script periodically samples the tilt data reported by the accelerometer, computes the variance over recent samples, and triggers the alarm when the variance exceeds a given threshold.

On a ThinkPad with Active Protection System running a modern Linux installation with the hdaps kernel module loaded, the script should work as is. Just run # tp-theft --arm and see (or rather, hear) what happens when you tilt your laptop.

The volume and alarm sound can be adjusted at the top of the script. On a ThinkPad T43, the synthetic siren at $alarm_volume=100 (up from the default 70) is quite ear-splitting, and combined with $acpi_volume=15 it is dangerously loud.

The script is designed to run continuously in the background, so by default the alarm will be activated only when the KDE screen saver is locked. If you you open the laptop lid (or press the lid button) shortly before or after the beginning of movement, the alarm will be suspended (except for a brief warning) and you will get a few seconds of grace to unlock the screen saver (preferably, using the integrated fingerprint reader!). You can disable this functionality by passing the --arm parameter, by setting $use_kde=0 and $use_lid=0, or by using the simpler script below.

Prerequisites

  • ThinkPad with Active Protection System
  • hdaps kernel module loaded (included in kernel 2.6.14 and later)
  • Optional: ibm_acpi module loaded with the experimental=1 parameter (included in kernel 2.6.14 and later; needed only for full volume control)

The following are included in all modern Linux distributions:

  • ALSA sound system, alsactl, aplay, amixer )
  • sox (SOund eXchange) sound utility

The script

tp-theft (download)

A basic script

This is a simpler version of the above script, which omits the fancier functionality such as KDE screensaver detection, lid detection and state machine.

Prerequisites

  • ThinkPad with Active Protection System
  • hdaps kernel module loaded (included in kernel 2.6.14 and later)
  • aumix mixer control utility (included in all modern Linux distributions)
  • sox (SOund eXchange) sound utility (included in all modern Linux distributions)

The script

#!/usr/bin/perl
# tp-theft v0.1 (http://thinkwiki.org/wiki/Script_for_theft_alarm_using_HDAPS)
# This script uses the HDAPS accelerometer found on recent ThinkPad models
# to emit an audio alarm when the laptop is tilted. In sufficiently
# populated environments, it can be used as a laptop theft deterrent.
#
# This file is placed in the public domain and may be freely distributed.

use strict;
use warnings;

##############################
# Siren volume and content

# Audio volume (0..100)
my $volume = 70;

# Synthesize a siren for 1.0 seconds:
my $play_cmd = "sox -t nul /dev/null -t ossdsp /dev/dsp synth 1.0 sine 2000-4000 sine 4000-2000";

# Play a file:
# my $play_cmd = "play keep_your_hands_off_me.wav";

##############################
# Other tweakables

my $thresh = 0.20;   # tilt threshold (increase value to decrease sensitivity)
my $interval = 0.1;  # sampling interval in seconds
my $depth = 10;      # number of recent samples to analyze
my $pos_file='/sys/devices/platform/hdaps/position';
my $verbose = 1;

##############################
# Code

sub get_pos {
    open(POS,"<",$pos_file) or die "Can't open HDAPS file $pos_file: $!\n";
    $_=<POS>;
    m/^\((-?\d+),(-?\d+)\)$/ or die "Can't parse $pos_file content\n";
    return ($1,$2);
}

sub stddev {
    my $sum=0;
    my $sumsq=0;
    my $n=$#_+1;
    for my $v (@_) {
	$sum += $v;
	$sumsq += $v*$v;
    }
    return sqrt($n*$sumsq - $sum*$sum)/($n*($n-1));
}

my (@XHIST, @YHIST);
my ($x,$y) = get_pos;
for (1..$depth) {
    push(@XHIST,$x);
    push(@YHIST,$y);
}
my $alarm_file; # flags ongoing alarm (and stores saved mixer settings)

while (1) {
    my ($x,$y) = get_pos;
    shift(@XHIST); push(@XHIST,$x);
    shift(@YHIST); push(@YHIST,$y);
    my $xdev = stddev(@XHIST);
    my $ydev = stddev(@YHIST);

    # Print variance and history
    print "X: v=$xdev (".join(',',@XHIST).")  Y: v=$ydev (".join(",",@YHIST).")\n" if $verbose>1;

    my $tilted = $xdev>$thresh || $ydev>$thresh;

    if ($tilted && !(defined($alarm_file) && -f $alarm_file)) {
	print "ALARM\n" if $verbose>0;
	$alarm_file = `mktemp /tmp/hdaps-tilt.XXXXXXXX` or die "mktemp: $?";
	chomp($alarm_file);
	system('/bin/bash', '-c', <<"EOF")==0 or die "Failed: $?";
( trap \"aumix -L -f $alarm_file > /dev/null; rm -f $alarm_file" EXIT HUP QUIT TERM
  aumix -S -f $alarm_file &&
  aumix -v $volume -w 100 &&
  $play_cmd) &
EOF
    }

    select(undef, undef, undef, $interval); # sleep
}

The author of the script disclaims all warranty for this script, and releases it to the public domain.

Ideas for improvement

Features awaiting contribution:

  • Disable the alarm when headphones are plugged in -- it may cause hearing damage (if the user ignores the initial warning), and won't be effective anyway. Can we detect whether the something is plugged into the headphones/line-out socket?
  • Start out quietly, and increase siren duration and volume if movement persists. Reset after a period of no movement.
  • Gnome and xscreensaver support (similarly to lightwatch.pl?)
  • Report theft via network (if you get a chance to).
  • Monitor AC power and take it into account for alarm activation -- thieves seldom carry a UPS.
  • Monitor proximity to a bluetooth device carried by the owner, and take it into account for alarm activation. I'll implement this if you get me a BMDC-3 Bluetooth card.
  • Don't arm the alarm if movement of similar magnitude was happening also before the screenwaver was auto-locked (the owner might be in a moving vehicle, etc.).
  • Theft attempts may be accompanied by rough handling, especially when the siren kicks in. So when starting an alarm also park the disk heads. Release the parking when a key is pressed (according /sys/bus/platform/drivers/hdaps/hdaps/keyboard_activity) so that the login prompt can start up. This requires kernel support for disk head parking and queue freezing, currently developed for the (original) HDAPS functionality.
  • When the alarm is triggered, also show a visual warning on the display. Override screensaver/powersaving if necessary.
  • Disarm the alarm (or hold it off for a few seconds, as already implemented for lid open) based on voice/sound recognition using the built-in microphone.
  • Implement this functionality in the embedded controller, so that the alarm will work even when the laptop is suspended. It may be possible to do so without IBM/Lenovo's involvement, using the embedded controller disassembly.
  • Disable the volume buttons when the script is running so that a thief can't just turn the volume down. (Not an issue when ibm_acpi volume control is available - see Prerequisites.)


Another Script (plugin-based)

there's another script with the same intention available at http://www.informatik.hu-berlin.de/~pilop/HOWTO_Gentoo_T43/#TheftAlarm

it uses a plugin-architecture for different checks (HDAPS, ethernet, power, lid, ...)

Yet another script (python/gtk based)

You can find yet another version of this script at

http://r3blog.nl/index.php/thinkpad-theft

It has almost the same features as the comprehensive script above, with a few improvements. It uses dbus to query the screensaver status and gconf for storing configuration value. To improve the delay before the alarm sounds, it has a built-in wav player, and it opens the file-descriptor of the wav at startup time (thereby removing the need to spawn an application to play the alarm; imagine someone stealing your laptop while you're doing heavy disk io). Furthermore, it has a trayicon allowing you to manipulate most settings stored in gconf aswell as showing you the current status of the alarm. The 0.2 release features activation on missing presence of a bluetooth or usb device.