Difference between revisions of "Code/tp-theft"

From ThinkWiki
Jump to: navigation, search
m (Workaround: sox doesn't like duration <0.1sec)
(v0.5.1 - silence BlueTooth stdout, comments)
Line 1: Line 1:
 
#!/usr/bin/perl
 
#!/usr/bin/perl
 
#
 
#
# tp-theft v0.5.0
+
# tp-theft v0.5.1
 
# (http://thinkwiki.org/wiki/Script_for_theft_alarm_using_HDAPS)
 
# (http://thinkwiki.org/wiki/Script_for_theft_alarm_using_HDAPS)
  
Line 22: Line 22:
 
# There is also an option to track a BlueTooth device (e.g., a mobile phone).
 
# There is also an option to track a BlueTooth device (e.g., a mobile phone).
 
# In this case, the alarm is activated (and optionally, the KDE desktop is  
 
# In this case, the alarm is activated (and optionally, the KDE desktop is  
# locked) whenever the device is turned off or too distant for a given time,
+
# locked) whenever the device is turned off or too distant for a given period,
# and deactivated when the BlueTooth is nearby. You need to provide the
+
# and deactivated when the BlueTooth device is nearby. You need to provide the
 
# device's BD address. If both KDE screen saver and BlueTooth checking are
 
# device's BD address. If both KDE screen saver and BlueTooth checking are
 
# enabled, then the alarm will be activated when *either* the screensaver
 
# enabled, then the alarm will be activated when *either* the screensaver
Line 424: Line 424:
 
                 $bt_pipe->writer();
 
                 $bt_pipe->writer();
 
                 $bt_pipe->autoflush(1);
 
                 $bt_pipe->autoflush(1);
                 open(STDOUT,">/dev/null");
+
                 open(STDOUT, ">/dev/null");
 +
                open(STDERR, ">/dev/null");
 
                 while(1) {
 
                 while(1) {
 
                     # Is the BlueTooth token reachable?
 
                     # Is the BlueTooth token reachable?

Revision as of 19:57, 23 February 2007

  1. !/usr/bin/perl
  2. tp-theft v0.5.1
  3. (http://thinkwiki.org/wiki/Script_for_theft_alarm_using_HDAPS)
  1. Provided under the GNU General Public License version 2 or later or
  2. the GNU Free Documentation License version 1.2 or later, at your option.
  3. See http://www.gnu.org/copyleft/gpl.html for the Warranty Disclaimer.
  1. This script uses the HDAPS accelerometer found on recent ThinkPad models
  2. to emit an audio alarm when the laptop is tilted. In sufficiently
  3. populated environments, it can be used as a laptop theft deterrent.
  4. Uses a state machine and some heuristics to reduce false alarms.
  5. By default the alarm will be activated only when the KDE screen saver is
  6. locked. If you you open the laptop lid (or press the lid button) shortly
  7. before or after the beginning of movement, the alarm will be suspended
  8. (except for a brief warning) and you will get a few seconds of grace to
  9. unlock the screen saver. You can disable this functionality by passing
  10. the "--arm" parameter, or by setting $use_kde=0 and $use_lid=0.
  11. There is also an option to track a BlueTooth device (e.g., a mobile phone).
  12. In this case, the alarm is activated (and optionally, the KDE desktop is
  13. locked) whenever the device is turned off or too distant for a given period,
  14. and deactivated when the BlueTooth device is nearby. You need to provide the
  15. device's BD address. If both KDE screen saver and BlueTooth checking are
  16. enabled, then the alarm will be activated when *either* the screensaver
  17. is enabled or the BlueTooth device is amiss.
  18. To control the sound and blinkenlights, and adjust the alarm activation
  19. parameters, see the variables below.

use strict; use warnings; use FileHandle; use IO::Pipe; use Time::HiRes qw(sleep time); use POSIX qw(:errno_h :signal_h);

  1. Siren volume and content
  1. Alarm audio volume (0..100)

my $alarm_volume = 70;

  1. 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";

  1. my $alarm_cmd = "aplay keep_your_hands_off_me.wav";
  1. Warning audio volume (0..100)

my $warn_volume = 45;

  1. 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.10 sine 2000-4000 sine 4000-2000 | aplay -q -fS16_LE -c2 -r48000";

  1. my $warn_cmd = "aplay warning.wav";
  1. Set ibm_acpi volume (0..15), if ibm_acpi is loaded with "experimental=1".
  2. Combining $acpi_volume=15 and $alarm_volume=100 makes the alarm
  3. dangerously loud.

my $acpi_volume = 10;

  1. Blink system LEDs when alarm activated?

my $use_led = 'safe'; # 0=off, 'safe'=only LEDs whose state you can recover, 'all'=pretty blinkenlights!

  1. Blink ThinkLight when alarm activated?

my $use_light = 0; # 0=off, 1=on

  1. Activation control
  1. Tilt threshold (increase value to decrease sensitivity):

my $thresh = 0.20;

  1. Minimum movement duration between warning and alarm:

my $min_hold = 1.3;

  1. When armed, any movement triggers alarm. How long should it remain armed?

my $arm_persist = 6;

  1. After this many seconds of no movement, will allow a grace period again:

my $grace_relax = 15;

  1. Activate according to KDE screen saver? Otherwise, always active:

my $use_kde = 1;

  1. When screen saver locked, wait this long before activation:

my $kde_lock_delay = 8;

  1. Provide grace period if laptop lid is opened?

my $use_lid = 1;

  1. Opening a lid will grant this many seconds of grace (once):

my $lid_grace = 7;

  1. Lid must to be opened within this time to hold/pause alarm:

my $lid_grace_window = 8;

  1. Alarm will hold off this long when grace is available:

my $lid_hold = 3;

  1. Control arming according by presence of a BlueTooth token

my $use_bluetooth = 0;

  1. Lock KDE screen saver when BlueTootk is not present?

my $bluetooth_lock_kde = 1;

  1. BD address of BlueTooth token (use "hcitool scan" to find this)

my $bluetooth_token_addr = '00:00:00:00:00:00';

  1. Consider token amiss when its received signal leve is below this (see "hcitool rssi")

my $bluetooth_min_rssi = -10;

  1. Activate if BlueTooth token not seen this long:

my $bluetooth_activate_period = 12;

  1. Disactivate if BlueTooth token seen this recently:

my $bluetooth_deactivate_period = 5;

  1. If BlueTooth detection activated KDE lock, don't do it again for this long

my $bluetooth_lock_kde_interval = 30;

  1. If BlueTooth wasn't polled for this long, disregard recent history

my $bluetooth_reset_period = 10;

  1. 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 $bluetooth_sleep = 1; # Sleep interval in BlueTooth check loop

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 $bluetooth_file = '/proc/acpi/ibm/bluetooth'; # load ibm_acpi with experimental=1

my $alsactl = '/usr/sbin/alsactl'; my $amixer = 'amixer'; my $kdesktop_lock = '/usr/bin/kdesktop_lock'; my $hcitool = '/usr/bin/hcitool'; my $l2ping = '/usr/bin/l2ping';

  1. 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 burp { # write whole file

   my ($filename) = shift;
   my $fh = new FileHandle($filename,">") or die "Can't open $filename for writing: $!";
   print $fh @_ or die "Can't write to $filename: $!";
   close $fh or die "Can't close $filename after writing: $!";

}

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

}

sub frac {

   my ($x) = @_;
   return $x-int($x);

}

sub max {

   return $_[0] > $_[1] ? $_[0] : $_[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 }


  1. 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 my @last_kls_opwhy = ();

sub check_kde_lock {

   # De/activate according to KDE screen saver:
   my $now=time();
   return @last_kls_opwhy 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
       @last_kls_opwhy = (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) {
           @last_kls_opwhy = (1, 'KDE screen saver is auto-locked');
       }
   } elsif ($kls eq 'force') { # screen saver with forced lock
       if ($now >= $last_kls_start + $kde_lock_delay) {
           @last_kls_opwhy = (1, 'KDE screen saver is forced-locked');
       }
   }
   return @last_kls_opwhy;

}

  1. Lock KDE desktop

sub force_kde_lock {

   return if ($last_kls eq 'auto' or $last_kls eq 'force');
   say(1, "Locking KDE desktop");
   # system('dcop kdesktop KScreensaverIface lock') or die "Cannot lock KDE dekstop: $!"
   my %oldENV = %ENV;
   my $kde_pids = `/sbin/pidof -x /usr/bin/startkde`;
   for my $kde_pid (split(/\s+/,$kde_pids)) {
       my $kde_env = slurp("/proc/$kde_pid/environ") or next;
       %ENV = ();
       for (split(/\x00/,$kde_env)) {
           m/^(DISPLAY|HOME|XAUTHORITY|USER|DBUS_SESSION_BUS_ADDRESS)=(.+)$/
               and $ENV{$1} = $2;
       }
       next unless defined($ENV{DISPLAY}) and $ENV{DISPLAY} =~ m/^:/;
       system('dcop kdesktop KScreensaverIface lock') and die "Cannot lock KDE dekstop: $!"
   }
   my %ENV = %oldENV;

}

  1. 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;

}


  1. LED blinking
  1. Flash LEDs

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

}

  1. Restore LEDs to normal state

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
   if (my $baydata = slurp($bay_file)) {
       my $is_bay = ($baydata =~ m/^status:\s*occupied$/m)?'on':'off';
       print $ledf "4 $is_bay\n"; # reset to correct status
   } else {
       print $ledf "4 on\n"; # force bay on, we don't know correct status
   }

}


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

}

  1. BlueTooth token detection


