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 while True: properties = await self._get_device_properties() services_resolved = properties.get("ServicesResolved", False) if services_resolved: break await asyncio.sleep(0.02, loop=self.loop) 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
async def add_new_service(self, uuid: str): """ Add a new GATT service to be hosted by the server Parameters ---------- uuid : str The UUID for the service to add """ await self.setup_task gatt_service: BlueZGattService = await self.app.add_service(uuid) dbus_obj: RemoteDBusObject = await self.bus.getRemoteObject( self.app.destination, gatt_service.path).asFuture(self.loop) dict_obj: Dict = await dbus_obj.callRemote( "GetAll", defs.GATT_SERVICE_INTERFACE, interface=defs.PROPERTIES_INTERFACE).asFuture(self.loop) service: BleakGATTServiceBlueZDBus = BleakGATTServiceBlueZDBus( dict_obj, gatt_service.path) self.services[uuid] = service
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 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