class BleakScannerDotNet(BaseBleakScanner): """The native Windows Bleak BLE Scanner. Implemented using `pythonnet <https://pythonnet.github.io/>`_, a package that provides an integration to the .NET Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel. Keyword Args: scanning mode (str): Set to ``Passive`` to avoid the ``Active`` scanning mode. SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement filtering that uses signal strength-based filtering. AdvertisementFilter (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter``): A BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE advertisement filtering that uses payload section-based filtering. """ def __init__(self, **kwargs): super(BleakScannerDotNet, self).__init__(**kwargs) self.watcher = None self._devices = {} self._scan_responses = {} self._received_token = None self._stopped_token = None if "scanning_mode" in kwargs and kwargs["scanning_mode"].lower( ) == "passive": self._scanning_mode = BluetoothLEScanningMode.Passive else: self._scanning_mode = BluetoothLEScanningMode.Active self._signal_strength_filter = kwargs.get("SignalStrengthFilter", None) self._advertisement_filter = kwargs.get("AdvertisementFilter", None) def _received_handler( self, sender: BluetoothLEAdvertisementWatcher, event_args: BluetoothLEAdvertisementReceivedEventArgs, ): if sender == self.watcher: logger.debug("Received {0}.".format( _format_event_args(event_args))) if (event_args.AdvertisementType == BluetoothLEAdvertisementType.ScanResponse): if event_args.BluetoothAddress not in self._scan_responses: self._scan_responses[ event_args.BluetoothAddress] = event_args else: if event_args.BluetoothAddress not in self._devices: self._devices[event_args.BluetoothAddress] = event_args if self._callback is None: return # Get a "BLEDevice" from parse_event args device = self.parse_eventargs(event_args) # Decode service data service_data = {} # 0x16 is service data with 16-bit UUID for section in event_args.Advertisement.GetSectionsByType(0x16): with BleakDataReader(section.Data) as reader: data = reader.read() service_data[ f"0000{data[1]:02x}{data[0]:02x}-0000-1000-8000-00805f9b34fb"] = data[ 2:] # 0x20 is service data with 32-bit UUID for section in event_args.Advertisement.GetSectionsByType(0x20): with BleakDataReader(section.Data) as reader: data = reader.read() service_data[ f"{data[3]:02x}{data[2]:02x}{data[1]:02x}{data[0]:02x}-0000-1000-8000-00805f9b34fb"] = data[ 4:] # 0x21 is service data with 128-bit UUID for section in event_args.Advertisement.GetSectionsByType(0x21): with BleakDataReader(section.Data) as reader: data = reader.read() service_data[str(UUID(bytes=data[15::-1]))] = data[16:] # Use the BLEDevice to populate all the fields for the advertisement data to return advertisement_data = AdvertisementData( local_name=event_args.Advertisement.LocalName, manufacturer_data=device.metadata["manufacturer_data"], service_data=service_data, service_uuids=device.metadata["uuids"], platform_data=(sender, event_args), ) self._callback(device, advertisement_data) def _stopped_handler( self, sender: BluetoothLEAdvertisementWatcher, e: BluetoothLEAdvertisementWatcherStoppedEventArgs, ): if sender == self.watcher: logger.debug("{0} devices found. Watcher status: {1}.".format( len(self._devices), self.watcher.Status)) async def start(self): self.watcher = BluetoothLEAdvertisementWatcher() self.watcher.ScanningMode = self._scanning_mode event_loop = asyncio.get_event_loop() self._received_token = self.watcher.add_Received( TypedEventHandler[BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementReceivedEventArgs, ]( lambda s, e: event_loop.call_soon_threadsafe( self._received_handler, s, e))) self._stopped_token = self.watcher.add_Stopped(TypedEventHandler[ BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs, ]( lambda s, e: event_loop.call_soon_threadsafe( self._stopped_handler, s, e))) if self._signal_strength_filter is not None: self.watcher.SignalStrengthFilter = self._signal_strength_filter if self._advertisement_filter is not None: self.watcher.AdvertisementFilter = self._advertisement_filter self.watcher.Start() async def stop(self): self.watcher.Stop() if self._received_token: self.watcher.remove_Received(self._received_token) self._received_token = None if self._stopped_token: self.watcher.remove_Stopped(self._stopped_token) self._stopped_token = None self.watcher = None async def set_scanning_filter(self, **kwargs): """Set a scanning filter for the BleakScanner. Keyword Args: SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement filtering that uses signal strength-based filtering. AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE advertisement filtering that uses payload section-based filtering. """ if "SignalStrengthFilter" in kwargs: # TODO: Handle SignalStrengthFilter parameters self._signal_strength_filter = kwargs["SignalStrengthFilter"] if "AdvertisementFilter" in kwargs: # TODO: Handle AdvertisementFilter parameters self._advertisement_filter = kwargs["AdvertisementFilter"] async def get_discovered_devices(self) -> List[BLEDevice]: found = [] for event_args in list(self._devices.values()): new_device = self.parse_eventargs(event_args) if (not new_device.name and event_args.BluetoothAddress in self._scan_responses): new_device.name = self._scan_responses[ event_args.BluetoothAddress].Advertisement.LocalName found.append(new_device) return found @staticmethod def parse_eventargs(event_args): bdaddr = _format_bdaddr(event_args.BluetoothAddress) uuids = [] for u in event_args.Advertisement.ServiceUuids: uuids.append(u.ToString()) data = {} for m in event_args.Advertisement.ManufacturerData: with BleakDataReader(m.Data) as reader: data[m.CompanyId] = reader.read() local_name = event_args.Advertisement.LocalName rssi = event_args.RawSignalStrengthInDBm return BLEDevice(bdaddr, local_name, event_args, rssi, uuids=uuids, manufacturer_data=data) # Windows specific @property def status(self) -> int: """Get status of the Watcher. Returns: Aborted 4 An error occurred during transition or scanning that stopped the watcher due to an error. Created 0 The initial status of the watcher. Started 1 The watcher is started. Stopped 3 The watcher is stopped. Stopping 2 The watcher stop command was issued. """ return self.watcher.Status if self.watcher else None
async def discover(timeout: float = 5.0, **kwargs) -> List[BLEDevice]: """Perform a Bluetooth LE Scan using Windows.Devices.Bluetooth.Advertisement Args: timeout (float): Time to scan for. Keyword Args: SignalStrengthFilter (Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter): A BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement filtering that uses signal strength-based filtering. AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE advertisement filtering that uses payload section-based filtering. string_output (bool): If set to false, ``discover`` returns .NET device objects instead. Returns: List of strings or objects found. """ signal_strength_filter = kwargs.get("SignalStrengthFilter", None) advertisement_filter = kwargs.get("AdvertisementFilter", None) watcher = BluetoothLEAdvertisementWatcher() devices = {} scan_responses = {} def _format_bdaddr(a): return ":".join("{:02X}".format(x) for x in a.to_bytes(6, byteorder="big")) def _format_event_args(e): try: return "{0}: {1}".format( _format_bdaddr(e.BluetoothAddress), e.Advertisement.LocalName or "Unknown", ) except Exception: return e.BluetoothAddress def _received_handler(sender, e): if sender == watcher: logger.debug("Received {0}.".format(_format_event_args(e))) if e.AdvertisementType == BluetoothLEAdvertisementType.ScanResponse: if e.BluetoothAddress not in scan_responses: scan_responses[e.BluetoothAddress] = e else: if e.BluetoothAddress not in devices: devices[e.BluetoothAddress] = e def _stopped_handler(sender, e): if sender == watcher: logger.debug( "{0} devices found. Watcher status: {1}.".format( len(devices), watcher.Status ) ) received_token = watcher.add_Received( TypedEventHandler[ BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementReceivedEventArgs, ](_received_handler) ) stopped_token = watcher.add_Stopped( TypedEventHandler[ BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs, ](_stopped_handler) ) watcher.ScanningMode = BluetoothLEScanningMode.Active if signal_strength_filter is not None: watcher.SignalStrengthFilter = signal_strength_filter if advertisement_filter is not None: watcher.AdvertisementFilter = advertisement_filter # Watcher works outside of the Python process. watcher.Start() await asyncio.sleep(timeout) watcher.Stop() watcher.remove_Received(received_token) watcher.remove_Stopped(stopped_token) found = [] for d in list(devices.values()): bdaddr = _format_bdaddr(d.BluetoothAddress) uuids = [] for u in d.Advertisement.ServiceUuids: uuids.append(u.ToString()) data = {} for m in d.Advertisement.ManufacturerData: with BleakDataReader(m.Data) as reader: data[m.CompanyId] = reader.read() local_name = d.Advertisement.LocalName if not local_name and d.BluetoothAddress in scan_responses: local_name = scan_responses[d.BluetoothAddress].Advertisement.LocalName found.append( BLEDevice( bdaddr, local_name, d, uuids=uuids, manufacturer_data=data, ) ) return found
class BleakScannerDotNet(BaseBleakScanner): """The native Windows Bleak BLE Scanner. Implemented using `pythonnet <https://pythonnet.github.io/>`_, a package that provides an integration to the .NET Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel. Keyword Args: scanning mode (str): Set to ``Passive`` to avoid the ``Active`` scanning mode. SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement filtering that uses signal strength-based filtering. AdvertisementFilter (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter``): A BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE advertisement filtering that uses payload section-based filtering. """ def __init__(self, **kwargs): super(BleakScannerDotNet, self).__init__() self.watcher = None self._devices = {} self._scan_responses = {} self._callback = None self._received_token = None self._stopped_token = None if "scanning_mode" in kwargs and kwargs["scanning_mode"].lower( ) == "passive": self._scanning_mode = BluetoothLEScanningMode.Passive else: self._scanning_mode = BluetoothLEScanningMode.Active self._signal_strength_filter = kwargs.get("SignalStrengthFilter", None) self._advertisement_filter = kwargs.get("AdvertisementFilter", None) def _received_handler(self, sender, event_args): if sender == self.watcher: logger.debug("Received {0}.".format( _format_event_args(event_args))) if (event_args.AdvertisementType == BluetoothLEAdvertisementType.ScanResponse): if event_args.BluetoothAddress not in self._scan_responses: self._scan_responses[ event_args.BluetoothAddress] = event_args else: if event_args.BluetoothAddress not in self._devices: self._devices[event_args.BluetoothAddress] = event_args if self._callback is not None: self._callback(sender, event_args) def _stopped_handler(self, sender, event_args): if sender == self.watcher: logger.debug("{0} devices found. Watcher status: {1}.".format( len(self._devices), self.watcher.Status)) async def start(self): self.watcher = BluetoothLEAdvertisementWatcher() self.watcher.ScanningMode = self._scanning_mode self._received_token = self.watcher.add_Received( TypedEventHandler[BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementReceivedEventArgs, ]( self._received_handler)) self._stopped_token = self.watcher.add_Stopped(TypedEventHandler[ BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs, ]( self._stopped_handler)) if self._signal_strength_filter is not None: self.watcher.SignalStrengthFilter = self._signal_strength_filter if self._advertisement_filter is not None: self.watcher.AdvertisementFilter = self._advertisement_filter self.watcher.Start() async def stop(self): self.watcher.Stop() if self._received_token: self.watcher.remove_Received(self._received_token) self._received_token = None if self._stopped_token: self.watcher.remove_Stopped(self._stopped_token) self._stopped_token = None self.watcher = None async def set_scanning_filter(self, **kwargs): """Set a scanning filter for the BleakScanner. Keyword Args: SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement filtering that uses signal strength-based filtering. AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE advertisement filtering that uses payload section-based filtering. """ if "SignalStrengthFilter" in kwargs: # TODO: Handle SignalStrengthFilter parameters self._signal_strength_filter = kwargs["SignalStrengthFilter"] if "AdvertisementFilter" in kwargs: # TODO: Handle AdvertisementFilter parameters self._advertisement_filter = kwargs["AdvertisementFilter"] async def get_discovered_devices(self) -> List[BLEDevice]: found = [] for event_args in list(self._devices.values()): new_device = self.parse_eventargs(event_args) if (not new_device.name and event_args.BluetoothAddress in self._scan_responses): new_device.name = self._scan_responses[ event_args.BluetoothAddress].Advertisement.LocalName found.append(new_device) return found @staticmethod def parse_eventargs(event_args): bdaddr = _format_bdaddr(event_args.BluetoothAddress) uuids = [] for u in event_args.Advertisement.ServiceUuids: uuids.append(u.ToString()) data = {} for m in event_args.Advertisement.ManufacturerData: with BleakDataReader(m.Data) as reader: data[m.CompanyId] = reader.read() local_name = event_args.Advertisement.LocalName return BLEDevice(bdaddr, local_name, event_args, uuids=uuids, manufacturer_data=data) def register_detection_callback(self, callback: Callable): """Set a function to act as Received Event Handler. Documentation for the Event Handler: https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcher.received Args: callback: Function accepting two arguments: sender (``Windows.Devices.Bluetooth.AdvertisementBluetoothLEAdvertisementWatcher``) and eventargs (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementReceivedEventArgs``) """ self._callback = callback # Windows specific @property def status(self) -> int: """Get status of the Watcher. Returns: Aborted 4 An error occurred during transition or scanning that stopped the watcher due to an error. Created 0 The initial status of the watcher. Started 1 The watcher is started. Stopped 3 The watcher is stopped. Stopping 2 The watcher stop command was issued. """ return self.watcher.Status if self.watcher else None @classmethod async def find_device_by_address(cls, device_identifier: str, timeout: float = 10.0, **kwargs) -> Union[BLEDevice, None]: """A convenience method for obtaining a ``BLEDevice`` object specified by Bluetooth address. Args: device_identifier (str): The Bluetooth address of the Bluetooth peripheral. timeout (float): Optional timeout to wait for detection of specified peripheral before giving up. Defaults to 10.0 seconds. Keyword Args: scanning mode (str): Set to ``Passive`` to avoid the ``Active`` scanning mode. SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement filtering that uses signal strength-based filtering. AdvertisementFilter (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter``): A BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE advertisement filtering that uses payload section-based filtering. Returns: The ``BLEDevice`` sought or ``None`` if not detected. """ ulong_id = int(device_identifier.replace(":", ""), 16) loop = asyncio.get_event_loop() stop_scanning_event = asyncio.Event() scanner = cls(timeout=timeout) def stop_if_detected(sender, event_args): if event_args.BluetoothAddress == ulong_id: loop.call_soon_threadsafe(stop_scanning_event.set) return await scanner._find_device_by_address(device_identifier, stop_scanning_event, stop_if_detected, timeout)