{

   my $temporary_bt = 0;
   my $bt_pid;
   my $bt_pipe;
   my $bluetooth_last_seen = 0; # last time we saw the BlueTooth token
   my $bluetooth_last_read = 0;   # last time we polled the BlueTooth child process
   my $bluetooth_ignore_missing_until = time() + $bluetooth_reset_period; 
   if ($use_bluetooth) {
       # If BlueTooth is disabled, temporarily enable it
       slurp($bluetooth_file) or die "Can't control bluetooth via $bluetooth_file: $!";
       my $bt_status = slurp($bluetooth_file);
       $temporary_bt = $bt_status =~ m/status:[\t]*disabled/;
       if ($temporary_bt) {
           say(1, 'BlueTooth was disabled, enabling');
           burp($bluetooth_file, "enable\n");
           sleep(1);
       }
   }
   sub bluetooth_reset {  # disregard recent (negative) history
       $bluetooth_ignore_missing_until = max($bluetooth_ignore_missing_until, time() + $bluetooth_reset_period);
   }
   sub check_bluetooth {
       if (!defined($bt_pid)) {
           # Create new child process, which will loop checking
           # for the token. Each time it sees the token, it writes
           # the current time to a pipe that's read by the main process.
           $bt_pipe = new IO::Pipe;
           $bt_pid = fork();
           die "Cannot fork BlueTooth check: $!" unless defined($bt_pid);
           if (!$bt_pid) {
               # Child
               $bt_pipe->writer();
               $bt_pipe->autoflush(1);
               open(STDOUT, ">/dev/null");
               open(STDERR, ">/dev/null");
               while(1) {
                   # Is the BlueTooth token reachable?
                   my $res = system($l2ping,'-c','1','-t',1,$bluetooth_token_addr);
                   die "Failed invoking l2ping: $!\n" if $res&0xFF;
                   if ($res==0) {
                      # Is the BlueTooth sufficiently close, as judged by signal strength?
                      $res = `$hcitool rssi $bluetooth_token_addr`;
                      if ($?==0 && $res =~ m/^RSSI return value: (-?[0-9]+)$/) {
                          my $rssi = $1;
                          printf $bt_pipe "%d\n", time() if ($rssi > $bluetooth_min_rssi);
                      }
                   }
                   sleep($bluetooth_sleep);
               }
            }
            $bt_pipe->reader();
            $bt_pipe->blocking(0);
       }
       while (1) { 
           my $res = <$bt_pipe>;
           last if ($!==POSIX::EAGAIN); # busy
           die "Error reading from BlueTooth check child process\n" unless defined($res);
           $bluetooth_last_seen = $res;
       }
       my $now = time();
       bluetooth_reset() if ($now > $bluetooth_last_read + $bluetooth_reset_period);
       $bluetooth_last_read = time();
       if ($now > max($bluetooth_last_seen + $bluetooth_activate_period, $bluetooth_ignore_missing_until)) {
           if ($bluetooth_lock_kde) {
               force_kde_lock();
               $bluetooth_ignore_missing_until = max($bluetooth_ignore_missing_until, time() + $bluetooth_lock_kde_interval);
           }
           return (1, "BlueTooth token not seen for $bluetooth_activate_period seconds, activating.");
       } elsif ($now < $bluetooth_last_seen + $bluetooth_deactivate_period) {
           return (0, "BlueTooth token seen during last $bluetooth_deactivate_period seconds, deactivating.");
       }
       return ();
   }
   sub bluetooth_restore {
       if ($temporary_bt) {
           say(1, 'Disabling BlueTooth');
           burp($bluetooth_file, "disable\n");
       }
       if ($bt_pid) {
           kill(SIGTERM, $bt_pid);
           waitpid($bt_pid, 0);
       }
   }

}

  1. Main code

