def _parse_msg(self, message: Message): if message.message_type != MessageType.SIGNAL: return logger.debug("received D-Bus signal: {0}.{1} ({2}): {3}".format( message.interface, message.member, message.path, message.body)) if message.member == "InterfacesAdded": path, interfaces = message.body if defs.GATT_SERVICE_INTERFACE in interfaces: obj = unpack_variants(interfaces[defs.GATT_SERVICE_INTERFACE]) # if this assert fails, it means our match rules are probably wrong assert obj["Device"] == self._device_path self.services.add_service(BleakGATTServiceBlueZDBus(obj, path)) if defs.GATT_CHARACTERISTIC_INTERFACE in interfaces: obj = unpack_variants( interfaces[defs.GATT_CHARACTERISTIC_INTERFACE]) service = next(x for x in self.services.services.values() if x.path == obj["Service"]) self.services.add_characteristic( BleakGATTCharacteristicBlueZDBus(obj, path, service.uuid, service.handle)) if defs.GATT_DESCRIPTOR_INTERFACE in interfaces: obj = unpack_variants( interfaces[defs.GATT_DESCRIPTOR_INTERFACE]) handle = extract_service_handle_from_path( obj["Characteristic"]) characteristic = self.services.characteristics[handle] self.services.add_descriptor( BleakGATTDescriptorBlueZDBus(obj, path, characteristic.uuid, handle)) elif message.member == "InterfacesRemoved": path, interfaces = message.body elif message.member == "PropertiesChanged": interface, changed, _ = message.body changed = unpack_variants(changed) if interface == defs.GATT_CHARACTERISTIC_INTERFACE: if message.path in self._notification_callbacks and "Value" in changed: handle = extract_service_handle_from_path(message.path) self._notification_callbacks[message.path]( handle, bytearray(changed["Value"])) elif interface == defs.DEVICE_INTERFACE: self._properties.update(changed) if "ServicesResolved" in changed: if changed["ServicesResolved"]: if self._services_resolved_event: self._services_resolved_event.set() else: self._services_resolved = False if "Connected" in changed and not changed["Connected"]: logger.debug(f"Device disconnected ({self._device_path})") if self._disconnect_monitor_event: self._disconnect_monitor_event.set() self._disconnect_monitor_event = None self._cleanup_all() if self._disconnected_callback is not None: self._disconnected_callback(self) disconnecting_event = self._disconnecting_event if disconnecting_event: disconnecting_event.set()
async def start(self): self._bus = await MessageBus(bus_type=BusType.SYSTEM).connect() self._devices.clear() self._cached_devices.clear() # Add signal listeners self._bus.add_message_handler(self._parse_msg) rules = MatchRules( interface=defs.OBJECT_MANAGER_INTERFACE, member="InterfacesAdded", arg0path=f"{self._adapter_path}/", ) reply = await add_match(self._bus, rules) assert_reply(reply) self._rules.append(rules) rules = MatchRules( interface=defs.OBJECT_MANAGER_INTERFACE, member="InterfacesRemoved", arg0path=f"{self._adapter_path}/", ) reply = await add_match(self._bus, rules) assert_reply(reply) self._rules.append(rules) rules = MatchRules( interface=defs.PROPERTIES_INTERFACE, member="PropertiesChanged", path_namespace=self._adapter_path, ) reply = await add_match(self._bus, rules) assert_reply(reply) self._rules.append(rules) # Find the HCI device to use for scanning and get cached device properties reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path="/", member="GetManagedObjects", interface=defs.OBJECT_MANAGER_INTERFACE, ) ) assert_reply(reply) # get only the device interface self._cached_devices = { path: unpack_variants(interfaces[defs.DEVICE_INTERFACE]) for path, interfaces in reply.body[0].items() if defs.DEVICE_INTERFACE in interfaces } logger.debug(f"cached devices: {self._cached_devices}") # Apply the filters reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=self._adapter_path, interface=defs.ADAPTER_INTERFACE, member="SetDiscoveryFilter", signature="a{sv}", body=[self._filters], ) ) assert_reply(reply) # Start scanning reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path=self._adapter_path, interface=defs.ADAPTER_INTERFACE, member="StartDiscovery", ) ) assert_reply(reply)
async def connect(self, **kwargs) -> bool: """Connect to the specified GATT server. Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. Returns: Boolean representing connection status. Raises: BleakError: If the device is already connected or if the device could not be found. BleakDBusError: If there was a D-Bus error asyncio.TimeoutError: If the connection timed out """ logger.debug( f"Connecting to device @ {self.address} with {self._adapter}") if self.is_connected: raise BleakError("Client is already connected") # A Discover must have been run before connecting to any devices. # Find the desired device before trying to connect. timeout = kwargs.get("timeout", self._timeout) if self._device_path is None: device = await BleakScannerBlueZDBus.find_device_by_address( self.address, timeout=timeout, adapter=self._adapter) if device: self._device_info = device.details.get("props") self._device_path = device.details["path"] else: raise BleakError( "Device with address {0} was not found.".format( self.address)) # Create system bus self._bus = await MessageBus(bus_type=BusType.SYSTEM, negotiate_unix_fd=True).connect() try: # Add signal handlers. These monitor the device D-Bus object and # all of its descendats (services, characteristics, descriptors). # This we always have an up-to-date state for all of these that is # maintained automatically in the background. self._bus.add_message_handler(self._parse_msg) rules = MatchRules( interface=defs.OBJECT_MANAGER_INTERFACE, member="InterfacesAdded", arg0path=f"{self._device_path}/", ) reply = await add_match(self._bus, rules) assert_reply(reply) rules = MatchRules( interface=defs.OBJECT_MANAGER_INTERFACE, member="InterfacesRemoved", arg0path=f"{self._device_path}/", ) reply = await add_match(self._bus, rules) assert_reply(reply) rules = MatchRules( interface=defs.PROPERTIES_INTERFACE, member="PropertiesChanged", path_namespace=self._device_path, ) reply = await add_match(self._bus, rules) assert_reply(reply) # Find the HCI device to use for scanning and get cached device properties reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, path="/", member="GetManagedObjects", interface=defs.OBJECT_MANAGER_INTERFACE, )) assert_reply(reply) interfaces_and_props: Dict[str, Dict[str, Variant]] = reply.body[0] # The device may have been removed from BlueZ since the time we stopped scanning if self._device_path not in interfaces_and_props: # Sometimes devices can be removed from the BlueZ object manager # before we connect to them. In this case we try using the # org.bluez.Adapter1.ConnectDevice method instead. This method # requires that bluetoothd is run with the --experimental flag # and is available since BlueZ 5.49. logger.debug( f"org.bluez.Device1 object not found, trying org.bluez.Adapter1.ConnectDevice ({self._device_path})" ) reply = await asyncio.wait_for( self._bus.call( Message( destination=defs.BLUEZ_SERVICE, interface=defs.ADAPTER_INTERFACE, path=f"/org/bluez/{self._adapter}", member="ConnectDevice", signature="a{sv}", body=[{ "Address": Variant("s", self._device_info["Address"]), "AddressType": Variant("s", self._device_info["AddressType"]), }], )), timeout, ) # FIXME: how to cancel connection if timeout? if (reply.message_type == MessageType.ERROR and reply.error_name == ErrorType.UNKNOWN_METHOD.value): logger.debug( f"org.bluez.Adapter1.ConnectDevice not found ({self._device_path}), try enabling bluetoothd --experimental" ) raise BleakError( "Device with address {0} could not be found. " "Try increasing `timeout` value or moving the device closer." .format(self.address)) assert_reply(reply) else: # required interface self._properties = unpack_variants(interfaces_and_props[ self._device_path][defs.DEVICE_INTERFACE]) # optional interfaces - services and characteristics may not # be populated yet for path, interfaces in interfaces_and_props.items(): if not path.startswith(self._device_path): continue if defs.GATT_SERVICE_INTERFACE in interfaces: obj = unpack_variants( interfaces[defs.GATT_SERVICE_INTERFACE]) self.services.add_service( BleakGATTServiceBlueZDBus(obj, path)) if defs.GATT_CHARACTERISTIC_INTERFACE in interfaces: obj = unpack_variants( interfaces[defs.GATT_CHARACTERISTIC_INTERFACE]) service = interfaces_and_props[obj["Service"]][ defs.GATT_SERVICE_INTERFACE] uuid = service["UUID"].value handle = extract_service_handle_from_path( obj["Service"]) self.services.add_characteristic( BleakGATTCharacteristicBlueZDBus( obj, path, uuid, handle)) if defs.GATT_DESCRIPTOR_INTERFACE in interfaces: obj = unpack_variants( interfaces[defs.GATT_DESCRIPTOR_INTERFACE]) characteristic = interfaces_and_props[ obj["Characteristic"]][ defs.GATT_CHARACTERISTIC_INTERFACE] uuid = characteristic["UUID"].value handle = extract_service_handle_from_path( obj["Characteristic"]) self.services.add_descriptor( BleakGATTDescriptorBlueZDBus( obj, path, uuid, handle)) try: reply = await asyncio.wait_for( self._bus.call( Message( destination=defs.BLUEZ_SERVICE, interface=defs.DEVICE_INTERFACE, path=self._device_path, member="Connect", )), timeout, ) assert_reply(reply) except BaseException: # calling Disconnect cancels any pending connect request try: reply = await self._bus.call( Message( destination=defs.BLUEZ_SERVICE, interface=defs.DEVICE_INTERFACE, path=self._device_path, member="Disconnect", )) try: assert_reply(reply) except BleakDBusError as e: # if the object no longer exists, then we know we # are disconnected for sure, so don't need to log a # warning about it if e.dbus_error != ErrorType.UNKNOWN_OBJECT.value: raise except Exception as e: logger.warning( f"Failed to cancel connection ({self._device_path}): {e}" ) raise if self.is_connected: logger.debug(f"Connection successful ({self._device_path})") else: raise BleakError( f"Connection was not successful! ({self._device_path})") # Create a task that runs until the device is disconnected. self._disconnect_monitor_event = asyncio.Event() asyncio.ensure_future(self._disconnect_monitor()) # Get all services. This means making the actual connection. await self.get_services() return True except BaseException: self._cleanup_all() raise
def _parse_msg(self, message: Message): if message.message_type != MessageType.SIGNAL: return logger.debug( "received D-Bus signal: {0}.{1} ({2}): {3}".format( message.interface, message.member, message.path, message.body ) ) if message.member == "InterfacesAdded": # if a new device is discovered while we are scanning, add it to # the discovered devices list obj_path: str interfaces_and_props: Dict[str, Dict[str, Variant]] obj_path, interfaces_and_props = message.body device_props = unpack_variants( interfaces_and_props.get(defs.DEVICE_INTERFACE, {}) ) if device_props: self._devices[obj_path] = device_props self._invoke_callback(obj_path, message) elif message.member == "InterfacesRemoved": # if a device disappears while we are scanning, remove it from the # discovered devices list obj_path: str interfaces: List[str] obj_path, interfaces = message.body if defs.DEVICE_INTERFACE in interfaces: # Using pop to avoid KeyError if obj_path does not exist self._devices.pop(obj_path, None) elif message.member == "PropertiesChanged": # Property change events basically mean that new advertising data # was received or the RSSI changed. Either way, it lets us know # that the device is active and we can add it to the discovered # devices list. interface: str changed: Dict[str, Variant] invalidated: List[str] interface, changed, invalidated = message.body if interface != defs.DEVICE_INTERFACE: return first_time_seen = False if message.path not in self._devices: if message.path not in self._cached_devices: # This can happen when we start scanning. The "PropertyChanged" # handler is attached before "GetManagedObjects" is called # and so self._cached_devices is not assigned yet. # This is not a problem. We just discard the property value # since "GetManagedObjects" will return a newer value. return first_time_seen = True self._devices[message.path] = self._cached_devices[message.path] changed = unpack_variants(changed) self._devices[message.path].update(changed) # Only do advertising data callback if this is the first time the # device has been seen or if an advertising data property changed. # Otherwise we get a flood of callbacks from RSSI changing. if first_time_seen or not _ADVERTISING_DATA_PROPERTIES.isdisjoint( changed.keys() ): self._invoke_callback(message.path, message)