Example #1
0
 def __init__(self, obj: dict, object_path: str, service_uuid: str,
              service_handle: int):
     super(BleakGATTCharacteristicBlueZDBus, self).__init__(obj)
     self.__descriptors = []
     self.__path = object_path
     self.__service_uuid = service_uuid
     self.__service_handle = service_handle
     self._handle = extract_service_handle_from_path(object_path)
Example #2
0
    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()
Example #3
0
    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
Example #4
0
 def __init__(self, obj, path):
     super().__init__(obj)
     self.__characteristics = []
     self.__path = path
     self.__handle = extract_service_handle_from_path(path)