my $state; # See state machine in main loop 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);
   return undef if $!{EBUSY};
   die "Can't open HDAPS file $pos_file: $!\n" if (!defined($pos) || $!);
   $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 || $use_bluetooth)  ? 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_lid() if $use_lid;
   # Check screensaver and BlueTooth. Activate if either says so,
   # otherwise deactivate if either says so.
   my ($op1, $why1); ($op1, $why1) = check_kde_lock() if $use_kde;
   my ($op2, $why2); ($op2, $why2) = check_bluetooth() if $use_bluetooth;
   my ($op, $why) = ( !defined($op1) || ( defined($op2) && $op2>$op1 ) ) ? ($op2,$why2) : ($op1,$why1);
   if (defined($op)) {
       if ($op==1 && $state==0) { set_state(0.5, $why); }
       if ($op==0 && $state>0) { set_state(0, $why); bluetooth_reset(); }
   }
   next unless $state>0;
   # Collect and analyze sensor data:
   my $now = time();
   my $tilted;
   my ($x,$y) = get_pos() or next; # Hopefully the error is transient
   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  (collecting motion data will soon activate)
       if ($#XHIST >= $depth && $#YHIST >= $depth) {
           set_state($arm_forced?7:$use_lid?1:2, "finished data collection");
       }
   } elsif ($state==1) { # ACTIVE+GRACE  (quiet for a long time, awaiting movement)
       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  (short for a shorter time, awaiting movement)
       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  (recent movemvent, but still holding off alarm; after long quiet)
       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  (armed, but recently quiet so allow grace if lid opened)
       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  (recent movement, but still holding off alarm; there was recent action)
       if ($now >= $state_end) {
           set_state(6, "hold ended, arming");
       }
   } elsif ($state==6) { # ARMED  (sound alarm on any movement)
       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); bluetooth_restore(); die "$@" if $@;