From 4902475caf8b8efa631e7cf763dc4de904325cc8 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 9 Mar 2021 11:50:40 +0100 Subject: [PATCH] Added active_scan mode to bluetooth.scanner backend to actively perform a lookup name on each device discovered at least once [see #174] --- .../backend/bluetooth/scanner/__init__.py | 84 +++++++++++++++++-- platypush/plugins/bluetooth/__init__.py | 2 +- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/platypush/backend/bluetooth/scanner/__init__.py b/platypush/backend/bluetooth/scanner/__init__.py index a9eed886..60140453 100644 --- a/platypush/backend/bluetooth/scanner/__init__.py +++ b/platypush/backend/bluetooth/scanner/__init__.py @@ -1,6 +1,9 @@ +import time +from threading import Thread, RLock from typing import Dict, Optional from platypush.backend.sensor import SensorBackend +from platypush.context import get_plugin from platypush.message.event.bluetooth import BluetoothDeviceFoundEvent, BluetoothDeviceLostEvent @@ -20,10 +23,13 @@ class BluetoothScannerBackend(SensorBackend): """ - def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10, **kwargs): + def __init__(self, device_id: Optional[int] = None, scan_duration: int = 10, active_scan: bool = False, **kwargs): """ :param device_id: Bluetooth adapter ID to use (default configured on the ``bluetooth`` plugin if None). :param scan_duration: How long the scan should run (default: 10 seconds). + :param active_scan: Set to True if you want to actively track the devices through a periodic + ``.lookup_name`` call. This is likely to produce a more accurate tracking, but it may negatively impact the + duration of devices that run on battery. """ super().__init__(plugin='bluetooth', plugin_args={ 'device_id': device_id, @@ -31,17 +37,79 @@ class BluetoothScannerBackend(SensorBackend): }, **kwargs) self._last_seen_devices = {} + self._devices_to_track = set() + self._tracking_thread: Optional[Thread] = None + self._bt_lock = RLock() + self.active_scan = active_scan + self.scan_duration = scan_duration + + def _add_last_seen_device(self, dev): + addr = dev.pop('addr') + if addr not in self._last_seen_devices: + self.bus.post(BluetoothDeviceFoundEvent(address=addr, **dev)) + self._last_seen_devices[addr] = {'addr': addr, **dev} + + if self.active_scan and addr not in self._devices_to_track: + self._devices_to_track.add(addr) + + def _remove_last_seen_device(self, addr: str): + dev = self._last_seen_devices.get(addr) + if not dev: + return + + self.bus.post(BluetoothDeviceLostEvent(address=addr, **dev)) + del self._last_seen_devices[addr] + + def _addr_tracker(self, addr): + with self._bt_lock: + name = get_plugin('bluetooth').lookup_name(addr, timeout=self.scan_duration) + + if name is None: + self._remove_last_seen_device(addr) + else: + self._add_last_seen_device({'addr': addr, 'name': name}) + + def _bt_tracker(self): + self.logger.info('Starting Bluetooth tracker') + while not self.should_stop(): + trackers = [] + for addr in self._devices_to_track: + tracker = Thread(target=self._addr_tracker, args=(addr,)) + tracker.start() + trackers.append(tracker) + + for tracker in trackers: + tracker.join(timeout=self.scan_duration) + + time.sleep(self.scan_duration) + + self.logger.info('Bluetooth tracker stopped') + + def get_measurement(self): + with self._bt_lock: + return super().get_measurement() def process_data(self, data: Dict[str, dict], new_data: Dict[str, dict]): for addr, dev in data.items(): - if addr not in self._last_seen_devices: - self.bus.post(BluetoothDeviceFoundEvent(address=dev.pop('addr'), **dev)) - self._last_seen_devices[addr] = {'addr': addr, **dev} + self._add_last_seen_device(dev) - for addr, dev in self._last_seen_devices.copy().items(): - if addr not in data: - self.bus.post(BluetoothDeviceLostEvent(address=dev.pop('addr'), **dev)) - del self._last_seen_devices[addr] + if not self.active_scan: + for addr, dev in self._last_seen_devices.copy().items(): + if addr not in data: + self._remove_last_seen_device(addr) + + def run(self): + if self.active_scan: + self._tracking_thread = Thread(target=self._bt_tracker) + self._tracking_thread.start() + + super().run() + + def on_stop(self): + super().on_stop() + if self._tracking_thread and self._tracking_thread.is_alive(): + self.logger.info('Waiting for the Bluetooth tracking thread to stop') + self._tracking_thread.join(timeout=self.scan_duration) # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/bluetooth/__init__.py b/platypush/plugins/bluetooth/__init__.py index 1d4e3ba2..0c72921e 100644 --- a/platypush/plugins/bluetooth/__init__.py +++ b/platypush/plugins/bluetooth/__init__.py @@ -118,7 +118,7 @@ class BluetoothPlugin(SensorPlugin): """ from bluetooth import lookup_name - self.logger.info('Looking up name for device {}'.format(addr)) + self.logger.debug('Looking up name for device {}'.format(addr)) name = lookup_name(addr, timeout=timeout) dev = {