Script for theft alarm using HDAPS
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 will buy an external motion detector alarm for those cases.
Prerequisites
- hdaps module loaded (comes with kernel 2.6.14 and later)
- sox (SOund eXchange) sound utility
- aumix command line mixer
The latter two should be included with your distribution, but check if they are installed.
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 an HDAPS-equipped laptop running a modern Linux installation with the hdaps kernel module loaded, the script should work as is. Just run ./th-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 a volume of 100 (up from the default 70) is quite ear-splitting.
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.
#!/usr/bin/perl # # tp-theft v0.3.1 (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 . 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 ossdsp /dev/dsp synth 1.0 sine 2000-4000 sine 4000-2000"; # my $alarm_cmd = "play 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 ossdsp /dev/dsp synth 0.05 sine 2000-4000 sine 4000-2000"; # my $warn_cmd = "play dump.wav"; # Blink system LEDs when alarm activated? my $use_led = 'safe'; # 0=off, 'safe'=only LEDs whose state you can recover, 'all'=everything ############################## # 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 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 $bay_file = '/proc/acpi/ibm/bay'; ############################## # 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 { my ($name, $volume, $cmd) = @_; return if (defined($alarm_file) && -f $alarm_file); say(1,$name); $alarm_file = `mktemp /tmp/tp-theft-sem.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 && $cmd ) & 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 = '/usr/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'; } } ############################## # LED blinking sub frac { my ($x) = @_; return $x-int($x); } sub led_active { 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 print $ledf '4 '.((frac($base)>0.7)?'on':'off')."\n"; # bay print $ledf '7 '.((frac($base+0.725)>0.7)?'on':'off')."\n"; # standby 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 } $ledf->close(); } sub led_default { 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 } ############################## # 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_default() if $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(0.5, "init") if !defined($state); 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, $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, $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_active(); } # Alarm: if (($state==4 || $state==6 || $state==7) && $tilted) { sound_alarm("ALARM", $alarm_volume, $alarm_cmd); } } }; print "Shutting down.\n" if $verbose; led_default() if ($state>0); die "$@" if $@;
The author of the script makes it available the terms of both the GPL version 2 or later, or at your option, the GFDL.
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.
#!/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.
To do
Features awaiting contribution:
- 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.).