class BleakScannerBlueZDBus(BaseBleakScanner): """The native Linux Bleak BLE Scanner. Args: loop (asyncio.events.AbstractEventLoop): The event loop to use. Keyword Args: """ def __init__(self, loop: AbstractEventLoop = None, **kwargs): super(BleakScannerBlueZDBus, self).__init__(loop, **kwargs) self._device = kwargs.get("device", "hci0") self._reactor = None self._bus = None self._cached_devices = {} self._devices = {} self._rules = list() # Discovery filters self._filters = kwargs.get("filters", {}) self._filters["Transport"] = "le" self._adapter_path = None self._interface = None self._callback = None async def start(self): self._reactor = AsyncioSelectorReactor(self.loop) self._bus = await client.connect(self._reactor, "system").asFuture(self.loop) # Add signal listeners self._rules.append( await self._bus.addMatch( self.parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesAdded", ).asFuture(self.loop) ) self._rules.append( await self._bus.addMatch( self.parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesRemoved", ).asFuture(self.loop) ) self._rules.append( await self._bus.addMatch( self.parse_msg, interface="org.freedesktop.DBus.Properties", member="PropertiesChanged", ).asFuture(self.loop) ) # Find the HCI device to use for scanning and get cached device properties objects = await self._bus.callRemote( "/", "GetManagedObjects", interface=defs.OBJECT_MANAGER_INTERFACE, destination=defs.BLUEZ_SERVICE, ).asFuture(self.loop) self._adapter_path, self._interface = _filter_on_adapter(objects, self._device) self._cached_devices = dict(_filter_on_device(objects)) # Apply the filters await self._bus.callRemote( self._adapter_path, "SetDiscoveryFilter", interface="org.bluez.Adapter1", destination="org.bluez", signature="a{sv}", body=[self._filters], ).asFuture(self.loop) # Start scanning await self._bus.callRemote( self._adapter_path, "StartDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(self.loop) async def stop(self): await self._bus.callRemote( self._adapter_path, "StopDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(self.loop) for rule in self._rules: await self._bus.delMatch(rule).asFuture(self.loop) self._rules.clear() # Try to disconnect the System Bus. try: self._bus.disconnect() except Exception as e: logger.error("Attempt to disconnect system bus failed: {0}".format(e)) try: self._reactor.stop() except ReactorNotRunning: pass self._bus = None self._reactor = None async def set_scanning_filter(self, **kwargs): self._filters = kwargs.get("filters", {}) self._filters["Transport"] = "le" async def get_discovered_devices(self) -> List[BLEDevice]: # Reduce output. discovered_devices = [] for path, props in self._devices.items(): if not props: logger.debug( "Disregarding %s since no properties could be obtained." % path ) continue name, address, _, path = _device_info(path, props) if address is None: continue uuids = props.get("UUIDs", []) manufacturer_data = props.get("ManufacturerData", {}) discovered_devices.append( BLEDevice( address, name, {"path": path, "props": props}, uuids=uuids, manufacturer_data=manufacturer_data, ) ) return discovered_devices def register_detection_callback(self, callback: Callable): """Set a function to be called on each Scanner discovery. Documentation for the Event Handler: https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcher.received Args: callback: Function accepting one argument of type ? """ self._callback = callback # Helper methods def parse_msg(self, message): if message.member == "InterfacesAdded": msg_path = message.body[0] try: device_interface = message.body[1].get("org.bluez.Device1", {}) except Exception as e: raise e self._devices[msg_path] = ( {**self._devices[msg_path], **device_interface} if msg_path in self._devices else device_interface ) elif message.member == "PropertiesChanged": iface, changed, invalidated = message.body if iface != defs.DEVICE_INTERFACE: return msg_path = message.path # the PropertiesChanged signal only sends changed properties, so we # need to get remaining properties from cached_devices. However, we # don't want to add all cached_devices to the devices dict since # they may not actually be nearby or powered on. if msg_path not in self._devices and msg_path in self._cached_devices: self._devices[msg_path] = self._cached_devices[msg_path] self._devices[msg_path] = ( {**self._devices[msg_path], **changed} if msg_path in self._devices else changed ) elif ( message.member == "InterfacesRemoved" and message.body[1][0] == defs.BATTERY_INTERFACE ): logger.info( "{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body ) ) return else: msg_path = message.path logger.info( "{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body ) ) logger.info( "{0}, {1} ({2} dBm), Object Path: {3}".format( *_device_info(msg_path, self._devices.get(msg_path)) ) ) if self._callback is not None: self._callback(message)
async def discover(timeout=5.0, loop=None, **kwargs): """Discover nearby Bluetooth Low Energy devices. For possible values for `filter`, see the parameters to the ``SetDiscoveryFilter`` method in the `BlueZ docs <https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt?h=5.48&id=0d1e3b9c5754022c779da129025d493a198d49cf>`_ The ``Transport`` parameter is always set to ``le`` by default in Bleak. Args: timeout (float): Duration to scan for. loop (asyncio.AbstractEventLoop): Optional event loop to use. Keyword Args: device (str): Bluetooth device to use for discovery. filters (dict): A dict of filters to be applied on discovery. Returns: List of tuples containing name, address and signal strength of nearby devices. """ device = kwargs.get("device", "hci0") loop = loop if loop else asyncio.get_event_loop() cached_devices = {} devices = {} rules = list() reactor = AsyncioSelectorReactor(loop) # Discovery filters filters = kwargs.get("filters", {}) filters["Transport"] = "le" def parse_msg(message): if message.member == "InterfacesAdded": msg_path = message.body[0] try: device_interface = message.body[1].get("org.bluez.Device1", {}) except Exception as e: raise e devices[msg_path] = ( {**devices[msg_path], **device_interface} if msg_path in devices else device_interface ) elif message.member == "PropertiesChanged": iface, changed, invalidated = message.body if iface != defs.DEVICE_INTERFACE: return msg_path = message.path # the PropertiesChanged signal only sends changed properties, so we # need to get remaining properties from cached_devices. However, we # don't want to add all cached_devices to the devices dict since # they may not actually be nearby or powered on. if msg_path not in devices and msg_path in cached_devices: devices[msg_path] = cached_devices[msg_path] devices[msg_path] = ( {**devices[msg_path], **changed} if msg_path in devices else changed ) elif ( message.member == "InterfacesRemoved" and message.body[1][0] == defs.BATTERY_INTERFACE ): logger.info( "{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body ) ) return else: msg_path = message.path logger.info( "{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body ) ) logger.info( "{0}, {1} ({2} dBm), Object Path: {3}".format( *_device_info(msg_path, devices.get(msg_path)) ) ) bus = await client.connect(reactor, "system").asFuture(loop) # Add signal listeners rules.append( await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesAdded", path_namespace="/org/bluez", ).asFuture(loop) ) rules.append( await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesRemoved", path_namespace="/org/bluez", ).asFuture(loop) ) rules.append( await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.Properties", member="PropertiesChanged", path_namespace="/org/bluez", ).asFuture(loop) ) # Find the HCI device to use for scanning and get cached device properties objects = await bus.callRemote( "/", "GetManagedObjects", interface=defs.OBJECT_MANAGER_INTERFACE, destination=defs.BLUEZ_SERVICE, ).asFuture(loop) adapter_path, interface = _filter_on_adapter(objects, device) cached_devices = dict(_filter_on_device(objects)) # Running Discovery loop. await bus.callRemote( adapter_path, "SetDiscoveryFilter", interface="org.bluez.Adapter1", destination="org.bluez", signature="a{sv}", body=[filters], ).asFuture(loop) await bus.callRemote( adapter_path, "StartDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(loop) await asyncio.sleep(timeout) await bus.callRemote( adapter_path, "StopDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(loop) # Reduce output. discovered_devices = [] for path, props in devices.items(): if not props: logger.debug( "Disregarding %s since no properties could be obtained." % path ) continue name, address, _, path = _device_info(path, props) if address is None: continue uuids = props.get("UUIDs", []) manufacturer_data = props.get("ManufacturerData", {}) discovered_devices.append( BLEDevice( address, name, {"path": path, "props": props}, uuids=uuids, manufacturer_data=manufacturer_data, ) ) for rule in rules: await bus.delMatch(rule).asFuture(loop) # Try to disconnect the System Bus. try: bus.disconnect() except Exception as e: logger.error("Attempt to disconnect system bus failed: {0}".format(e)) try: reactor.stop() except ReactorNotRunning: # I think Bleak will always end up here, but I want to call stop just in case... pass return discovered_devices
class BleakClientBlueZDBus(BaseBleakClient): """A native Linux Bleak Client Implemented by using the `BlueZ DBUS API <https://docs.ubuntu.com/core/en/stacks/bluetooth/bluez/docs/reference/dbus-api>`_. Args: address (str): The MAC address of the BLE peripheral to connect to. loop (asyncio.events.AbstractEventLoop): The event loop to use. Keyword Args: timeout (float): Timeout for required ``discover`` call. Defaults to 2.0. """ def __init__(self, address, loop=None, **kwargs): super(BleakClientBlueZDBus, self).__init__(address, loop, **kwargs) self.device = kwargs.get("device") if kwargs.get("device") else "hci0" self.address = address # Backend specific, TXDBus objects and data self._device_path = None self._bus = None self._reactor = None self._rules = {} self._subscriptions = list() self._disconnected_callback = None self._char_path_to_uuid = {} # We need to know BlueZ version since battery level characteristic # are stored in a separate DBus interface in the BlueZ >= 5.48. p = subprocess.Popen(["bluetoothctl", "--version"], stdout=subprocess.PIPE) out, _ = p.communicate() s = re.search(b"(\\d+).(\\d+)", out.strip(b"'")) self._bluez_version = tuple(map(int, s.groups())) # Connectivity methods def set_disconnected_callback( self, callback: Callable[[BaseBleakClient, Future], None], **kwargs ) -> None: """Set the disconnected callback. The callback will be called on DBus PropChanged event with the 'Connected' key set to False. A disconnect callback must accept two positional arguments, the BleakClient and the Future that called it. Example: .. code-block::python async with BleakClient(mac_addr, loop=loop) as client: def disconnect_callback(client, future): print(f"Disconnected callback called on {client}!") client.set_disconnected_callback(disconnect_callback) Args: callback: callback to be called on disconnection. """ self._disconnected_callback = callback async def connect(self, **kwargs) -> bool: """Connect to the specified GATT server. Keyword Args: timeout (float): Timeout for required ``discover`` call. Defaults to 2.0. Returns: Boolean representing connection status. """ # A Discover must have been run before connecting to any devices. Do a quick one here # to ensure that it has been done. timeout = kwargs.get("timeout", self._timeout) await discover(timeout=timeout, device=self.device, loop=self.loop) self._reactor = AsyncioSelectorReactor(self.loop) # Create system bus self._bus = await txdbus_connect(self._reactor, busAddress="system").asFuture( self.loop ) # TODO: Handle path errors from txdbus/dbus self._device_path = get_device_object_path(self.device, self.address) def _services_resolved_callback(message): iface, changed, invalidated = message.body is_resolved = defs.DEVICE_INTERFACE and changed.get( "ServicesResolved", False ) if iface == is_resolved: logger.info("Services resolved.") self.services_resolved = True rule_id = await signals.listen_properties_changed( self._bus, self.loop, _services_resolved_callback ) logger.debug( "Connecting to BLE device @ {0} with {1}".format(self.address, self.device) ) try: await self._bus.callRemote( self._device_path, "Connect", interface="org.bluez.Device1", destination="org.bluez", ).asFuture(self.loop) except RemoteError as e: raise BleakError(str(e)) if await self.is_connected(): logger.debug("Connection successful.") else: raise BleakError( "Connection to {0} was not successful!".format(self.address) ) # Get all services. This means making the actual connection. await self.get_services() properties = await self._get_device_properties() if not properties.get("Connected"): raise BleakError("Connection failed!") await self._bus.delMatch(rule_id).asFuture(self.loop) self._rules["PropChanged"] = await signals.listen_properties_changed( self._bus, self.loop, self._properties_changed_callback ) return True async def _cleanup(self) -> None: for rule_name, rule_id in self._rules.items(): logger.debug("Removing rule {0}, ID: {1}".format(rule_name, rule_id)) try: await self._bus.delMatch(rule_id).asFuture(self.loop) except Exception as e: logger.error("Could not remove rule {0} ({1}): {2}".format(rule_id, rule_name, e)) self._rules = {} for _uuid in list(self._subscriptions): try: await self.stop_notify(_uuid) except Exception as e: logger.error("Could not remove notifications on characteristic {0}: {1}".format(_uuid, e)) self._subscriptions = [] async def disconnect(self) -> bool: """Disconnect from the specified GATT server. Returns: Boolean representing if device is disconnected. """ logger.debug("Disconnecting from BLE device...") # Remove all residual notifications. await self._cleanup() # Try to disconnect the actual device/peripheral try: await self._bus.callRemote( self._device_path, "Disconnect", interface=defs.DEVICE_INTERFACE, destination=defs.BLUEZ_SERVICE, ).asFuture(self.loop) except Exception as e: logger.error("Attempt to disconnect device failed: {0}".format(e)) # See if it has been disconnected. is_disconnected = not await self.is_connected() # Try to disconnect the System Bus. try: self._bus.disconnect() except Exception as e: logger.error("Attempt to disconnect system bus failed: {0}".format(e)) # Stop the Twisted reactor holding the connection to the DBus system. try: self._reactor.stop() except Exception as e: # I think Bleak will always end up here, but I want to call stop just in case... logger.debug("Attempt to stop Twisted reactor failed: {0}".format(e)) finally: self._bus = None self._reactor = None return is_disconnected async def is_connected(self) -> bool: """Check connection status between this client and the server. Returns: Boolean representing connection status. """ # TODO: Listen to connected property changes. return await self._bus.callRemote( self._device_path, "Get", interface=defs.PROPERTIES_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="ss", body=[defs.DEVICE_INTERFACE, "Connected"], returnSignature="v", ).asFuture(self.loop) # GATT services methods async def get_services(self) -> BleakGATTServiceCollection: """Get all services registered for this GATT server. Returns: A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. """ if self._services_resolved: return self.services sleep_loop_sec = 0.02 total_slept_sec = 0 services_resolved = False while total_slept_sec < 5.0: properties = await self._get_device_properties() services_resolved = properties.get("ServicesResolved", False) if services_resolved: break await asyncio.sleep(sleep_loop_sec, loop=self.loop) total_slept_sec += sleep_loop_sec if not services_resolved: raise BleakError("Services discovery error") logger.debug("Get Services...") objs = await get_managed_objects( self._bus, self.loop, self._device_path + "/service" ) # There is no guarantee that services are listed before characteristics # Managed Objects dict. # Need multiple iterations to construct the Service Collection _chars, _descs = [], [] for object_path, interfaces in objs.items(): logger.debug(utils.format_GATT_object(object_path, interfaces)) if defs.GATT_SERVICE_INTERFACE in interfaces: service = interfaces.get(defs.GATT_SERVICE_INTERFACE) self.services.add_service( BleakGATTServiceBlueZDBus(service, object_path) ) elif defs.GATT_CHARACTERISTIC_INTERFACE in interfaces: char = interfaces.get(defs.GATT_CHARACTERISTIC_INTERFACE) _chars.append([char, object_path]) elif defs.GATT_DESCRIPTOR_INTERFACE in interfaces: desc = interfaces.get(defs.GATT_DESCRIPTOR_INTERFACE) _descs.append([desc, object_path]) for char, object_path in _chars: _service = list(filter(lambda x: x.path == char["Service"], self.services)) self.services.add_characteristic( BleakGATTCharacteristicBlueZDBus(char, object_path, _service[0].uuid) ) self._char_path_to_uuid[object_path] = char.get("UUID") for desc, object_path in _descs: _characteristic = list( filter( lambda x: x.path == desc["Characteristic"], self.services.characteristics.values(), ) ) self.services.add_descriptor( BleakGATTDescriptorBlueZDBus(desc, object_path, _characteristic[0].uuid) ) self._services_resolved = True return self.services # IO methods async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], **kwargs) -> bytearray: """Perform read operation on the specified GATT characteristic. Args: _uuid (str or UUID): The uuid of the characteristics to read from. Returns: (bytearray) The read data. """ characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: # Special handling for BlueZ >= 5.48, where Battery Service (0000180f-0000-1000-8000-00805f9b34fb:) # has been moved to interface org.bluez.Battery1 instead of as a regular service. if _uuid == "00002a19-0000-1000-8000-00805f9b34fb" and ( self._bluez_version[0] == 5 and self._bluez_version[1] >= 48 ): props = await self._get_device_properties( interface=defs.BATTERY_INTERFACE ) # Simulate regular characteristics read to be consistent over all platforms. value = bytearray([props.get("Percentage", "")]) logger.debug( "Read Battery Level {0} | {1}: {2}".format( _uuid, self._device_path, value ) ) return value if str(_uuid) == '00002a00-0000-1000-8000-00805f9b34fb' and ( self._bluez_version[0] == 5 and self._bluez_version[1] >= 48 ): props = await self._get_device_properties( interface=defs.DEVICE_INTERFACE ) # Simulate regular characteristics read to be consistent over all platforms. value = bytearray(props.get("Name", "").encode('ascii')) logger.debug( "Read Device Name {0} | {1}: {2}".format( _uuid, self._device_path, value ) ) return value raise BleakError( "Characteristic with UUID {0} could not be found!".format(_uuid) ) value = bytearray( await self._bus.callRemote( characteristic.path, "ReadValue", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="a{sv}", body=[{}], returnSignature="ay", ).asFuture(self.loop) ) logger.debug( "Read Characteristic {0} | {1}: {2}".format( _uuid, characteristic.path, value ) ) return value async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: """Perform read operation on the specified GATT descriptor. Args: handle (int): The handle of the descriptor to read from. Returns: (bytearray) The read data. """ descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError("Descriptor with handle {0} was not found!".format(handle)) value = bytearray( await self._bus.callRemote( descriptor.path, "ReadValue", interface=defs.GATT_DESCRIPTOR_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="a{sv}", body=[{}], returnSignature="ay", ).asFuture(self.loop) ) logger.debug( "Read Descriptor {0} | {1}: {2}".format(handle, descriptor.path, value) ) return value async def write_gatt_char( self, _uuid: Union[str, uuid.UUID], data: bytearray, response: bool = False ) -> None: """Perform a write operation on the specified GATT characteristic. NB: the version check below is for the "type" option to the "Characteristic.WriteValue" method that was added to Bluez in 5.50 ttps://git.kernel.org/pub/scm/bluetooth/bluez.git/commit?id=fa9473bcc48417d69cc9ef81d41a72b18e34a55a Before that commit, "Characteristic.WriteValue" was only "Write with response". "Characteristic.AcquireWrite" was added in Bluez 5.46 https://git.kernel.org/pub/scm/bluetooth/bluez.git/commit/doc/gatt-api.txt?id=f59f3dedb2c79a75e51a3a0d27e2ae06fefc603e which can be used to "Write without response", but for older versions of Bluez, it is not possible to "Write without response". Args: _uuid (str or UUID): The uuid of the characteristics to write to. data (bytes or bytearray): The data to send. response (bool): If write-with-response operation should be done. Defaults to `False`. """ characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: raise BleakError("Characteristic {0} was not found!".format(_uuid)) if ( "write" not in characteristic.properties and "write-without-response" not in characteristic.properties ): raise BleakError( "Characteristic %s does not support write operations!" % str(_uuid) ) if not response and "write-without-response" not in characteristic.properties: response = True # Force response here, since the device only supports that. if ( response and "write" not in characteristic.properties and "write-without-response" in characteristic.properties ): response = False logger.warning( "Characteristic %s does not support Write with response. Trying without..." % str(_uuid) ) # See docstring for details about this handling. if not response and self._bluez_version[0] == 5 and self._bluez_version[1] < 46: raise BleakError("Write without response requires at least BlueZ 5.46") if response or (self._bluez_version[0] == 5 and self._bluez_version[1] > 50): # TODO: Add OnValueUpdated handler for response=True? await self._bus.callRemote( characteristic.path, "WriteValue", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="aya{sv}", body=[data, {"type": "request" if response else "command"}], returnSignature="", ).asFuture(self.loop) else: # Older versions of BlueZ don't have the "type" option, so we have # to write the hard way. This isn't the most efficient way of doing # things, but it works. fd, _ = await self._bus.callRemote( characteristic.path, "AcquireWrite", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="a{sv}", body=[{}], returnSignature="hq", ).asFuture(self.loop) os.write(fd, data) os.close(fd) logger.debug( "Write Characteristic {0} | {1}: {2}".format( _uuid, characteristic.path, data ) ) async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None: """Perform a write operation on the specified GATT descriptor. Args: handle (int): The handle of the descriptor to read from. data (bytes or bytearray): The data to send. """ descriptor = self.services.get_descriptor(handle) if not descriptor: raise BleakError("Descriptor with handle {0} was not found!".format(handle)) await self._bus.callRemote( descriptor.path, 'WriteValue', interface=defs.GATT_DESCRIPTOR_INTERFACE, destination=defs.BLUEZ_SERVICE, signature='aya{sv}', body=[data, {'type': 'command'}], returnSignature='', ).asFuture(self.loop) logger.debug( "Write Descriptor {0} | {1}: {2}".format( handle, descriptor.path, data ) ) async def start_notify( self, _uuid: Union[str, uuid.UUID], callback: Callable[[str, Any], Any], **kwargs ) -> None: """Activate notifications/indications on a characteristic. Callbacks must accept two inputs. The first will be a uuid string object and the second will be a bytearray. .. code-block:: python def callback(sender, data): print(f"{sender}: {data}") client.start_notify(char_uuid, callback) Args: _uuid (str or UUID): The uuid of the characteristics to start notification on. callback (function): The function to be called on notification. Keyword Args: notification_wrapper (bool): Set to `False` to avoid parsing of notification to bytearray. """ _wrap = kwargs.get("notification_wrapper", True) characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: # Special handling for BlueZ >= 5.48, where Battery Service (0000180f-0000-1000-8000-00805f9b34fb:) # has been moved to interface org.bluez.Battery1 instead of as a regular service. # The org.bluez.Battery1 on the other hand does not provide a notification method, so here we cannot # provide this functionality... # See https://kernel.googlesource.com/pub/scm/bluetooth/bluez/+/refs/tags/5.48/doc/battery-api.txt if str(_uuid) == "00002a19-0000-1000-8000-00805f9b34fb" and ( self._bluez_version[0] == 5 and self._bluez_version[1] >= 48 ): raise BleakError( "Notifications on Battery Level Char ({0}) is not " "possible in BlueZ >= 5.48. Use regular read instead.".format(_uuid) ) raise BleakError( "Characteristic with UUID {0} could not be found!".format(_uuid) ) await self._bus.callRemote( characteristic.path, "StartNotify", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="", body=[], returnSignature="", ).asFuture(self.loop) if _wrap: self._notification_callbacks[ characteristic.path ] = _data_notification_wrapper( callback, self._char_path_to_uuid ) # noqa | E123 error in flake8... else: self._notification_callbacks[ characteristic.path ] = _regular_notification_wrapper( callback, self._char_path_to_uuid ) # noqa | E123 error in flake8... self._subscriptions.append(str(_uuid)) async def stop_notify(self, _uuid: Union[str, uuid.UUID]) -> None: """Deactivate notification/indication on a specified characteristic. Args: _uuid: The characteristic to stop notifying/indicating on. """ characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: raise BleakError("Characteristic {0} was not found!".format(_uuid)) await self._bus.callRemote( characteristic.path, "StopNotify", interface=defs.GATT_CHARACTERISTIC_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="", body=[], returnSignature="", ).asFuture(self.loop) self._notification_callbacks.pop(characteristic.path, None) self._subscriptions.remove(str(_uuid)) # DBUS introspection method for characteristics. async def get_all_for_characteristic(self, _uuid: Union[str, uuid.UUID]) -> dict: """Get all properties for a characteristic. This method should generally not be needed by end user, since it is a DBus specific method. Args: _uuid: The characteristic to get properties for. Returns: (dict) Properties dictionary """ characteristic = self.services.get_characteristic(str(_uuid)) if not characteristic: raise BleakError("Characteristic {0} was not found!".format(_uuid)) out = await self._bus.callRemote( characteristic.path, "GetAll", interface=defs.PROPERTIES_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="s", body=[defs.GATT_CHARACTERISTIC_INTERFACE], returnSignature="a{sv}", ).asFuture(self.loop) return out async def _get_device_properties(self, interface=defs.DEVICE_INTERFACE) -> dict: """Get properties of the connected device. Args: interface: Which DBus interface to get properties on. Defaults to `org.bluez.Device1`. Returns: (dict) The properties. """ return await self._bus.callRemote( self._device_path, "GetAll", interface=defs.PROPERTIES_INTERFACE, destination=defs.BLUEZ_SERVICE, signature="s", body=[interface], returnSignature="a{sv}", ).asFuture(self.loop) # Internal Callbacks def _properties_changed_callback(self, message): """Notification handler. In the BlueZ DBus API, notifications come as PropertiesChanged callbacks on the GATT Characteristic interface that StartNotify has been called on. Args: message (): The PropertiesChanged DBus signal message relaying the new data on the GATT Characteristic. """ logger.debug( "DBUS: path: {}, domain: {}, body: {}".format( message.path, message.body[0], message.body[1] ) ) if message.body[0] == defs.GATT_CHARACTERISTIC_INTERFACE: if message.path in self._notification_callbacks: logger.info( "GATT Char Properties Changed: {0} | {1}".format( message.path, message.body[1:] ) ) self._notification_callbacks[message.path]( message.path, message.body[1] ) elif message.body[0] == defs.DEVICE_INTERFACE: device_path = "/org/bluez/%s/dev_%s" % ( self.device, self.address.replace(":", "_"), ) if message.path.lower() == device_path.lower(): message_body_map = message.body[1] if ( "Connected" in message_body_map and not message_body_map["Connected"] ): logger.debug("Device {} disconnected.".format(self.address)) task = self.loop.create_task(self._cleanup()) if self._disconnected_callback is not None: task.add_done_callback(partial(self._disconnected_callback, self))
async def scanner(loop: asyncio.AbstractEventLoop, outqueue: asyncio.Queue, stopevent: asyncio.Event, device: str = 'hci0', **kwargs): """Perform a continuous Bluetooth LE Scan Args: loop: async event loop outqueue: outgoing queue stopevent: stop event device: bluetooth device """ logger.info(f'>>> scanner:linux device:{device}') q = queue.Queue(QUEUE_SIZE) devices = {} cached_devices = {} rules = list() # ----------------------------------------------------------------------------- def queue_put(msg_path): try: if msg_path in devices: props = devices[msg_path] name, address, _, _ = _device_info(msg_path, props) # logger.debug(f'>>> {name} {path} {address}') if q and address: q.put( BLEDevice(address, name, { "path": msg_path, "props": props }, uuids=props.get("UUIDs", []), manufacturer_data=props.get( "ManufacturerData", {}))) except: logger.exception(f'>>> exception') # ----------------------------------------------------------------------------- def parse_msg(message): if message.member == "InterfacesAdded": logger.debug(f'>>> {message.member} {message.path}:{message.body}') msg_path = message.body[0] try: device_interface = message.body[1].get("org.bluez.Device1", {}) except Exception as e: raise e # store device devices[msg_path] = ({ **devices[msg_path], **device_interface } if msg_path in devices else device_interface) # put BLEDevice object to the queue logger.debug(f'>>> InterfacesAdded body:{msg_path}') queue_put(msg_path) elif message.member == "PropertiesChanged": logger.debug(f'>>> {message.member} {message.path}:{message.body}') msg_path = message.path iface, changed, _ = message.body if iface != DEVICE_INTERFACE: return # store changed info if msg_path not in devices and msg_path in cached_devices: devices[msg_path] = cached_devices[msg_path] devices[msg_path] = ({ **devices[msg_path], **changed } if msg_path in devices else changed) # put BLEDevice object to the queue logger.debug(f'>>> PropertiesChanged body:{msg_path}') queue_put(msg_path) elif message.member == "InterfacesRemoved": logger.debug(f'>>> {message.member} {message.path}:{message.body}') return else: msg_path = message.path logger.warning("{0}, {1} ({2}): {3}".format( message.member, message.interface, message.path, message.body)) # ----------------------------------------------------------------------------- try: logger.info(f'>>> Starting...') # Connect to the txdbus reactor = AsyncioSelectorReactor(loop) bus = await client.connect(reactor, "system").asFuture(loop) # Add signal listeners rules.append(await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesAdded", ).asFuture(loop)) rules.append(await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.ObjectManager", member="InterfacesRemoved", ).asFuture(loop)) rules.append(await bus.addMatch( parse_msg, interface="org.freedesktop.DBus.Properties", member="PropertiesChanged", ).asFuture(loop)) # Find the HCI device to use for scanning and get cached device properties objects = await bus.callRemote( "/", "GetManagedObjects", interface=OBJECT_MANAGER_INTERFACE, destination=BLUEZ_SERVICE, ).asFuture(loop) adapter_path, interface = _filter_on_adapter(objects, device) logger.info(f'>>> device:{device} adapter_path:{adapter_path}') logger.debug(f'>>> interface:{interface}') cached_devices = dict(_filter_on_device(objects)) logger.debug(f">>> cached_devices:{cached_devices}") # Running Discovery loop. await bus.callRemote( adapter_path, "SetDiscoveryFilter", interface="org.bluez.Adapter1", destination="org.bluez", signature="a{sv}", body=[{ "Transport": "le" }], ).asFuture(loop) await bus.callRemote( adapter_path, "StartDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(loop) # Run Communication loop while not stopevent.is_set(): try: l_data = q.get_nowait() if l_data and outqueue: await outqueue.put(l_data) except queue.Empty: try: await asyncio.sleep(0.1) except asyncio.CancelledError: logger.warning(f'>>> CancelledError') break except: logger.exception(f'>>> exception') break try: await bus.callRemote( adapter_path, "StopDiscovery", interface="org.bluez.Adapter1", destination="org.bluez", ).asFuture(loop) except RemoteError: logger.error(f'>>> RemoteError') except: logger.exception(f'>>> exception') # Stop discovery logger.info(f'>>> Disconnecting...') for rule in rules: await bus.delMatch(rule).asFuture(loop) rules.clear() # Disconnect txdbus client try: bus.disconnect() except Exception as l_e: logger.error(f'>>> Attempt to disconnect system bus failed: {l_e}') try: reactor.stop() except ReactorNotRunning as l_e: logger.error(f'>>> Attempt to stop reactor failed: {l_e}') bus = None reactor = None