Example #1
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 #2
0
    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)
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 _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)