From edff9b2c27acfc0f13e7c3eadd62582ca8d9f501 Mon Sep 17 00:00:00 2001 From: PatrickR <> Date: Sun, 4 Oct 2020 15:23:07 +0000 Subject: [PATCH] lepresenced: V0.93 lepresenced-0.93-1.deb: added git-svn-id: https://svn.fhem.de/fhem/trunk@22908 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- .../PRESENCE/deb/lepresenced-0.93-1.deb | Bin 0 -> 9192 bytes fhem/contrib/PRESENCE/lepresenced | 530 ++++++++++++------ 2 files changed, 355 insertions(+), 175 deletions(-) create mode 100644 fhem/contrib/PRESENCE/deb/lepresenced-0.93-1.deb diff --git a/fhem/contrib/PRESENCE/deb/lepresenced-0.93-1.deb b/fhem/contrib/PRESENCE/deb/lepresenced-0.93-1.deb new file mode 100644 index 0000000000000000000000000000000000000000..ce81eae263d66c6524c19278e2caa229b05275c1 GIT binary patch literal 9192 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 1; -use constant INET_RECV_BUFFER => 1024; -use constant MAINLOOP_SLEEP_US => 250 * 1000; +use Data::Dumper; -use constant CLEANUP_INTERVAL => 15 * 60; -use constant CLEANUP_MAX_AGE => 30 * 60; -use constant STATS_INTERVAL => 5 * 60; -use constant DUMP_INTERVAL => 10; +Readonly my $RETRY_SLEEP => 1; +Readonly my $INET_RECV_BUFFER => 1024; +Readonly my $MAINLOOP_SLEEP_US => 250 * 1000; +Readonly my $BATTERY_TASK_SETTLE_PRE_SLEEP => 1; +Readonly my $BATTERY_TASK_SETTLE_POST_SLEEP => 2; -use constant DEFAULT_RSSI_THRESHOLD => 10; -use constant RSSI_WINDOW => 10; +Readonly my $KILL_SIGNAL => 2; #SIGINT -use constant ME => 'lepresenced'; -use constant VERSION => '0.92'; +Readonly my $CLEANUP_INTERVAL => 15 * 60; +Readonly my $CLEANUP_MAX_AGE => 30 * 60; +Readonly my $STATS_INTERVAL_INFO => 5 * 60; +Readonly my $STATS_INTERVAL_DEBUG => 1 * 60; +Readonly my $DUMP_INTERVAL => 10; +Readonly my $DEFAULT_BATTERY_INTERVAL_H => 6; +Readonly my $SHORT_BATTERY_INTERVAL_S => 2 * 60; -use constant PIDFILE => '/var/run/' . ME . '.pid'; +Readonly my $DEFAULT_RSSI_THRESHOLD => 10; +Readonly my $RSSI_WINDOW => 10; -use constant { - HCIDUMP_STATE_NONE => 0, - HCIDUMP_STATE_LE_META_EVENT => 1, - HCIDUMP_STATE_LE_ADVERTISING_REPORT => 2, - HCIDUMP_STATE_ADV_INT => 3, - HCIDUMP_STATE_SCAN_RSP => 4, -}; +Readonly my $ME => 'lepresenced'; +Readonly my $VERSION => '0.93'; + +Readonly my $PIDFILE => "/var/run/$ME.pid"; + +Readonly my $BATTERY_LEVEL_CHARACTERISTIC_UUID => '00002a19-0000-1000-8000-00805f9b34fb'; +Readonly my $BATTERY_MAX_AGE_FACTOR => 4; + +Readonly my $HCIDUMP_STATE_NONE => 0; +Readonly my $HCIDUMP_STATE_LE_META_EVENT => 1; +Readonly my $HCIDUMP_STATE_LE_ADVERTISING_REPORT => 2; +Readonly my $HCIDUMP_STATE_ADV_INT => 3; +Readonly my $HCIDUMP_STATE_SCAN_RSP => 4; + +Readonly my $THREAD_COMMAND_RUN => 0; +Readonly my $THREAD_COMMAND_STOP => 1; +Readonly my $THREAD_COMMAND_RESTART => 2; my %devices :shared; my @clients = (); my ($log_level, $log_target); my $debug; my ($beacons_hcitool, $beacons_hcidump) : shared = (0, 0); -my $restart_hcitool :shared; + +my %thread_commands :shared = ( + 'bluetooth_scan_thread' => $THREAD_COMMAND_RUN, + 'bluetooth_dump_thread' => $THREAD_COMMAND_RUN, +); +my ($next_dump_time, $next_stats_time, $next_cleanup_time, $next_battery_time); +$next_battery_time = time() + $SHORT_BATTERY_INTERVAL_S; sub syslogw { - return if (scalar(@_) < 2); + my ($priority, @args) = @_; + return if (scalar(@args) < 1); my $logmessage; - my $priority = shift(); - if (scalar(@_)==1) { - my ($message) = @_; + if (scalar(@args)==1) { + my ($message) = @args; $logmessage = sprintf("[tid:%i] %s: $message", threads->self()->tid(), (caller(1))[3] // 'main'); } else { - my ($format, @args) = @_; + my ($format, @args) = @args; $logmessage = sprintf("[tid:%i] %s: $format", threads->self()->tid(), (caller(1))[3] // 'main', @args); } if ($log_level >= $priority) { @@ -96,21 +118,22 @@ sub syslogw { } } printf("%s\n", $logmessage) if ($debug); + return(); } sub error_exit { - my $exit_code = shift(); - syslogw(LOG_ERR, @_); + my ($exit_code, @args) = @_; + syslogw(LOG_ERR, @args); foreach my $thread (threads->list()) { $thread->exit(0); } exit ($exit_code); } -sub usage_exit() { +sub usage_exit { print("usage:\n"); - printf("\t%s --bluetoothdevice --listenaddress --listenport --loglevel --logtarget --daemon\n", ME); - printf("\t%s -b -a -p -l -t -d\n", ME); + printf("\t%s --bluetoothdevice --listenaddress --listenport --loglevel --logtarget --daemon\n", $ME); + printf("\t%s -b -a -p -l -t -d\n", $ME); print("valid log levels:\n"); print("\tLOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG. Default: LOG_INFO\n"); print("valid log targets:\n"); @@ -118,15 +141,41 @@ sub usage_exit() { print("optional arguments:\n"); print("\t--debug - print extensive debug output to stdout (mutually exclusive with --daemon).\n"); print("\t--legacymode - legacy mode without rssi detection. Use if you do not have hcidump installed.\n"); - printf("\t--rssithreshold - rssi deviation to trigger an update. Minimum value: 5, default: %s\n", DEFAULT_RSSI_THRESHOLD); + printf("\t--rssithreshold - rssi deviation to trigger an update. Minimum value: 5, default: %s.\n", $DEFAULT_RSSI_THRESHOLD); + printf("\t--batteryinterval - interval for battery checks in hours, default: %s.\n", $DEFAULT_BATTERY_INTERVAL_H); print("examples:\n"); - printf("\t%s --bluetoothdevice hci0 --listenaddress 127.0.0.1 --listenport 5333 --daemon\n", ME); - printf("\t%s --loglevel LOG_DEBUG --daemon\n", ME); + printf("\t%s --bluetoothdevice hci0 --listenaddress 127.0.0.1 --listenport 5333 --daemon\n", $ME); + printf("\t%s --loglevel LOG_DEBUG --daemon\n", $ME); closelog(); exit(1); } -sub parse_options() { +sub parse_log_level { + my ($log_level_str) = @_; + $log_level_str = uc($log_level_str); + + return ( $log_level_str eq 'LOG_EMERG' ? LOG_EMERG + : $log_level_str eq 'LOG_ALERT' ? LOG_ALERT + : $log_level_str eq 'LOG_CRIT' ? LOG_CRIT + : $log_level_str eq 'LOG_ERR' ? LOG_ERR + : $log_level_str eq 'LOG_WARNING' ? LOG_WARNING + : $log_level_str eq 'LOG_NOTICE' ? LOG_NOTICE + : $log_level_str eq 'LOG_INFO' ? LOG_INFO + : $log_level_str eq 'LOG_DEBUG' ? LOG_DEBUG + : usage_exit() + ); +} + +sub humanize_thread_command { + my ($command) = @_; + return ( $command eq $THREAD_COMMAND_RUN ? 'THREAD_COMMAND_RUN' + : $command eq $THREAD_COMMAND_STOP ? 'THREAD_COMMAND_STOP' + : $command eq $THREAD_COMMAND_RESTART ? 'THREAD_COMMAND_RESTART' + : '?' + ); +} + +sub parse_options { my $device = "hci0"; my $daemonize = 0; my $listen_address = "0.0.0.0"; @@ -135,7 +184,8 @@ sub parse_options() { my $log_level = "LOG_INFO"; my $debug = 0; my $legacy_mode = 0; - my $rssi_threshold = DEFAULT_RSSI_THRESHOLD; + my $rssi_threshold = $DEFAULT_RSSI_THRESHOLD; + my $battery_interval_h = $DEFAULT_BATTERY_INTERVAL_H; GetOptions( 'bluetoothdevice|device|b=s' => \$device, @@ -147,32 +197,34 @@ sub parse_options() { 'debug!' => \$debug, 'legacymode|legacy!' => \$legacy_mode, 'rssithreshold=i' => \$rssi_threshold, + 'batteryinterval=i' => \$battery_interval_h, ) or usage_exit(); usage_exit() if ($rssi_threshold < 5); + usage_exit() if ($battery_interval_h < 1); $listen_address =~ m/^\d+\.\d+\.\d+\.\d+$/ or usage_exit(); - $log_level =~ m/^LOG_(EMERG|ALERT|CRIT|ERR|WARNING|NOTICE|INFO|DEBUG)$/ or usage_exit(); $log_target =~ m/^(syslog|stdout)$/ or usage_exit(); - $log_level = eval($log_level); + $log_level = parse_log_level($log_level); $daemonize = 0 if ($debug); - return ($device, $daemonize, $listen_address, $listen_port, $log_level, $log_target, $debug, $legacy_mode, $rssi_threshold); + return ($device, $daemonize, $listen_address, $listen_port, $log_level, $log_target, $debug, $legacy_mode, $rssi_threshold, $battery_interval_h); } -sub sanity_check($) { +sub sanity_check { my ($legacy_mode) = @_; error_exit(3, "ERROR: lepresenced is already running. Exiting.") if (!flock DATA, LOCK_EX | LOCK_NB); # log md5 digest of lepresenced - open (my $me, "<$0"); + open (my $me, '<', $0); binmode ($me); - syslogw(LOG_INFO, "md5 digest of '%s' is: %s.", $0, Digest::MD5->new->addfile($me)->hexdigest()); - + syslogw(LOG_INFO, "md5 digest of '%s' is: '%s'.", $0, Digest::MD5->new->addfile($me)->hexdigest()); + close($me); + # check if necessary external binaries exist my $ok = 1; - foreach my $binary ($legacy_mode ? qw/hciconfig hcitool/ : qw/hciconfig hcitool hcidump/) { + foreach my $binary ($legacy_mode ? qw/hciconfig hcitool gatttool/ : qw/hciconfig hcitool gatttool hcidump/) { my $binpath = `which $binary 2>/dev/null`; chomp($binpath); if ($? == 0) { @@ -183,10 +235,11 @@ sub sanity_check($) { } } error_exit(4, "ERROR: Exiting due to missing binaries.") if (!$ok); + return(); } -sub update_device($$$) { - my ($mac, $name, $rssi) = @_; +sub update_device { + my ($mac, $name, $rssi, $address_type) = @_; $mac = lc($mac); { lock(%devices); @@ -201,127 +254,160 @@ sub update_device($$$) { $devices{$mac}{'rssi'} = $rssi; $devices{$mac}{'reported_rssi'} = $rssi if (!defined($devices{$mac}{'reported_rssi'})); $devices{$mac}{'prevtimestamp'} = $devices{$mac}{'timestamp'}; + $devices{$mac}{'address_type'} = lc($address_type); $devices{$mac}{'timestamp'} = time(); } + return(); } -sub bluetooth_scan_thread($$) { +sub set_thread_command { + my ($thread, $command) = @_; + syslogw(LOG_DEBUG, "Setting thread command of thread '%s' to '%s'.", $thread, humanize_thread_command($command)); + $thread_commands{$thread} = $command; + return(); +} + +sub bluetooth_scan_thread { my ($device, $legacy_mode) = @_; my $hcitool; - $restart_hcitool = 0; + for(;;) { - ($beacons_hcitool, $beacons_hcidump) = (0, 0); - my $pid = open($hcitool, "-|", "stdbuf -oL hcitool -i " . $device . " lescan --duplicates 2>&1") || die('Unable to start scanning. Please make sure hcitool and stdbuf are installed!'); - while (<$hcitool>) { - if ($restart_hcitool) { - $restart_hcitool = 0; - last(); - } - chomp($_); - if ($_ eq 'LE Scan ...') { - syslogw(LOG_INFO, "Received '%s'.", $_); - } elsif (my ($fbmac, $fbname) = $_ =~ /^([\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2})\s(.*)$/i) { - $beacons_hcitool++; - if ($legacy_mode) { - #syslogw(LOG_DEBUG, "Received advertisement from bluetooth mac address '%s' with name '%s'.", $fbmac, $fbname); - update_device($fbmac, $fbname, 'unknown'); + #syslogw(LOG_DEBUG, "Thread command: '%s'.", $thread_commands{bluetooth_scan_thread}); + if ($thread_commands{bluetooth_scan_thread} != $THREAD_COMMAND_STOP) { + ($beacons_hcitool, $beacons_hcidump) = (0, 0); + my $pid = open($hcitool, "-|", "stdbuf -oL hcitool -i " . $device . " lescan --duplicates 2>&1") || die('Unable to start scanning. Please make sure hcitool and stdbuf are installed!'); + while (<$hcitool>) { + #syslogw(LOG_DEBUG, "Thread command: '%s'.", $thread_commands{bluetooth_scan_thread}) if ($thread_commands{bluetooth_scan_thread} != $THREAD_COMMAND_RUN); + last() if ($thread_commands{bluetooth_scan_thread} != $THREAD_COMMAND_RUN); + chomp($_); + if ($_ eq 'LE Scan ...') { + syslogw(LOG_INFO, "Received '%s'.", $_); + } elsif (my ($fbmac, $fbname) = $_ =~ /^([\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2})\s(.*)$/i) { + $beacons_hcitool++; + if ($legacy_mode) { + update_device($fbmac, $fbname, 'unknown', undef); + } + } elsif ( + $_ =~ m/^Set scan parameters failed: Input\/output error$/ || + $_ =~ m/^Invalid device: Network is down$/ + ) { + syslogw(LOG_WARNING, "Received '%s', resetting...", $_); + system(sprintf('hciconfig %s reset', $device)); + } else { + syslogw(LOG_WARNING, "Received unknown output: '%s'!", $_); } - } elsif ( - $_ =~ m/^Set scan parameters failed: Input\/output error$/ || - $_ =~ m/^Invalid device: Network is down$/ - ) { - syslogw(LOG_WARNING, "Received '%s', resetting...", $_); - system(sprintf('hciconfig %s reset', $device)); - } else { - syslogw(LOG_WARNING, "Received unknown output: '%s'!", $_); } + kill($KILL_SIGNAL, $pid); + close($hcitool); + syslogw(LOG_WARNING, + $thread_commands{bluetooth_scan_thread} == $THREAD_COMMAND_STOP ? "hcitool was stopped." + : $thread_commands{bluetooth_scan_thread} == $THREAD_COMMAND_RESTART ? "restarting hcitool..." + : "hcitool exited, retrying..." + ); + set_thread_command('bluetooth_scan_thread', $THREAD_COMMAND_RUN) if ($thread_commands{bluetooth_scan_thread} == $THREAD_COMMAND_RESTART); } - syslogw(LOG_WARNING, "hcitool exited, retrying..."); - close($hcitool); - sleep(RETRY_SLEEP); + sleep($RETRY_SLEEP); } + return(); } -sub bluetooth_dump_thread($) { +sub bluetooth_dump_thread { my ($device) = @_; my $hcidump; my %rssitable; for(;;) { - my $pid = open($hcidump, "-|", "hcidump -i " . $device) || die('Unable to start scanning. Please make sure hcidump is installed or use legacy mode (--legacymode)!'); - my $state = HCIDUMP_STATE_NONE; - my $current_mac = ''; - my $current_rssi = ''; - my $current_name = ''; - - while (<$hcidump>) { - chomp($_); - if ($_ =~ m/^< HCI Command: / && $beacons_hcitool > 0) { # Ignore initial settings, i. e. before first beacon - # https://forum.fhem.de/index.php/topic,75559.msg1007719.html#msg1007719 - syslogw(LOG_WARNING, "Received '%s', telling hcidump to restart...", $_); - $state = HCIDUMP_STATE_NONE; - $restart_hcitool = 1; - } elsif ($_ =~ m/^>/) { - if ($current_mac) { - #printf("DEBUG: mac: %s, name: '%s', rssi: %s\n", $current_mac, $current_name, $current_rssi); - - # update rssi queue - unless (exists $rssitable{$current_mac}) { - $rssitable{$current_mac} = []; + #syslogw(LOG_DEBUG, "Thread command: '%s'.", $thread_commands{bluetooth_dump_thread}); + if ($thread_commands{bluetooth_dump_thread} != $THREAD_COMMAND_STOP) { + ($beacons_hcitool, $beacons_hcidump) = (0, 0); + my $pid = open($hcidump, "-|", "hcidump -i " . $device . " 2>&1") || die('Unable to start scanning. Please make sure hcidump is installed or use legacy mode (--legacymode)!'); + my $state = $HCIDUMP_STATE_NONE; + my $current_mac = ''; + my $current_rssi = ''; + my $current_name = ''; + my $current_address_type = ''; + + while (<$hcidump>) { + #syslogw(LOG_DEBUG, "Thread command: '%s'.", $thread_commands{bluetooth_dump_thread}) if ($thread_commands{bluetooth_dump_thread} != $THREAD_COMMAND_RUN); + last() if ($thread_commands{bluetooth_dump_thread} != $THREAD_COMMAND_RUN); + chomp($_); + if ($_ =~ m/^< HCI Command: /) { + if ($beacons_hcitool > 0) { # Ignore initial settings, i. e. before first beacon + # https://forum.fhem.de/index.php/topic,75559.msg1007719.html#msg1007719 + syslogw(LOG_WARNING, "Received '%s', telling hcidump and hcitool to restart...", $_); + set_thread_command('bluetooth_scan_thread', $THREAD_COMMAND_RESTART); + set_thread_command('bluetooth_dump_thread', $THREAD_COMMAND_RESTART); } - if ($current_rssi) { - shift(@{$rssitable{$current_mac}}) if(scalar(@{$rssitable{$current_mac}}) >= RSSI_WINDOW); - push(@{$rssitable{$current_mac}}, $current_rssi); + } elsif ($_ =~ m/^>/) { + if ($current_mac) { + # update rssi queue + unless (exists $rssitable{$current_mac}) { + $rssitable{$current_mac} = []; + } + if ($current_rssi) { + shift(@{$rssitable{$current_mac}}) if(scalar(@{$rssitable{$current_mac}}) >= $RSSI_WINDOW); + push(@{$rssitable{$current_mac}}, $current_rssi); + } + my $mean_rssi = 0; + foreach my $rssi (@{$rssitable{$current_mac}}) { + $mean_rssi += $rssi; + } + $mean_rssi = int($mean_rssi / scalar(@{$rssitable{$current_mac}})); + #printf("DEBUG: mac: %s, rssi count: %i, rssis: %s, mean: %s\n", $current_mac, scalar(@{$rssitable{$current_mac}}), join(',', @{$rssitable{$current_mac}}), $mean_rssi); + update_device($current_mac, $current_name, $mean_rssi, $current_address_type); + } + $current_mac = ''; + $current_rssi = ''; + $current_name = ''; + $current_address_type = ''; + if ($_ =~ m/^> HCI Event: LE Meta Event \(0x3e\) plen \d+$/) { + $state = $HCIDUMP_STATE_LE_META_EVENT; + } else { + $state = $HCIDUMP_STATE_NONE; } - my $mean_rssi = 0; - foreach my $rssi (@{$rssitable{$current_mac}}) { - $mean_rssi += $rssi; + } elsif ( + $state == $HCIDUMP_STATE_LE_META_EVENT && + $_ eq ' LE Advertising Report' + ) { + $state = $HCIDUMP_STATE_LE_ADVERTISING_REPORT; + } elsif ($state == $HCIDUMP_STATE_LE_ADVERTISING_REPORT) { + if ( + $_ eq ' ADV_IND - Connectable undirected advertising (0)' || + $_ eq ' ADV_NONCONN_IND - Non connectable undirected advertising (3)' + ) { + $state = $HCIDUMP_STATE_ADV_INT; + } elsif ($_ eq ' SCAN_RSP - Scan Response (4)') { + $state = $HCIDUMP_STATE_SCAN_RSP; } - $mean_rssi = int($mean_rssi / scalar(@{$rssitable{$current_mac}})); - #printf("DEBUG: mac: %s, rssi count: %i, rssis: %s, mean: %s\n", $current_mac, scalar(@{$rssitable{$current_mac}}), join(',', @{$rssitable{$current_mac}}), $mean_rssi); - - update_device($current_mac, $current_name, $mean_rssi); - } - $current_mac = ''; - $current_rssi = ''; - $current_name = ''; - if ($_ =~ m/^> HCI Event: LE Meta Event \(0x3e\) plen \d+$/) { - $state = HCIDUMP_STATE_LE_META_EVENT; - } else { - $state = HCIDUMP_STATE_NONE; - } - } elsif ( - $state == HCIDUMP_STATE_LE_META_EVENT && - $_ eq ' LE Advertising Report' - ) { - $state = HCIDUMP_STATE_LE_ADVERTISING_REPORT; - } elsif ($state == HCIDUMP_STATE_LE_ADVERTISING_REPORT) { - if ( - $_ eq ' ADV_IND - Connectable undirected advertising (0)' || - $_ eq ' ADV_NONCONN_IND - Non connectable undirected advertising (3)' - ) { - $state = HCIDUMP_STATE_ADV_INT; - } elsif ($_ eq ' SCAN_RSP - Scan Response (4)') { - $state = HCIDUMP_STATE_SCAN_RSP; - } - } elsif ($state == HCIDUMP_STATE_SCAN_RSP || $state == HCIDUMP_STATE_ADV_INT) { - if ($_ =~ m/^ bdaddr ([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}) \((Public|Random)\)$/) { - $beacons_hcidump++; - $current_mac = $1; - } elsif ($_ =~ m/^ Complete local name: '(.*)'$/) { - $current_name = $1; - } elsif ($_ =~ m/^ RSSI: (-\d+)$/) { - $current_rssi = $1; + } elsif ($state == $HCIDUMP_STATE_SCAN_RSP || $state == $HCIDUMP_STATE_ADV_INT) { + if ($_ =~ m/^ bdaddr ([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}) \((Public|Random)\)$/) { + $beacons_hcidump++; + $current_mac = $1; + $current_address_type = $2; + } elsif ($_ =~ m/^ Complete local name: '(.*)'$/) { + $current_name = $1; + } elsif ($_ =~ m/^ RSSI: (-\d+)$/) { + $current_rssi = $1; + } + } elsif ($_ !~ m/^ /) { + syslogw(LOG_DEBUG, 'Received \'%s\'.', $_); } } + kill($KILL_SIGNAL, $pid); + close($hcidump); + syslogw(LOG_WARNING, + $thread_commands{bluetooth_dump_thread} == $THREAD_COMMAND_STOP ? "hcidump was stopped." + : $thread_commands{bluetooth_dump_thread} == $THREAD_COMMAND_RESTART ? "restarting hcidump..." + : "hcidump exited, retrying..." + ); + set_thread_command('bluetooth_dump_thread', $THREAD_COMMAND_RUN) if ($thread_commands{bluetooth_dump_thread} == $THREAD_COMMAND_RESTART); } - syslogw(LOG_WARNING, "hcidump exited, retrying..."); - close($hcidump); - sleep(RETRY_SLEEP); + sleep($RETRY_SLEEP); } + return(); } -sub handle_command($$) { +sub handle_command { my ($buf, $current_client) = @_; if (my ($mac, undef, $interval) = $buf =~ m/^\s*(([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})\s*\|\s*(\d+)\s*$/) { $mac = lc($mac); @@ -345,6 +431,7 @@ sub handle_command($$) { foreach my $client (grep { $_->{'handle'} == $current_client } @clients) { $client->{'next_check'} = 0; #now } + $next_battery_time = time() + $SHORT_BATTERY_INTERVAL_S; print $current_client "command accepted\n" } elsif ($buf =~ m/^\s*ping\s*$/) { syslogw(LOG_DEBUG, "Received ping command from client %s:%i.", $current_client->peerhost(), $current_client->peerport()); @@ -362,7 +449,7 @@ sub handle_command($$) { return(0); } -sub gather_stats() { +sub gather_stats { my ($min_age, $max_age, $devices); { lock(%devices); @@ -376,34 +463,40 @@ sub gather_stats() { return($min_age, $max_age, $devices); } -sub stats_task() { +sub stats_task { my ($min_age, $max_age, $devices) = gather_stats(); syslogw(LOG_INFO, "Active clients: %i, known devices: %i (min/max age: %s/%s), received beacons (hcitool/hcidump/difference): %i/%i/%i", scalar(@clients), $devices, $min_age // '%', $max_age // '%', $beacons_hcitool, $beacons_hcidump, abs($beacons_hcitool - $beacons_hcidump)); + return(); } -sub dump_task() { +sub dump_task { printf("Known devices (%i):\n", scalar(keys(%devices))); foreach my $mac (sort keys(%devices)) { - printf("\tmac: %s, ages: %2s/%2s, rssi: %s, name: %s\n", + printf("\tmac: %s, ages: %2s/%2s, rssi: %s, name: %s, battery: %s\n", $mac, time() - $devices{$mac}{'timestamp'}, $devices{$mac}{'prevtimestamp'} ? time() - $devices{$mac}{'prevtimestamp'} : '%', $devices{$mac}{'rssi'}, - $devices{$mac}{'name'} + $devices{$mac}{'name'}, + exists($devices{$mac}{'battery_level'}) ? sprintf("%s (age: %ss)", $devices{$mac}{'battery_level'}, time() - $devices{$mac}{'battery_time'}) : 'unknown' ); } printf("Received beacons (hcitool/hcidump): %i/%i, difference: %i\n", $beacons_hcitool, $beacons_hcidump, abs($beacons_hcitool - $beacons_hcidump)); + return(); } -sub cleanup_task() { +sub cleanup_task { my $start_time = time(); my $deleted_items = 0; { lock(%devices); foreach my $mac (keys(%devices)) { my $age = time() - $devices{$mac}{'timestamp'}; - if ($age > CLEANUP_MAX_AGE) { + if ( + $age > $CLEANUP_MAX_AGE && + scalar(grep { $_->{'mac'} eq $mac } @clients) == 0 + ) { $deleted_items++; syslogw(LOG_DEBUG, "Deleting device %s.", $mac); delete($devices{$mac}); @@ -411,30 +504,103 @@ sub cleanup_task() { } } syslogw(LOG_INFO, "Cleanup finished, deleted %i devices in %i seconds.", $deleted_items, time() - $start_time); + return(); +} +sub get_battery_level { + my ($device, $mac) = @_; + my $address_type = $devices{$mac}{'address_type'} // 'public'; + open(my $gatttool, "-|", "gatttool -i $device -b $mac -t $address_type --char-read --uuid=$BATTERY_LEVEL_CHARACTERISTIC_UUID 2>&1") || die('Error executing gatttool!'); + + my $result = 'unknown'; + while (<$gatttool>) { + chomp($_); + syslogw(LOG_DEBUG, "gatttool (mac: %s, address type: '%s'): '%s'", $mac, $address_type, $_); + if ($_ =~ m/^handle:\s[0-9A-Fa-fx]+\s+value:\s([0-9a-f]+)\s*$/) { + # Success: 'handle: 0x0028 value: 64' + $result = hex($1); + } elsif ($_ =~ m/^Read characteristics by UUID failed: No attribute found within the given range$/) { + # Unsupported: 'Read characteristics by UUID failed: No attribute found within the given range' + $result = 'unknown (unsupported)'; + printf + } elsif ($_ =~ m/^connect error: Connection refused \(111\)$/) { + # Unreachable (after 40s): 'connect error: Connection refused (111)' + # Shouldn't happen very often because we try to query only reachable clients + $result = 'unknown (timeout)'; + } + } + close($gatttool); + return($result); +} +sub battery_task { + my ($device) = @_; + my @present_clients; + foreach my $client (@clients) { + push(@present_clients, $client) if (is_present($client)); + } + + if (scalar(@present_clients) > 0) { + syslogw(LOG_INFO, "Starting battery task, %i reachable device(s) to query...", scalar(@present_clients)); + + set_thread_command('bluetooth_dump_thread', $THREAD_COMMAND_STOP); + set_thread_command('bluetooth_scan_thread', $THREAD_COMMAND_STOP); + sleep($BATTERY_TASK_SETTLE_PRE_SLEEP); + + foreach my $client (@present_clients) { + my $battery_level = get_battery_level($device, $client->{'mac'}); + syslogw(LOG_INFO, "Battery level for mac %s is %s.", $client->{'mac'}, $battery_level); + # Don't overwrite a valid battery level with unknown + if(defined($devices{$client->{'mac'}}) && $battery_level !~ m/^unknown/) { + lock(%devices); + $devices{$client->{'mac'}}{'battery_level'} = $battery_level; + $devices{$client->{'mac'}}{'battery_time'} = time(); + # allow present clients a full interval to recover after scan stop + $client->{'next_check'} = time() + $client->{'interval'}; + } + } + + sleep($BATTERY_TASK_SETTLE_POST_SLEEP); + set_thread_command('bluetooth_scan_thread', $THREAD_COMMAND_RUN); + set_thread_command('bluetooth_dump_thread', $THREAD_COMMAND_RUN); + + syslogw(LOG_INFO, "Battery task completed."); + } else { + syslogw(LOG_INFO, "Skipping battery task, no devices to query."); + } + return(); } -openlog(ME, 'pid', LOG_USER); -(my $device, my $daemonize, my $listen_address, my $listen_port, $log_level, $log_target, $debug, my $legacy_mode, my $rssi_threshold) = parse_options(); +sub is_present { + my ($client) = @_; + return( + defined($devices{$client->{'mac'}}) && + time()-$devices{$client->{'mac'}}{timestamp} <= $client->{'interval'} && + defined($devices{$client->{'mac'}}{prevtimestamp}) && time()-$devices{$client->{'mac'}}{prevtimestamp} <= $client->{'interval'} + ); +} + +openlog($ME, 'pid', LOG_USER); +(my $device, my $daemonize, my $listen_address, my $listen_port, $log_level, $log_target, $debug, my $legacy_mode, my $rssi_threshold, my $battery_interval_h) = parse_options(); local $SIG{INT} = local $SIG{TERM} = local $SIG{HUP} = sub { syslogw(LOG_NOTICE, "Caught signal, cleaning up and exiting..."); - unlink(PIDFILE) if (-e PIDFILE); + unlink($PIDFILE) if (-e $PIDFILE); closelog(); exit(1); }; -syslogw(LOG_NOTICE, "Version %s started (device: %s, listen addr: %s, listen port: %s, daemonize: %i, legacy mode: %i, rssi threshold: %i, log level: %i, debug: %i).", - VERSION, $device, $listen_address, $listen_port, $daemonize, $legacy_mode, $rssi_threshold, $log_level, $debug); +syslogw(LOG_NOTICE, "Version %s started (device: %s, listen addr: %s, listen port: %s, daemonize: %i, legacy mode: %i, rssi threshold: %i, battery interval: %i, log level: %i, debug: %i).", + $VERSION, $device, $listen_address, $listen_port, $daemonize, $legacy_mode, $rssi_threshold, $battery_interval_h, $log_level, $debug); sanity_check($legacy_mode); -daemonize('root', 'root', PIDFILE) if $daemonize; +daemonize('root', 'root', $PIDFILE) if $daemonize; -my $bluetooth_scan_thread = threads->new(\&bluetooth_scan_thread, $device, $legacy_mode)->detach(); -my $bluetooth_dump_thread = threads->new(\&bluetooth_dump_thread, $device)->detach() if (!$legacy_mode); +my ($bluetooth_dump_thread, $bluetooth_scan_thread); +$bluetooth_scan_thread = threads->new(\&bluetooth_scan_thread, $device, $legacy_mode)->detach(); +$bluetooth_dump_thread = threads->new(\&bluetooth_dump_thread, $device)->detach() if (!$legacy_mode); my $current_client; -$| = 1; -my $server_socket = new IO::Socket::INET ( +local $| = 1; +my $server_socket = IO::Socket::INET->new( LocalHost => $listen_address, LocalPort => $listen_port, Proto => 'tcp', @@ -444,11 +610,11 @@ my $server_socket = new IO::Socket::INET ( $server_socket or error_exit(2, "ERROR: Unable to create TCP server: $!, Exiting."); my $select = IO::Select->new($server_socket) or error_exit(1, "ERROR: Unable to select: $!, Exiting."); -my $next_stats_time = time() + STATS_INTERVAL; -my $next_dump_time = time() + DUMP_INTERVAL if ($debug); -my $next_cleanup_time = time() + CLEANUP_INTERVAL; +$next_stats_time = time() + $STATS_INTERVAL_DEBUG; +$next_dump_time = time() + $DUMP_INTERVAL if ($debug); +$next_cleanup_time = time() + $CLEANUP_INTERVAL; -$SIG{PIPE} = sub { +local $SIG{PIPE} = sub { syslogw(LOG_INFO, "SIGPIPE received!"); }; @@ -460,7 +626,7 @@ for(;;) { $select->add($client_socket); syslogw(LOG_INFO, "Connection from %s:%s. Connected clients: %i.", $client_socket->peerhost(), $client_socket->peerport(), $select->count()-1); } else { - sysread ($current_client, my $buf, INET_RECV_BUFFER); + sysread ($current_client, my $buf, $INET_RECV_BUFFER); my $disconnect; if ($buf) { chomp($buf); @@ -490,21 +656,33 @@ for(;;) { } } } - + # Check for due client updates, cleanup, stats # For performance reasons, a maximum of one task is performed per loop if (my @due_clients = grep { time() >= $_->{'next_check'} } @clients) { foreach my $client (@due_clients) { - if ( - defined($devices{$client->{'mac'}}) && - time()-$devices{$client->{'mac'}}{timestamp} <= $client->{'interval'} && - defined($devices{$client->{'mac'}}{prevtimestamp}) && time()-$devices{$client->{'mac'}}{prevtimestamp} <= $client->{'interval'} - ) { - syslogw(LOG_DEBUG, "Sending update for mac address %s, ages: %i/%i, max age: %i, rssi: %i, result: present.", $client->{'mac'}, time()-$devices{$client->{'mac'}}{'timestamp'}, time()-$devices{$client->{'mac'}}{'prevtimestamp'}, $client->{'interval'}, $devices{$client->{'mac'}}{'rssi'}); - printf {$client->{'handle'}} "present;device_name=%s;rssi=%s;model=lan-lepresenced;daemon=%s V%s\n", $devices{$client->{'mac'}}{name}, $devices{$client->{'mac'}}{'rssi'}, ME, VERSION; + if (is_present($client)) { + my $battery_age = exists($devices{$client->{'mac'}}{'battery_time'}) ? int((time() - $devices{$client->{'mac'}}{'battery_time'})/3600) : 'unknown'; + my $send_battery = defined($devices{$client->{'mac'}}{'battery_level'}) && $battery_age ne 'unknown' && $battery_age <= $battery_interval_h * $BATTERY_MAX_AGE_FACTOR; + syslogw(LOG_DEBUG, "Sending update for mac address %s, ages: %i/%i, max age: %i, rssi: %i, battery level: %s (age: %s)%s, result: present.", + $client->{'mac'}, + time()-$devices{$client->{'mac'}}{'timestamp'}, + time()-$devices{$client->{'mac'}}{'prevtimestamp'}, + $client->{'interval'}, + $devices{$client->{'mac'}}{'rssi'}, + $devices{$client->{'mac'}}{'battery_level'} // 'unknown', + $battery_age, + $send_battery ? '' : ' (ignored)' + ); + printf {$client->{'handle'}} "present;device_name=%s;rssi=%s%s;model=lan-lepresenced;daemon=%s V%s\n", + $devices{$client->{'mac'}}{'name'}, + $devices{$client->{'mac'}}{'rssi'}, + $send_battery ? sprintf(";batteryPercent=%s;batteryPercentAge=%s", $devices{$client->{'mac'}}{'battery_level'} // 'unknown', $battery_age) : '', + $ME, $VERSION + ; } else { syslogw(LOG_DEBUG, "Sending update for mac address %s, max age: %i, result: absence.", $client->{'mac'}, $client->{'interval'}); - printf {$client->{'handle'}} "absence;rssi=unreachable;model=lan-lepresenced;daemon=%s V%s\n", ME, VERSION; + printf {$client->{'handle'}} "absence;rssi=unreachable;model=lan-lepresenced;daemon=%s V%s\n", $ME, $VERSION; } if (defined($devices{$client->{'mac'}})) { lock(%devices); @@ -514,16 +692,18 @@ for(;;) { } } elsif (time() > $next_cleanup_time) { cleanup_task(); - $next_cleanup_time = time() + CLEANUP_INTERVAL; + $next_cleanup_time = time() + $CLEANUP_INTERVAL; } elsif (time() > $next_stats_time) { stats_task(); - $next_stats_time = time() + STATS_INTERVAL; + $next_stats_time = time() + ($log_level == LOG_DEBUG ? $STATS_INTERVAL_DEBUG : $STATS_INTERVAL_INFO); } elsif ($debug && time() > $next_dump_time) { dump_task(); - $next_dump_time = time() + DUMP_INTERVAL; + $next_dump_time = time() + $DUMP_INTERVAL; + } elsif (time() > $next_battery_time) { + battery_task($device); + $next_battery_time = time() + $battery_interval_h * 60 * 60; } - - usleep(MAINLOOP_SLEEP_US); + usleep($MAINLOOP_SLEEP_US); } $server_socket->close();