Esempio n. 1
0
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)
        discovered = await discover(timeout=timeout,
                                    device=self.device,
                                    loop=self.loop)

        # Issue 150 hints at the device path not being possible to create as
        # is done in the `get_device_object_path` method. Try to get it from
        # BlueZ instead.
        # Otherwise, use the old fallback and hope for the best.
        bluez_devices = list(
            filter(lambda d: d.address.lower() == self.address.lower(),
                   discovered))
        if bluez_devices:
            self._device_path = bluez_devices[0].details["path"]
        else:
            # TODO: Better to always get path from BlueZ backend...
            self._device_path = get_device_object_path(self.device,
                                                       self.address)

        self._reactor = get_reactor(self.loop)

        # Create system bus
        self._bus = await txdbus_connect(
            self._reactor, busAddress="system").asFuture(self.loop)

        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:
            await self._cleanup_all()
            if 'Method "Connect" with signature "" on interface' in str(e):
                raise BleakError(
                    "Device with address {0} could not be found. "
                    "Try increasing `timeout` value or moving the device closer."
                    .format(self.address))
            else:
                raise BleakError(str(e))

        if await self.is_connected():
            logger.debug("Connection successful.")
        else:
            await self._cleanup_all()
            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"):
            await self._cleanup_all()
            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_notifications(self) -> None:
        """
        Remove all pending notifications of the client. This method is used to
        free the DBus matches that have been established.
        """
        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 _cleanup_dbus_resources(self) -> None:
        """
        Free the resources allocated for both the DBus bus and the Twisted
        reactor. Use this method upon final disconnection.
        """
        # 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))
        else:
            # Critical to remove the `self._bus` object here to since it was
            # closed above. If not, calls made to it later could lead to
            # a stuck client.
            self._bus = None

    async def _cleanup_all(self) -> None:
        """
        Free all the allocated resource in DBus and Twisted. Use this method to
        eventually cleanup all otherwise leaked resources.
        """
        await self._cleanup_notifications()
        await self._cleanup_dbus_resources()

    async def disconnect(self) -> bool:
        """Disconnect from the specified GATT server.

        Returns:
            Boolean representing if device is disconnected.

        """
        logger.debug("Disconnecting from BLE device...")
        if self._bus is None:
            # No connection exists. Either one hasn't been created or
            # we have already called disconnect and closed the txdbus
            # connection.
            return True

        # Remove all residual notifications.
        await self._cleanup_notifications()

        # 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))

        is_disconnected = not await self.is_connected()

        await self._cleanup_dbus_resources()

        # Reset all stored services.
        self.services = BleakGATTServiceCollection()

        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,
                    int(_characteristic[0].handle),
                ))

        self._services_resolved = True
        return self.services

    # IO methods

    async def read_gatt_char(self,
                             char_specifier: Union[BleakGATTCharacteristic,
                                                   int, str, uuid.UUID],
                             **kwargs) -> bytearray:
        """Perform read operation on the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from,
                specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.

        Returns:
            (bytearray) The read data.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier

        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 str(char_specifier
                   ) == "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(
                    char_specifier, self._device_path, value))
                return value
            if str(char_specifier
                   ) == "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(
                    char_specifier, self._device_path, value))
                return value

            raise BleakError(
                "Characteristic with UUID {0} could not be found!".format(
                    char_specifier))

        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(
            characteristic.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,
        char_specifier: Union[BleakGATTCharacteristic, int, 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
        https://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:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write
                to, specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            data (bytes or bytearray): The data to send.
            response (bool): If write-with-response operation should be done. Defaults to `False`.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier

        if not characteristic:
            raise BleakError(
                "Characteristic {0} was not found!".format(char_specifier))
        if ("write" not in characteristic.properties
                and "write-without-response" not in characteristic.properties):
            raise BleakError(
                "Characteristic %s does not support write operations!" %
                str(characteristic.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(characteristic.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(
            characteristic.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, char_specifier: Union[BleakGATTCharacteristic,
                                                       int, 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:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to activate
                notifications/indications on a characteristic, specified by either integer handle,
                UUID or directly by the BleakGATTCharacteristic object representing it.
            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)
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier

        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(char_specifier
                   ) == "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(char_specifier))
            raise BleakError(
                "Characteristic with UUID {0} could not be found!".format(
                    char_specifier))
        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(characteristic.handle)

    async def stop_notify(
        self, char_specifier: Union[BleakGATTCharacteristic, int, str,
                                    uuid.UUID]
    ) -> None:
        """Deactivate notification/indication on a specified characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate
                notification/indication on, specified by either integer handle, UUID or
                directly by the BleakGATTCharacteristic object representing it.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} not found!".format(char_specifier))

        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(characteristic.handle)

    # DBUS introspection method for characteristics.

    async def get_all_for_characteristic(
        self, char_specifier: Union[BleakGATTCharacteristic, int, 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:
            char_specifier: The characteristic to get properties for, specified by either
                integer handle, UUID or directly by the BleakGATTCharacteristic
                object representing it.

        Returns:
            (dict) Properties dictionary

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} not found!".format(char_specifier))

        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_all())
                    if self._disconnected_callback is not None:
                        task.add_done_callback(
                            partial(self._disconnected_callback, self))
Esempio n. 2
0
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_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.

    Keyword Args:
        timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.
        disconnected_callback (callable): Callback that will be scheduled in the
            event loop when the client is disconnected. The callable must take one
            argument, which will be this client object.
        adapter (str): Bluetooth adapter to use for discovery.
    """
    def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
        super(BleakClientBlueZDBus, self).__init__(address_or_ble_device,
                                                   **kwargs)
        # kwarg "device" is for backwards compatibility
        self._adapter = kwargs.get("adapter", kwargs.get("device", "hci0"))

        # Backend specific, D-Bus objects and data
        if isinstance(address_or_ble_device, BLEDevice):
            self._device_path = address_or_ble_device.details["path"]
            self._device_info = address_or_ble_device.details.get("props")
        else:
            self._device_path = None
            self._device_info = None

        # D-Bus message bus
        self._bus: Optional[MessageBus] = None
        # D-Bus properties for the device
        self._properties: Dict[str, Any] = {}
        # provides synchronization between get_services() and PropertiesChanged signal
        self._services_resolved_event: Optional[asyncio.Event] = None
        # indicates disconnect request in progress when not None
        self._disconnecting_event: Optional[asyncio.Event] = None
        # used to ensure device gets disconnected if event loop crashes
        self._disconnect_monitor_event: Optional[asyncio.Event] = None

        # used to override mtu_size property
        self._mtu_size: Optional[int] = None

        # BlueZ version features
        self._can_write_without_response = check_bluez_version(5, 46)
        self._write_without_response_workaround_needed = not check_bluez_version(
            5, 51)
        self._hides_battery_characteristic = check_bluez_version(5, 48)
        self._hides_device_name_characteristic = check_bluez_version(5, 48)

    # Connectivity methods

    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

    async def _disconnect_monitor(self) -> None:
        # This task runs until the device is disconnected. If the task is
        # cancelled, it probably means that the event loop crashed so we
        # try to disconnected the device. Otherwise BlueZ will keep the device
        # connected even after Python exits. This will only work if the event
        # loop is called with asyncio.run() or otherwise runs pending tasks
        # after the original event loop stops. This will also cause an exception
        # if a run loop is stopped before the device is disconnected since this
        # task will still be running and asyncio compains if a loop with running
        # tasks is stopped.
        try:
            await self._disconnect_monitor_event.wait()
        except asyncio.CancelledError:
            try:
                # by using send() instead of call(), we ensure that the message
                # gets sent, but we don't wait for a reply, which could take
                # over one second while the device disconnects.
                await self._bus.send(
                    Message(
                        destination=defs.BLUEZ_SERVICE,
                        path=self._device_path,
                        interface=defs.DEVICE_INTERFACE,
                        member="Disconnect",
                    ))
            except Exception:
                pass

    def _cleanup_all(self) -> None:
        """
        Free all the allocated resource in DBus. Use this method to
        eventually cleanup all otherwise leaked resources.
        """
        logger.debug(f"_cleanup_all({self._device_path})")

        if not self._bus:
            logger.debug(f"already disconnected ({self._device_path})")
            return

        # Try to disconnect the System Bus.
        try:
            self._bus.disconnect()
        except Exception as e:
            logger.error(
                f"Attempt to disconnect system bus failed ({self._device_path}): {e}"
            )
        else:
            # Critical to remove the `self._bus` object here to since it was
            # closed above. If not, calls made to it later could lead to
            # a stuck client.
            self._bus = None

            # Reset all stored services.
            self.services = BleakGATTServiceCollection()
            self._services_resolved = False

    async def disconnect(self) -> bool:
        """Disconnect from the specified GATT server.

        Returns:
            Boolean representing if device is disconnected.

        Raises:
            BleakDBusError: If there was a D-Bus error
            asyncio.TimeoutError if the device was not disconnected within 10 seconds
        """
        logger.debug(f"Disconnecting ({self._device_path})")

        if self._bus is None:
            # No connection exists. Either one hasn't been created or
            # we have already called disconnect and closed the D-Bus
            # connection.
            logger.debug(f"already disconnected ({self._device_path})")
            return True

        if self._disconnecting_event:
            # another call to disconnect() is already in progress
            logger.debug(f"already in progress ({self._device_path})")
            await asyncio.wait_for(self._disconnecting_event.wait(),
                                   timeout=10)
        elif self.is_connected:
            self._disconnecting_event = asyncio.Event()
            try:
                # Try to disconnect the actual device/peripheral
                reply = await self._bus.call(
                    Message(
                        destination=defs.BLUEZ_SERVICE,
                        path=self._device_path,
                        interface=defs.DEVICE_INTERFACE,
                        member="Disconnect",
                    ))
                assert_reply(reply)
                await asyncio.wait_for(self._disconnecting_event.wait(),
                                       timeout=10)
            finally:
                self._disconnecting_event = None

        # sanity check to make sure _cleanup_all() was triggered by the
        # "PropertiesChanged" signal handler and that it completed successfully
        assert self._bus is None

        return True

    async def pair(self, *args, **kwargs) -> bool:
        """Pair with the peripheral.

        You can use ConnectDevice method if you already know the MAC address of the device.
        Else you need to StartDiscovery, Trust, Pair and Connect in sequence.

        Returns:
            Boolean regarding success of pairing.

        """
        # See if it is already paired.
        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=self._device_path,
                interface=defs.PROPERTIES_INTERFACE,
                member="Get",
                signature="ss",
                body=[defs.DEVICE_INTERFACE, "Paired"],
            ))
        assert_reply(reply)
        if reply.body[0].value:
            logger.debug(
                f"BLE device @ {self.address} already paired with {self._adapter}"
            )
            return True

        # Set device as trusted.
        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=self._device_path,
                interface=defs.PROPERTIES_INTERFACE,
                member="Set",
                signature="ssv",
                body=[defs.DEVICE_INTERFACE, "Trusted",
                      Variant("b", True)],
            ))
        assert_reply(reply)

        logger.debug("Pairing to BLE device @ {0} with {1}".format(
            self.address, self._adapter))

        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=self._device_path,
                interface=defs.DEVICE_INTERFACE,
                member="Pair",
            ))
        assert_reply(reply)

        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=self._device_path,
                interface=defs.PROPERTIES_INTERFACE,
                member="Get",
                signature="ss",
                body=[defs.DEVICE_INTERFACE, "Paired"],
            ))
        assert_reply(reply)

        return reply.body[0].value

    async def unpair(self) -> bool:
        """Unpair with the peripheral.

        Returns:
            Boolean regarding success of unpairing.

        """
        warnings.warn(
            "Unpairing is seemingly unavailable in the BlueZ DBus API at the moment."
        )
        return False

    @property
    def is_connected(self) -> bool:
        """Check connection status between this client and the server.

        Returns:
            Boolean representing connection status.

        """
        return self._DeprecatedIsConnectedReturn(
            False if self._bus is None else self._properties.
            get("Connected", False))

    async def _acquire_mtu(self) -> None:
        """Acquires the MTU for this device by calling the "AcquireWrite" or
        "AcquireNotify" method of the first characteristic that has such a method.

        This method only needs to be called once, after connecting to the device
        but before accessing the ``mtu_size`` property.

        If a device uses encryption on characteristics, it will need to be bonded
        first before calling this method.
        """
        # This will try to get the "best" characteristic for getting the MTU.
        # We would rather not start notifications if we don't have to.
        try:
            method = "AcquireWrite"
            char = next(c for c in self.services.characteristics.values()
                        if "write-without-response" in c.properties)
        except StopIteration:
            method = "AcquireNotify"
            char = next(c for c in self.services.characteristics.values()
                        if "notify" in c.properties)

        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=char.path,
                interface=defs.GATT_CHARACTERISTIC_INTERFACE,
                member=method,
                signature="a{sv}",
                body=[{}],
            ))
        assert_reply(reply)

        # we aren't actually using the write or notify, we just want the MTU
        os.close(reply.unix_fds[0])
        self._mtu_size = reply.body[1]

    @property
    def mtu_size(self) -> int:
        """Get ATT MTU size for active connection"""
        if self._mtu_size is None:
            warnings.warn(
                "Using default MTU value. Call _assign_mtu() or set _mtu_size first to avoid this warning."
            )
            return 23

        return self._mtu_size

    # GATT services methods

    async def get_services(self, **kwargs) -> BleakGATTServiceCollection:
        """Get all services registered for this GATT server.

        Returns:
           A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree.

        """
        if not self.is_connected:
            raise BleakError("Not connected")

        if self._services_resolved:
            return self.services

        if not self._properties["ServicesResolved"]:
            logger.debug(f"Waiting for ServicesResolved ({self._device_path})")
            self._services_resolved_event = asyncio.Event()
            try:
                await asyncio.wait_for(self._services_resolved_event.wait(), 5)
            finally:
                self._services_resolved_event = None

        self._services_resolved = True
        return self.services

    # IO methods

    async def read_gatt_char(
        self,
        char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str,
                              UUID],
        **kwargs,
    ) -> bytearray:
        """Perform read operation on the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to read from,
                specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristicBlueZDBus object representing it.

        Returns:
            (bytearray) The read data.

        """
        if not self.is_connected:
            raise BleakError("Not connected")

        if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier

        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 str(char_specifier
                   ) == "00002a19-0000-1000-8000-00805f9b34fb" and (
                       self._hides_battery_characteristic):
                reply = await self._bus.call(
                    Message(
                        destination=defs.BLUEZ_SERVICE,
                        path=self._device_path,
                        interface=defs.PROPERTIES_INTERFACE,
                        member="GetAll",
                        signature="s",
                        body=[defs.BATTERY_INTERFACE],
                    ))
                assert_reply(reply)
                # Simulate regular characteristics read to be consistent over all platforms.
                value = bytearray(reply.body[0]["Percentage"].value)
                logger.debug("Read Battery Level {0} | {1}: {2}".format(
                    char_specifier, self._device_path, value))
                return value
            if str(char_specifier
                   ) == "00002a00-0000-1000-8000-00805f9b34fb" and (
                       self._hides_device_name_characteristic):
                # Simulate regular characteristics read to be consistent over all platforms.
                value = bytearray(self._properties["Name"].encode("ascii"))
                logger.debug("Read Device Name {0} | {1}: {2}".format(
                    char_specifier, self._device_path, value))
                return value

            raise BleakError(
                "Characteristic with UUID {0} could not be found!".format(
                    char_specifier))

        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=characteristic.path,
                interface=defs.GATT_CHARACTERISTIC_INTERFACE,
                member="ReadValue",
                signature="a{sv}",
                body=[{}],
            ))
        assert_reply(reply)
        value = bytearray(reply.body[0])

        logger.debug("Read Characteristic {0} | {1}: {2}".format(
            characteristic.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.

        """
        if not self.is_connected:
            raise BleakError("Not connected")

        descriptor = self.services.get_descriptor(handle)
        if not descriptor:
            raise BleakError(
                "Descriptor with handle {0} was not found!".format(handle))

        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=descriptor.path,
                interface=defs.GATT_DESCRIPTOR_INTERFACE,
                member="ReadValue",
                signature="a{sv}",
                body=[{}],
            ))
        assert_reply(reply)
        value = bytearray(reply.body[0])

        logger.debug("Read Descriptor {0} | {1}: {2}".format(
            handle, descriptor.path, value))
        return value

    async def write_gatt_char(
        self,
        char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str,
                              UUID],
        data: Union[bytes, bytearray, memoryview],
        response: bool = False,
    ) -> None:
        """Perform a write operation on the specified GATT characteristic.

        .. note::

            The version check below is for the "type" option to the
            "Characteristic.WriteValue" method that was added to `Bluez in 5.51
            <https://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:
            char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to write
                to, specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristicBlueZDBus object representing it.
            data (bytes or bytearray): The data to send.
            response (bool): If write-with-response operation should be done. Defaults to `False`.

        """
        if not self.is_connected:
            raise BleakError("Not connected")

        if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier

        if not characteristic:
            raise BleakError(
                "Characteristic {0} was not found!".format(char_specifier))
        if ("write" not in characteristic.properties
                and "write-without-response" not in characteristic.properties):
            raise BleakError(
                "Characteristic %s does not support write operations!" %
                str(characteristic.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(characteristic.uuid))

        # See docstring for details about this handling.
        if not response and not self._can_write_without_response:
            raise BleakError(
                "Write without response requires at least BlueZ 5.46")
        if response or not self._write_without_response_workaround_needed:
            # TODO: Add OnValueUpdated handler for response=True?
            reply = await self._bus.call(
                Message(
                    destination=defs.BLUEZ_SERVICE,
                    path=characteristic.path,
                    interface=defs.GATT_CHARACTERISTIC_INTERFACE,
                    member="WriteValue",
                    signature="aya{sv}",
                    body=[
                        bytes(data),
                        {
                            "type":
                            Variant("s", "request" if response else "command")
                        },
                    ],
                ))
            assert_reply(reply)
        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.
            reply = await self._bus.call(
                Message(
                    destination=defs.BLUEZ_SERVICE,
                    path=characteristic.path,
                    interface=defs.GATT_CHARACTERISTIC_INTERFACE,
                    member="AcquireWrite",
                    signature="a{sv}",
                    body=[{}],
                ))
            assert_reply(reply)
            fd = reply.unix_fds[0]
            try:
                os.write(fd, data)
            finally:
                os.close(fd)

        logger.debug("Write Characteristic {0} | {1}: {2}".format(
            characteristic.uuid, characteristic.path, data))

    async def write_gatt_descriptor(
            self, handle: int, data: Union[bytes, bytearray,
                                           memoryview]) -> 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.

        """
        if not self.is_connected:
            raise BleakError("Not connected")

        descriptor = self.services.get_descriptor(handle)
        if not descriptor:
            raise BleakError(
                "Descriptor with handle {0} was not found!".format(handle))

        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=descriptor.path,
                interface=defs.GATT_DESCRIPTOR_INTERFACE,
                member="WriteValue",
                signature="aya{sv}",
                body=[bytes(data), {
                    "type": Variant("s", "command")
                }],
            ))
        assert_reply(reply)

        logger.debug("Write Descriptor {0} | {1}: {2}".format(
            handle, descriptor.path, data))

    async def start_notify(
        self,
        char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str,
                              UUID],
        callback: Callable[[int, bytearray], None],
        **kwargs,
    ) -> None:
        """Activate notifications/indications on a characteristic.

        Callbacks must accept two inputs. The first will be a integer handle of the characteristic generating the
        data and the second will be a ``bytearray`` containing the data sent from the connected server.

        .. code-block:: python

            def callback(sender: int, data: bytearray):
                print(f"{sender}: {data}")
            client.start_notify(char_uuid, callback)

        Args:
            char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to activate
                notifications/indications on a characteristic, specified by either integer handle,
                UUID or directly by the BleakGATTCharacteristicBlueZDBus object representing it.
            callback (function): The function to be called on notification.
        """
        if not self.is_connected:
            raise BleakError("Not connected")

        if inspect.iscoroutinefunction(callback):

            def bleak_callback(s, d):
                asyncio.ensure_future(callback(s, d))

        else:
            bleak_callback = callback

        if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier

        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(char_specifier
                   ) == "00002a19-0000-1000-8000-00805f9b34fb" and (
                       self._hides_battery_characteristic):
                raise BleakError(
                    "Notifications on Battery Level Char ({0}) is not "
                    "possible in BlueZ >= 5.48. Use regular read instead.".
                    format(char_specifier))
            raise BleakError(
                "Characteristic with UUID {0} could not be found!".format(
                    char_specifier))

        self._notification_callbacks[characteristic.path] = bleak_callback

        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=characteristic.path,
                interface=defs.GATT_CHARACTERISTIC_INTERFACE,
                member="StartNotify",
            ))
        assert_reply(reply)

    async def stop_notify(
        self,
        char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str,
                              UUID],
    ) -> None:
        """Deactivate notification/indication on a specified characteristic.

        Args:
            char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to deactivate
                notification/indication on, specified by either integer handle, UUID or
                directly by the BleakGATTCharacteristicBlueZDBus object representing it.

        """
        if not self.is_connected:
            raise BleakError("Not connected")

        if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} not found!".format(char_specifier))

        reply = await self._bus.call(
            Message(
                destination=defs.BLUEZ_SERVICE,
                path=characteristic.path,
                interface=defs.GATT_CHARACTERISTIC_INTERFACE,
                member="StopNotify",
            ))
        assert_reply(reply)

        self._notification_callbacks.pop(characteristic.path, None)

    # Internal Callbacks

    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()
Esempio n. 3
0
class BleakClientDotNet(BaseBleakClient):
    """The native Windows Bleak Client.

    Implemented using `pythonnet <https://pythonnet.github.io/>`_, a package that provides an integration to the .NET
    Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel.

    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: str, loop: AbstractEventLoop = None, **kwargs):
        super(BleakClientDotNet, self).__init__(address, loop, **kwargs)

        # Backend specific. Python.NET objects.
        self._device_info = None
        self._requester = None
        self._bridge = Bridge()

        self._address_type = (
            kwargs["address_type"] if "address_type" in kwargs
            and kwargs["address_type"] in ("public", "random") else None)

    def __str__(self):
        return "BleakClientDotNet ({0})".format(self.address)

    # Connectivity methods

    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.

        """
        # Try to find the desired device.
        timeout = kwargs.get("timeout", self._timeout)
        devices = await discover(timeout=timeout, loop=self.loop)
        sought_device = list(
            filter(lambda x: x.address.upper() == self.address.upper(),
                   devices))

        if len(sought_device):
            self._device_info = sought_device[0].details
        else:
            raise BleakError("Device with address {0} was "
                             "not found.".format(self.address))

        logger.debug("Connecting to BLE device @ {0}".format(self.address))

        args = [UInt64(self._device_info.BluetoothAddress)]
        if self._address_type is not None:
            args.append(BluetoothAddressType.Public if self._address_type ==
                        "public" else BluetoothAddressType.Random)
        self._requester = await wrap_IAsyncOperation(
            IAsyncOperation[BluetoothLEDevice](
                BluetoothLEDevice.FromBluetoothAddressAsync(*args)),
            return_type=BluetoothLEDevice,
            loop=self.loop,
        )

        def _ConnectionStatusChanged_Handler(sender, args):
            logger.debug("_ConnectionStatusChanged_Handler: " +
                         args.ToString())

        self._requester.ConnectionStatusChanged += _ConnectionStatusChanged_Handler

        # Obtain services, which also leads to connection being established.
        services = await self.get_services()
        connected = False
        if self._services_resolved:
            # If services has been resolved, then we assume that we are connected. This is due to
            # some issues with getting `is_connected` to give correct response here.
            connected = True
        else:
            for _ in range(5):
                await asyncio.sleep(0.2, loop=self.loop)
                connected = await self.is_connected()
                if connected:
                    break

        if connected:
            logger.debug("Connection successful.")
        else:
            raise BleakError("Connection to {0} was not successful!".format(
                self.address))

        return connected

    async def disconnect(self) -> bool:
        """Disconnect from the specified GATT server.

        Returns:
            Boolean representing connection status.

        """
        logger.debug("Disconnecting from BLE device...")
        # Remove notifications
        # TODO: Make sure all notifications are removed prior to Dispose.
        # Dispose all components that we have requested and created.
        for service in self.services:
            service.obj.Dispose()
        self.services = BleakGATTServiceCollection()
        self._requester.Dispose()
        self._requester = None

        return not await self.is_connected()

    async def is_connected(self) -> bool:
        """Check connection status between this client and the server.

        Returns:
            Boolean representing connection status.

        """
        if self._requester:
            return (self._requester.ConnectionStatus ==
                    BluetoothConnectionStatus.Connected)
        else:
            return False

    def set_disconnected_callback(self, callback: Callable[[BaseBleakClient],
                                                           None],
                                  **kwargs) -> None:
        """Set the disconnected callback.

        N.B. This is not implemented in the .NET backend yet.

        Args:
            callback: callback to be called on disconnection.

        """
        raise NotImplementedError(
            "This is not implemented in the .NET backend yet")

    # 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.

        """
        # Return the Service Collection.
        if self._services_resolved:
            return self.services
        else:
            logger.debug("Get Services...")
            services_result = await wrap_IAsyncOperation(
                IAsyncOperation[GattDeviceServicesResult](
                    self._requester.GetGattServicesAsync()),
                return_type=GattDeviceServicesResult,
                loop=self.loop,
            )

            if services_result.Status != GattCommunicationStatus.Success:
                if services_result.Status == GattCommunicationStatus.ProtocolError:
                    raise BleakDotNetTaskError(
                        "Could not get GATT services: {0} (Error: 0x{1:02X})".
                        format(
                            _communication_statues.get(services_result.Status,
                                                       ""),
                            services_result.ProtocolError,
                        ))
                else:
                    raise BleakDotNetTaskError(
                        "Could not get GATT services: {0}".format(
                            _communication_statues.get(services_result.Status,
                                                       "")))

            # TODO: Check if fetching yeilds failures...
            for service in services_result.Services:
                characteristics_result = await wrap_IAsyncOperation(
                    IAsyncOperation[GattCharacteristicsResult](
                        service.GetCharacteristicsAsync()),
                    return_type=GattCharacteristicsResult,
                    loop=self.loop,
                )
                self.services.add_service(BleakGATTServiceDotNet(service))
                if characteristics_result.Status != GattCommunicationStatus.Success:
                    if (characteristics_result.Status ==
                            GattCommunicationStatus.ProtocolError):
                        raise BleakDotNetTaskError(
                            "Could not get GATT characteristics for {0}: {1} (Error: 0x{2:02X})"
                            .format(
                                service,
                                _communication_statues.get(
                                    characteristics_result.Status, ""),
                                characteristics_result.ProtocolError,
                            ))
                    else:
                        raise BleakDotNetTaskError(
                            "Could not get GATT characteristics for {0}: {1}".
                            format(
                                service,
                                _communication_statues.get(
                                    characteristics_result.Status, ""),
                            ))
                for characteristic in characteristics_result.Characteristics:
                    descriptors_result = await wrap_IAsyncOperation(
                        IAsyncOperation[GattDescriptorsResult](
                            characteristic.GetDescriptorsAsync()),
                        return_type=GattDescriptorsResult,
                        loop=self.loop,
                    )
                    self.services.add_characteristic(
                        BleakGATTCharacteristicDotNet(characteristic))
                    if descriptors_result.Status != GattCommunicationStatus.Success:
                        if (characteristics_result.Status ==
                                GattCommunicationStatus.ProtocolError):
                            raise BleakDotNetTaskError(
                                "Could not get GATT descriptors for {0}: {1} (Error: 0x{2:02X})"
                                .format(
                                    service,
                                    _communication_statues.get(
                                        descriptors_result.Status, ""),
                                    descriptors_result.ProtocolError,
                                ))
                        else:
                            raise BleakDotNetTaskError(
                                "Could not get GATT descriptors for {0}: {1}".
                                format(
                                    characteristic,
                                    _communication_statues.get(
                                        descriptors_result.Status, ""),
                                ))
                    for descriptor in list(descriptors_result.Descriptors):
                        self.services.add_descriptor(
                            BleakGATTDescriptorDotNet(
                                descriptor,
                                characteristic.Uuid.ToString(),
                                int(characteristic.AttributeHandle),
                            ))

            self._services_resolved = True
            return self.services

    # I/O methods

    async def read_gatt_char(self,
                             char_specifier: Union[BleakGATTCharacteristic,
                                                   int, str, uuid.UUID],
                             use_cached=False,
                             **kwargs) -> bytearray:
        """Perform read operation on the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from,
                specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            use_cached (bool): `False` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        Returns:
            (bytearray) The read data.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {0} was not found!".format(char_specifier))

        read_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattReadResult](characteristic.obj.ReadValueAsync(
                BluetoothCacheMode.
                Cached if use_cached else BluetoothCacheMode.Uncached)),
            return_type=GattReadResult,
            loop=self.loop,
        )
        if read_result.Status == GattCommunicationStatus.Success:
            reader = DataReader.FromBuffer(IBuffer(read_result.Value))
            output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength)
            reader.ReadBytes(output)
            value = bytearray(output)
            logger.debug("Read Characteristic {0} : {1}".format(
                characteristic.uuid, value))
        else:
            if read_result.Status == GattCommunicationStatus.ProtocolError:
                raise BleakDotNetTaskError(
                    "Could not get GATT characteristics for {0}: {1} (Error: 0x{2:02X})"
                    .format(
                        characteristic.uuid,
                        _communication_statues.get(read_result.Status, ""),
                        read_result.ProtocolError,
                    ))
            else:
                raise BleakError(
                    "Could not read characteristic value for {0}: {1}".format(
                        characteristic.uuid,
                        _communication_statues.get(read_result.Status, ""),
                    ))
        return value

    async def read_gatt_descriptor(self,
                                   handle: int,
                                   use_cached=False,
                                   **kwargs) -> bytearray:
        """Perform read operation on the specified GATT descriptor.

        Args:
            handle (int): The handle of the descriptor to read from.
            use_cached (bool): `False` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        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))

        read_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattReadResult](descriptor.obj.ReadValueAsync(
                BluetoothCacheMode.
                Cached if use_cached else BluetoothCacheMode.Uncached)),
            return_type=GattReadResult,
            loop=self.loop,
        )
        if read_result.Status == GattCommunicationStatus.Success:
            reader = DataReader.FromBuffer(IBuffer(read_result.Value))
            output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength)
            reader.ReadBytes(output)
            value = bytearray(output)
            logger.debug("Read Descriptor {0} : {1}".format(handle, value))
        else:
            if read_result.Status == GattCommunicationStatus.ProtocolError:
                raise BleakDotNetTaskError(
                    "Could not get GATT characteristics for {0}: {1} (Error: 0x{2:02X})"
                    .format(
                        descriptor.uuid,
                        _communication_statues.get(read_result.Status, ""),
                        read_result.ProtocolError,
                    ))
            else:
                raise BleakError(
                    "Could not read Descriptor value for {0}: {1}".format(
                        descriptor.uuid,
                        _communication_statues.get(read_result.Status, ""),
                    ))

        return value

    async def write_gatt_char(
        self,
        char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID],
        data: bytearray,
        response: bool = False,
    ) -> None:
        """Perform a write operation of the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write
                to, specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            data (bytes or bytearray): The data to send.
            response (bool): If write-with-response operation should be done. Defaults to `False`.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} was not found!".format(char_specifier))

        writer = DataWriter()
        writer.WriteBytes(Array[Byte](data))
        response = (GattWriteOption.WriteWithResponse
                    if response else GattWriteOption.WriteWithoutResponse)
        write_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattWriteResult](
                characteristic.obj.WriteValueWithResultAsync(
                    writer.DetachBuffer(), response)),
            return_type=GattWriteResult,
            loop=self.loop,
        )
        if write_result.Status == GattCommunicationStatus.Success:
            logger.debug("Write Characteristic {0} : {1}".format(
                characteristic.uuid, data))
        else:
            if write_result.Status == GattCommunicationStatus.ProtocolError:
                raise BleakError(
                    "Could not write value {0} to characteristic {1}: {2} (Error: 0x{3:02X})"
                    .format(
                        data,
                        characteristic.uuid,
                        _communication_statues.get(write_result.Status, ""),
                        write_result.ProtocolError,
                    ))
            else:
                raise BleakError(
                    "Could not write value {0} to characteristic {1}: {2}".
                    format(
                        data,
                        characteristic.uuid,
                        _communication_statues.get(write_result.Status, ""),
                    ))

    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))

        writer = DataWriter()
        writer.WriteBytes(Array[Byte](data))
        write_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattWriteResult](descriptor.obj.WriteValueAsync(
                writer.DetachBuffer())),
            return_type=GattWriteResult,
            loop=self.loop,
        )
        if write_result.Status == GattCommunicationStatus.Success:
            logger.debug("Write Descriptor {0} : {1}".format(handle, data))
        else:
            if write_result.Status == GattCommunicationStatus.ProtocolError:
                raise BleakError(
                    "Could not write value {0} to characteristic {1}: {2} (Error: 0x{3:02X})"
                    .format(
                        data,
                        descriptor.uuid,
                        _communication_statues.get(write_result.Status, ""),
                        write_result.ProtocolError,
                    ))
            else:
                raise BleakError(
                    "Could not write value {0} to descriptor {1}: {2}".format(
                        data,
                        descriptor.uuid,
                        _communication_statues.get(write_result.Status, ""),
                    ))

    async def start_notify(self, char_specifier: Union[BleakGATTCharacteristic,
                                                       int, 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:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to activate
                notifications/indications on a characteristic, specified by either integer handle,
                UUID or directly by the BleakGATTCharacteristic object representing it.
            callback (function): The function to be called on notification.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {0} not found!".format(char_specifier))

        if self._notification_callbacks.get(characteristic.handle):
            await self.stop_notify(characteristic)

        status = await self._start_notify(characteristic, callback)

        if status != GattCommunicationStatus.Success:
            # TODO: Find out how to get the ProtocolError code that describes a
            #  potential GattCommunicationStatus.ProtocolError result.
            raise BleakError("Could not start notify on {0}: {1}".format(
                characteristic.uuid, _communication_statues.get(status, "")))

    async def _start_notify(
        self,
        characteristic: BleakGATTCharacteristic,
        callback: Callable[[str, Any], Any],
    ):
        """Internal method performing call to BleakUWPBridge method.

        Args:
            characteristic: The BleakGATTCharacteristic to start notification on.
            callback: The function to be called on notification.

        Returns:
            (int) The GattCommunicationStatus of the operation.

        """
        characteristic_obj = characteristic.obj
        if (characteristic_obj.CharacteristicProperties
                & GattCharacteristicProperties.Indicate):
            cccd = GattClientCharacteristicConfigurationDescriptorValue.Indicate
        elif (characteristic_obj.CharacteristicProperties
              & GattCharacteristicProperties.Notify):
            cccd = GattClientCharacteristicConfigurationDescriptorValue.Notify
        else:
            cccd = getattr(
                GattClientCharacteristicConfigurationDescriptorValue, "None")

        try:
            # TODO: Enable adding multiple handlers!
            self._notification_callbacks[
                characteristic.handle] = TypedEventHandler[
                    GattCharacteristic, GattValueChangedEventArgs](
                        _notification_wrapper(self.loop, callback))
            self._bridge.AddValueChangedCallback(
                characteristic_obj,
                self._notification_callbacks[characteristic.handle])
        except Exception as e:
            logger.debug("Start Notify problem: {0}".format(e))
            if characteristic_obj.Uuid.ToString(
            ) in self._notification_callbacks:
                callback = self._notification_callbacks.pop(
                    characteristic.handle)
                self._bridge.RemoveValueChangedCallback(
                    characteristic_obj, callback)

            return GattCommunicationStatus.AccessDenied

        status = await wrap_IAsyncOperation(
            IAsyncOperation[GattCommunicationStatus](
                characteristic_obj.
                WriteClientCharacteristicConfigurationDescriptorAsync(cccd)),
            return_type=GattCommunicationStatus,
            loop=self.loop,
        )

        if status != GattCommunicationStatus.Success:
            # This usually happens when a device reports that it support indicate,
            # but it actually doesn't.
            if characteristic.handle in self._notification_callbacks:
                callback = self._notification_callbacks.pop(
                    characteristic.handle)
                self._bridge.RemoveValueChangedCallback(
                    characteristic_obj, callback)

            return GattCommunicationStatus.AccessDenied
        return status

    async def stop_notify(
        self, char_specifier: Union[BleakGATTCharacteristic, int, str,
                                    uuid.UUID]
    ) -> None:
        """Deactivate notification/indication on a specified characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate
                notification/indication on, specified by either integer handle, UUID or
                directly by the BleakGATTCharacteristic object representing it.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} not found!".format(char_specifier))

        status = await wrap_IAsyncOperation(
            IAsyncOperation[GattCommunicationStatus](
                characteristic.obj.
                WriteClientCharacteristicConfigurationDescriptorAsync(
                    getattr(
                        GattClientCharacteristicConfigurationDescriptorValue,
                        "None"))),
            return_type=GattCommunicationStatus,
            loop=self.loop,
        )

        if status != GattCommunicationStatus.Success:
            raise BleakError("Could not stop notify on {0}: {1}".format(
                characteristic.uuid, _communication_statues.get(status, "")))
        else:
            callback = self._notification_callbacks.pop(characteristic.handle)
            self._bridge.RemoveValueChangedCallback(characteristic.obj,
                                                    callback)
Esempio n. 4
0
class BleakClientDotNet(BaseBleakClient):
    """The native Windows Bleak Client.

    Implemented using `pythonnet <https://pythonnet.github.io/>`_, a package that provides an integration to the .NET
    Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel.

    Args:
        address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.

    Keyword Args:
            timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.

    """
    def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
        super(BleakClientDotNet, self).__init__(address_or_ble_device,
                                                **kwargs)

        # Backend specific. Python.NET objects.
        if isinstance(address_or_ble_device, BLEDevice):
            self._device_info = address_or_ble_device.details.BluetoothAddress
        else:
            self._device_info = None
        self._requester = None
        self._connect_events: list[asyncio.Event] = []
        self._disconnect_events: list[asyncio.Event] = []
        self._connection_status_changed_token: EventRegistrationToken = None
        self._session: GattSession = None

        self._address_type = (
            kwargs["address_type"] if "address_type" in kwargs
            and kwargs["address_type"] in ("public", "random") else None)

    def __str__(self):
        return "BleakClientDotNet ({0})".format(self.address)

    # Connectivity methods

    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.

        """
        # Try to find the desired device.
        if self._device_info is None:
            timeout = kwargs.get("timeout", self._timeout)
            device = await BleakScannerDotNet.find_device_by_address(
                self.address, timeout=timeout)

            if device:
                self._device_info = device.details.BluetoothAddress
            else:
                raise BleakError(
                    "Device with address {0} was not found.".format(
                        self.address))

        logger.debug("Connecting to BLE device @ {0}".format(self.address))

        args = [UInt64(self._device_info)]
        if self._address_type is not None:
            args.append(BluetoothAddressType.Public if self._address_type ==
                        "public" else BluetoothAddressType.Random)
        self._requester = await wrap_IAsyncOperation(
            IAsyncOperation[BluetoothLEDevice](
                BluetoothLEDevice.FromBluetoothAddressAsync(*args)),
            return_type=BluetoothLEDevice,
        )

        # Called on disconnect event or on failure to connect.
        def handle_disconnect():
            if self._connection_status_changed_token:
                self._requester.remove_ConnectionStatusChanged(
                    self._connection_status_changed_token)
                self._connection_status_changed_token = None

            if self._requester:
                self._requester.Dispose()
                self._requester = None

            if self._session:
                self._session.Dispose()
                self._session = None

        def handle_connection_status_changed(
            connection_status: BluetoothConnectionStatus, ):
            if connection_status == BluetoothConnectionStatus.Connected:
                for e in self._connect_events:
                    e.set()

            elif connection_status == BluetoothConnectionStatus.Disconnected:
                if self._disconnected_callback:
                    self._disconnected_callback(self)

                for e in self._disconnect_events:
                    e.set()

                handle_disconnect()

        loop = asyncio.get_event_loop()

        def _ConnectionStatusChanged_Handler(sender, args):
            logger.debug("_ConnectionStatusChanged_Handler: %d",
                         sender.ConnectionStatus)
            loop.call_soon_threadsafe(handle_connection_status_changed,
                                      sender.ConnectionStatus)

        self._connection_status_changed_token = (
            self._requester.add_ConnectionStatusChanged(
                TypedEventHandler[BluetoothLEDevice,
                                  Object](_ConnectionStatusChanged_Handler)))

        # Start a GATT Session to connect
        event = asyncio.Event()
        self._connect_events.append(event)
        try:
            self._session = await wrap_IAsyncOperation(
                IAsyncOperation[GattSession](GattSession.FromDeviceIdAsync(
                    self._requester.BluetoothDeviceId)),
                return_type=GattSession,
            )
            # This keeps the device connected until we dispose the session or
            # until we set MaintainConnection = False.
            self._session.MaintainConnection = True
            await asyncio.wait_for(event.wait(), timeout=10)
        except BaseException:
            handle_disconnect()
            raise
        finally:
            self._connect_events.remove(event)

        await self.get_services()

        return True

    async def disconnect(self) -> bool:
        """Disconnect from the specified GATT server.

        Returns:
            Boolean representing if device is disconnected.

        Raises:
            asyncio.TimeoutError: If device did not disconnect with 10 seconds.

        """
        logger.debug("Disconnecting from BLE device...")
        # Remove notifications.
        for characteristic in self.services.characteristics.values():
            token = self._notification_callbacks.pop(characteristic.handle,
                                                     None)
            if token:
                characteristic.obj.remove_ValueChanged(token)

        # Dispose all service components that we have requested and created.
        for service in self.services:
            service.obj.Dispose()
        self.services = BleakGATTServiceCollection()
        self._services_resolved = False

        # Without this, disposing the BluetoothLEDevice won't disconnect it
        if self._session:
            self._session.Dispose()

        # Dispose of the BluetoothLEDevice and see that the connection
        # status is now Disconnected.
        if self._requester:
            event = asyncio.Event()
            self._disconnect_events.append(event)
            try:
                self._requester.Dispose()
                await asyncio.wait_for(event.wait(), timeout=10)
            finally:
                self._disconnect_events.remove(event)

        return True

    async def is_connected(self) -> bool:
        """Check connection status between this client and the server.

        Returns:
            Boolean representing connection status.

        """
        if self._requester:
            return (self._requester.ConnectionStatus ==
                    BluetoothConnectionStatus.Connected)
        else:
            return False

    async def pair(self, protection_level=None, **kwargs) -> bool:
        """Attempts to pair with the device.

        Keyword Args:
            protection_level:
                    DevicePairingProtectionLevel
                        1: None - Pair the device using no levels of protection.
                        2: Encryption - Pair the device using encryption.
                        3: EncryptionAndAuthentication - Pair the device using encryption and authentication.

        Returns:
            Boolean regarding success of pairing.

        """
        if (self._requester.DeviceInformation.Pairing.CanPair
                and not self._requester.DeviceInformation.Pairing.IsPaired):

            # Currently only supporting Just Works solutions...
            ceremony = DevicePairingKinds.ConfirmOnly
            custom_pairing = self._requester.DeviceInformation.Pairing.Custom

            def handler(sender, args):
                args.Accept()

            custom_pairing.PairingRequested += handler

            if protection_level:
                raise NotImplementedError(
                    "Cannot set minimally required protection level yet...")
            else:
                pairing_result = await wrap_IAsyncOperation(
                    IAsyncOperation[DevicePairingResult](
                        custom_pairing.PairAsync.Overloads[DevicePairingKinds](
                            ceremony)),
                    return_type=DevicePairingResult,
                )

            try:
                custom_pairing.PairingRequested -= handler
            except Exception as e:
                # TODO: Find a way to remove WinRT events...
                pass
            finally:
                del handler

            if pairing_result.Status not in (
                    DevicePairingResultStatus.Paired,
                    DevicePairingResultStatus.AlreadyPaired,
            ):
                raise BleakError("Could not pair with device: {0}: {1}".format(
                    pairing_result.Status,
                    _pairing_statuses.get(pairing_result.Status),
                ))
            else:
                logger.info(
                    "Paired to device with protection level {0}.".format(
                        pairing_result.ProtectionLevelUsed))

        return self._requester.DeviceInformation.Pairing.IsPaired

    async def unpair(self) -> bool:
        """Attempts to unpair from the device.

        Returns:
            Boolean on whether the unparing was successful.

        """

        if self._requester.DeviceInformation.Pairing.IsPaired:
            unpairing_result = await wrap_IAsyncOperation(
                IAsyncOperation[DeviceUnpairingResult](
                    self._requester.DeviceInformation.Pairing.UnpairAsync()),
                return_type=DeviceUnpairingResult,
            )

            if unpairing_result.Status not in (
                    DevicePairingResultStatus.Paired,
                    DevicePairingResultStatus.AlreadyPaired,
            ):
                raise BleakError(
                    "Could not unpair with device: {0}: {1}".format(
                        unpairing_result.Status,
                        _unpairing_statuses.get(unpairing_result.Status),
                    ))
            else:
                logger.info("Unpaired with device.")

        return not self._requester.DeviceInformation.Pairing.IsPaired

    # 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.

        """
        # Return the Service Collection.
        if self._services_resolved:
            return self.services
        else:
            logger.debug("Get Services...")
            services_result = await wrap_IAsyncOperation(
                IAsyncOperation[GattDeviceServicesResult](
                    self._requester.GetGattServicesAsync()),
                return_type=GattDeviceServicesResult,
            )

            if services_result.Status != GattCommunicationStatus.Success:
                if services_result.Status == GattCommunicationStatus.ProtocolError:
                    raise BleakDotNetTaskError(
                        "Could not get GATT services: {0} (Error: 0x{1:02X}: {2})"
                        .format(
                            _communication_statues.get(services_result.Status,
                                                       ""),
                            services_result.ProtocolError,
                            CONTROLLER_ERROR_CODES.get(
                                services_result.ProtocolError, "Unknown"),
                        ))
                else:
                    raise BleakDotNetTaskError(
                        "Could not get GATT services: {0}".format(
                            _communication_statues.get(services_result.Status,
                                                       "")))

            for service in services_result.Services:
                characteristics_result = await wrap_IAsyncOperation(
                    IAsyncOperation[GattCharacteristicsResult](
                        service.GetCharacteristicsAsync()),
                    return_type=GattCharacteristicsResult,
                )
                self.services.add_service(BleakGATTServiceDotNet(service))
                if characteristics_result.Status != GattCommunicationStatus.Success:
                    if (characteristics_result.Status ==
                            GattCommunicationStatus.ProtocolError):
                        raise BleakDotNetTaskError(
                            "Could not get GATT characteristics for {0}: {1} (Error: 0x{2:02X}: {3})"
                            .format(
                                service,
                                _communication_statues.get(
                                    characteristics_result.Status, ""),
                                characteristics_result.ProtocolError,
                                CONTROLLER_ERROR_CODES.get(
                                    characteristics_result.ProtocolError,
                                    "Unknown"),
                            ))
                    else:
                        raise BleakDotNetTaskError(
                            "Could not get GATT characteristics for {0}: {1}".
                            format(
                                service,
                                _communication_statues.get(
                                    characteristics_result.Status, ""),
                            ))
                for characteristic in characteristics_result.Characteristics:
                    descriptors_result = await wrap_IAsyncOperation(
                        IAsyncOperation[GattDescriptorsResult](
                            characteristic.GetDescriptorsAsync()),
                        return_type=GattDescriptorsResult,
                    )
                    self.services.add_characteristic(
                        BleakGATTCharacteristicDotNet(characteristic))
                    if descriptors_result.Status != GattCommunicationStatus.Success:
                        if (characteristics_result.Status ==
                                GattCommunicationStatus.ProtocolError):
                            raise BleakDotNetTaskError(
                                "Could not get GATT descriptors for {0}: {1} (Error: 0x{2:02X}: {3})"
                                .format(
                                    service,
                                    _communication_statues.get(
                                        descriptors_result.Status, ""),
                                    descriptors_result.ProtocolError,
                                    CONTROLLER_ERROR_CODES.get(
                                        descriptors_result.ProtocolError,
                                        "Unknown"),
                                ))
                        else:
                            raise BleakDotNetTaskError(
                                "Could not get GATT descriptors for {0}: {1}".
                                format(
                                    characteristic,
                                    _communication_statues.get(
                                        descriptors_result.Status, ""),
                                ))
                    for descriptor in list(descriptors_result.Descriptors):
                        self.services.add_descriptor(
                            BleakGATTDescriptorDotNet(
                                descriptor,
                                characteristic.Uuid.ToString(),
                                int(characteristic.AttributeHandle),
                            ))

            logger.info("Services resolved for %s", str(self))
            self._services_resolved = True
            return self.services

    # I/O methods

    async def read_gatt_char(self,
                             char_specifier: Union[BleakGATTCharacteristic,
                                                   int, str, uuid.UUID],
                             use_cached=False,
                             **kwargs) -> bytearray:
        """Perform read operation on the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from,
                specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            use_cached (bool): `False` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        Returns:
            (bytearray) The read data.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {0} was not found!".format(char_specifier))

        read_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattReadResult](characteristic.obj.ReadValueAsync(
                BluetoothCacheMode.
                Cached if use_cached else BluetoothCacheMode.Uncached)),
            return_type=GattReadResult,
        )
        if read_result.Status == GattCommunicationStatus.Success:
            with BleakDataReader(read_result.Value) as reader:
                value = bytearray(reader.read())
            logger.debug("Read Characteristic {0} : {1}".format(
                characteristic.uuid, value))
        else:
            if read_result.Status == GattCommunicationStatus.ProtocolError:
                raise BleakDotNetTaskError(
                    "Could not get GATT characteristics for {0}: {1} (Error: 0x{2:02X}: {3})"
                    .format(
                        characteristic.uuid,
                        _communication_statues.get(read_result.Status, ""),
                        read_result.ProtocolError,
                        CONTROLLER_ERROR_CODES.get(read_result.ProtocolError,
                                                   "Unknown"),
                    ))
            else:
                raise BleakError(
                    "Could not read characteristic value for {0}: {1}".format(
                        characteristic.uuid,
                        _communication_statues.get(read_result.Status, ""),
                    ))
        return value

    async def read_gatt_descriptor(self,
                                   handle: int,
                                   use_cached=False,
                                   **kwargs) -> bytearray:
        """Perform read operation on the specified GATT descriptor.

        Args:
            handle (int): The handle of the descriptor to read from.
            use_cached (bool): `False` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        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))

        read_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattReadResult](descriptor.obj.ReadValueAsync(
                BluetoothCacheMode.
                Cached if use_cached else BluetoothCacheMode.Uncached)),
            return_type=GattReadResult,
        )
        if read_result.Status == GattCommunicationStatus.Success:
            with BleakDataReader(read_result.Value) as reader:
                value = bytearray(reader.read())
            logger.debug("Read Descriptor {0} : {1}".format(handle, value))
        else:
            if read_result.Status == GattCommunicationStatus.ProtocolError:
                raise BleakDotNetTaskError(
                    "Could not get GATT characteristics for {0}: {1} (Error: 0x{2:02X}: {3})"
                    .format(
                        descriptor.uuid,
                        _communication_statues.get(read_result.Status, ""),
                        read_result.ProtocolError,
                        CONTROLLER_ERROR_CODES.get(read_result.ProtocolError,
                                                   "Unknown"),
                    ))
            else:
                raise BleakError(
                    "Could not read Descriptor value for {0}: {1}".format(
                        descriptor.uuid,
                        _communication_statues.get(read_result.Status, ""),
                    ))

        return value

    async def write_gatt_char(
        self,
        char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID],
        data: bytearray,
        response: bool = False,
    ) -> None:
        """Perform a write operation of the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write
                to, specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            data (bytes or bytearray): The data to send.
            response (bool): If write-with-response operation should be done. Defaults to `False`.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} was not found!".format(char_specifier))

        with BleakDataWriter(data) as writer:
            response = (GattWriteOption.WriteWithResponse
                        if response else GattWriteOption.WriteWithoutResponse)
            write_result = await wrap_IAsyncOperation(
                IAsyncOperation[GattWriteResult](
                    characteristic.obj.WriteValueWithResultAsync(
                        writer.detach_buffer(), response)),
                return_type=GattWriteResult,
            )

        if write_result.Status == GattCommunicationStatus.Success:
            logger.debug("Write Characteristic {0} : {1}".format(
                characteristic.uuid, data))
        else:
            if write_result.Status == GattCommunicationStatus.ProtocolError:
                raise BleakError(
                    "Could not write value {0} to characteristic {1}: {2} (Error: 0x{3:02X}: {4})"
                    .format(
                        data,
                        characteristic.uuid,
                        _communication_statues.get(write_result.Status, ""),
                        write_result.ProtocolError,
                        CONTROLLER_ERROR_CODES.get(write_result.ProtocolError,
                                                   "Unknown"),
                    ))
            else:
                raise BleakError(
                    "Could not write value {0} to characteristic {1}: {2}".
                    format(
                        data,
                        characteristic.uuid,
                        _communication_statues.get(write_result.Status, ""),
                    ))

    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))

        with BleakDataWriter(data) as writer:
            write_result = await wrap_IAsyncOperation(
                IAsyncOperation[GattWriteResult](
                    descriptor.obj.WriteValueAsync(writer.DetachBuffer())),
                return_type=GattWriteResult,
            )

        if write_result.Status == GattCommunicationStatus.Success:
            logger.debug("Write Descriptor {0} : {1}".format(handle, data))
        else:
            if write_result.Status == GattCommunicationStatus.ProtocolError:
                raise BleakError(
                    "Could not write value {0} to characteristic {1}: {2} (Error: 0x{3:02X}: {4})"
                    .format(
                        data,
                        descriptor.uuid,
                        _communication_statues.get(write_result.Status, ""),
                        write_result.ProtocolError,
                        CONTROLLER_ERROR_CODES.get(write_result.ProtocolError,
                                                   "Unknown"),
                    ))
            else:
                raise BleakError(
                    "Could not write value {0} to descriptor {1}: {2}".format(
                        data,
                        descriptor.uuid,
                        _communication_statues.get(write_result.Status, ""),
                    ))

    async def start_notify(self, char_specifier: Union[BleakGATTCharacteristic,
                                                       int, str, uuid.UUID],
                           callback: Callable[[int, bytearray],
                                              None], **kwargs) -> None:
        """Activate notifications/indications on a characteristic.

        Callbacks must accept two inputs. The first will be a integer handle of the characteristic generating the
        data and the second will be a ``bytearray`` containing the data sent from the connected server.

        .. code-block:: python

            def callback(sender: int, data: bytearray):
                print(f"{sender}: {data}")
            client.start_notify(char_uuid, callback)

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to activate
                notifications/indications on a characteristic, specified by either integer handle,
                UUID or directly by the BleakGATTCharacteristic object representing it.
            callback (function): The function to be called on notification.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {0} not found!".format(char_specifier))

        if characteristic.handle in self._notification_callbacks:
            await self.stop_notify(characteristic)

        characteristic_obj = characteristic.obj
        if (characteristic_obj.CharacteristicProperties
                & GattCharacteristicProperties.Indicate):
            cccd = GattClientCharacteristicConfigurationDescriptorValue.Indicate
        elif (characteristic_obj.CharacteristicProperties
              & GattCharacteristicProperties.Notify):
            cccd = GattClientCharacteristicConfigurationDescriptorValue.Notify
        else:
            cccd = getattr(
                GattClientCharacteristicConfigurationDescriptorValue, "None")

        self._notification_callbacks[
            characteristic.handle] = characteristic_obj.add_ValueChanged(
                TypedEventHandler[GattCharacteristic,
                                  GattValueChangedEventArgs](
                                      _notification_wrapper(
                                          callback, asyncio.get_event_loop())))

        status = await wrap_IAsyncOperation(
            IAsyncOperation[GattCommunicationStatus](
                characteristic_obj.
                WriteClientCharacteristicConfigurationDescriptorAsync(cccd)),
            return_type=GattCommunicationStatus,
        )

        if status != GattCommunicationStatus.Success:
            # This usually happens when a device reports that it support indicate,
            # but it actually doesn't.
            characteristic_obj.remove_ValueChanged(
                self._notification_callbacks.pop(characteristic.handle))
            # TODO: Find out how to get the ProtocolError code that describes a potential GattCommunicationStatus.ProtocolError result.
            raise BleakError("Could not start notify on {0}: {1}".format(
                characteristic.uuid, _communication_statues.get(status, "")))

    async def stop_notify(
        self, char_specifier: Union[BleakGATTCharacteristic, int, str,
                                    uuid.UUID]
    ) -> None:
        """Deactivate notification/indication on a specified characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate
                notification/indication on, specified by either integer handle, UUID or
                directly by the BleakGATTCharacteristic object representing it.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} not found!".format(char_specifier))

        status = await wrap_IAsyncOperation(
            IAsyncOperation[GattCommunicationStatus](
                characteristic.obj.
                WriteClientCharacteristicConfigurationDescriptorAsync(
                    getattr(
                        GattClientCharacteristicConfigurationDescriptorValue,
                        "None"))),
            return_type=GattCommunicationStatus,
        )

        if status != GattCommunicationStatus.Success:
            raise BleakError("Could not stop notify on {0}: {1}".format(
                characteristic.uuid, _communication_statues.get(status, "")))

        characteristic.obj.remove_ValueChanged(
            self._notification_callbacks.pop(characteristic.handle))
Esempio n. 5
0
class BleakClientP4Android(BaseBleakClient):
    """A python-for-android Bleak Client

    Args:
        address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.

    Keyword Args:
        disconnected_callback (callable): Callback that will be scheduled in the
            event loop when the client is disconnected. The callable must take one
            argument, which will be this client object.
        adapter (str): Bluetooth adapter to use for discovery. [unused]
    """
    def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
        super(BleakClientP4Android, self).__init__(address_or_ble_device,
                                                   **kwargs)
        # kwarg "device" is for backwards compatibility
        self.__adapter = kwargs.get("adapter", kwargs.get("device", None))
        self.__gatt = None
        self.__mtu = None

    def __del__(self):
        if self.__gatt is not None:
            self.__gatt.close()
            self.__gatt = None

    # Connectivity methods

    async def connect(self, **kwargs) -> bool:
        """Connect to the specified GATT server.

        Returns:
            Boolean representing connection status.

        """
        loop = asyncio.get_event_loop()

        self.__adapter = defs.BluetoothAdapter.getDefaultAdapter()
        if self.__adapter is None:
            raise BleakError(
                "Bluetooth is not supported on this hardware platform")
        if self.__adapter.getState() != defs.BluetoothAdapter.STATE_ON:
            raise BleakError("Bluetooth is not turned on")

        self.__device = self.__adapter.getRemoteDevice(self.address)

        self.__callbacks = _PythonBluetoothGattCallback(self, loop)

        self._subscriptions = {}

        logger.debug(f"Connecting to BLE device @ {self.address}")

        (self.__gatt, ) = await self.__callbacks.perform_and_wait(
            dispatchApi=self.__device.connectGatt,
            dispatchParams=(
                defs.context,
                False,
                self.__callbacks.java,
                defs.BluetoothDevice.TRANSPORT_LE,
            ),
            resultApi="onConnectionStateChange",
            resultExpected=(defs.BluetoothProfile.STATE_CONNECTED, ),
            return_indicates_status=False,
        )

        try:
            logger.debug("Connection successful.")

            # unlike other backends, Android doesn't automatically negotiate
            # the MTU, so we request the largest size possible like BlueZ
            logger.debug("requesting mtu...")
            (self.__mtu, ) = await self.__callbacks.perform_and_wait(
                dispatchApi=self.__gatt.requestMtu,
                dispatchParams=(517, ),
                resultApi="onMtuChanged",
            )

            logger.debug("discovering services...")
            await self.__callbacks.perform_and_wait(
                dispatchApi=self.__gatt.discoverServices,
                dispatchParams=(),
                resultApi="onServicesDiscovered",
            )

            await self.get_services()
        except BaseException:
            # if connecting is canceled or one of the above fails, we need to
            # disconnect
            try:
                await self.disconnect()
            except Exception:
                pass
            raise

        return True

    async def disconnect(self) -> bool:
        """Disconnect from the specified GATT server.

        Returns:
            Boolean representing if device is disconnected.

        """
        logger.debug("Disconnecting from BLE device...")
        if self.__gatt is None:
            # No connection exists. Either one hasn't been created or
            # we have already called disconnect and closed the gatt
            # connection.
            logger.debug("already disconnected")
            return True

        # Try to disconnect the actual device/peripheral
        try:
            await self.__callbacks.perform_and_wait(
                dispatchApi=self.__gatt.disconnect,
                dispatchParams=(),
                resultApi="onConnectionStateChange",
                resultExpected=(defs.BluetoothProfile.STATE_DISCONNECTED, ),
                unless_already=True,
                return_indicates_status=False,
            )
            self.__gatt.close()
        except Exception as e:
            logger.error(f"Attempt to disconnect device failed: {e}")

        self.__gatt = None
        self.__callbacks = None

        # Reset all stored services.
        self.services = BleakGATTServiceCollection()
        self._services_resolved = False

        return True

    async def pair(self, *args, **kwargs) -> bool:
        """Pair with the peripheral.

        You can use ConnectDevice method if you already know the MAC address of the device.
        Else you need to StartDiscovery, Trust, Pair and Connect in sequence.

        Returns:
            Boolean regarding success of pairing.

        """
        loop = asyncio.get_event_loop()

        bondedFuture = loop.create_future()

        def handleBondStateChanged(context, intent):
            bond_state = intent.getIntExtra(
                defs.BluetoothDevice.EXTRA_BOND_STATE, -1)
            if bond_state == -1:
                loop.call_soon_threadsafe(
                    bondedFuture.set_exception,
                    BleakError(f"Unexpected bond state {bond_state}"),
                )
            elif bond_state == defs.BluetoothDevice.BOND_NONE:
                loop.call_soon_threadsafe(
                    bondedFuture.set_exception,
                    BleakError(
                        f"Device with address {self.address} could not be paired with."
                    ),
                )
            elif bond_state == defs.BluetoothDevice.BOND_BONDED:
                loop.call_soon_threadsafe(bondedFuture.set_result, True)

        receiver = BroadcastReceiver(
            handleBondStateChanged,
            actions=[defs.BluetoothDevice.ACTION_BOND_STATE_CHANGED],
        )
        receiver.start()
        try:
            # See if it is already paired.
            bond_state = self.__device.getBondState()
            if bond_state == defs.BluetoothDevice.BOND_BONDED:
                return True
            elif bond_state == defs.BluetoothDevice.BOND_NONE:
                logger.debug(f"Pairing to BLE device @ {self.address}")
                if not self.__device.createBond():
                    raise BleakError(
                        f"Could not initiate bonding with device @ {self.address}"
                    )
            return await bondedFuture
        finally:
            await receiver.stop()

    async def unpair(self) -> bool:
        """Unpair with the peripheral.

        Returns:
            Boolean regarding success of unpairing.

        """
        warnings.warn(
            "Unpairing is seemingly unavailable in the Android API at the moment."
        )
        return False

    @property
    def is_connected(self) -> bool:
        """Check connection status between this client and the server.

        Returns:
            Boolean representing connection status.

        """
        return (self.__callbacks is not None
                and self.__callbacks.states["onConnectionStateChange"][1]
                == defs.BluetoothProfile.STATE_CONNECTED)

    @property
    def mtu_size(self) -> Optional[int]:
        return self.__mtu

    # 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

        logger.debug("Get Services...")
        for java_service in self.__gatt.getServices():

            service = BleakGATTServiceP4Android(java_service)
            self.services.add_service(service)

            for java_characteristic in java_service.getCharacteristics():

                characteristic = BleakGATTCharacteristicP4Android(
                    java_characteristic, service.uuid, service.handle)
                self.services.add_characteristic(characteristic)

                for descriptor_index, java_descriptor in enumerate(
                        java_characteristic.getDescriptors()):

                    descriptor = BleakGATTDescriptorP4Android(
                        java_descriptor,
                        characteristic.uuid,
                        characteristic.handle,
                        descriptor_index,
                    )
                    self.services.add_descriptor(descriptor)

        self._services_resolved = True
        return self.services

    # IO methods

    async def read_gatt_char(
        self,
        char_specifier: Union[BleakGATTCharacteristicP4Android, int, str,
                              uuid.UUID],
        **kwargs,
    ) -> bytearray:
        """Perform read operation on the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to read from,
                specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristicP4Android object representing it.

        Returns:
            (bytearray) The read data.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristicP4Android):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier

        if not characteristic:
            raise BleakError(
                f"Characteristic with UUID {char_specifier} could not be found!"
            )

        (value, ) = await self.__callbacks.perform_and_wait(
            dispatchApi=self.__gatt.readCharacteristic,
            dispatchParams=(characteristic.obj, ),
            resultApi=("onCharacteristicRead", characteristic.handle),
        )
        value = bytearray(value)
        logger.debug(
            f"Read Characteristic {characteristic.uuid} | {characteristic.handle}: {value}"
        )
        return value

    async def read_gatt_descriptor(
        self,
        desc_specifier: Union[BleakGATTDescriptorP4Android, str, uuid.UUID],
        **kwargs,
    ) -> bytearray:
        """Perform read operation on the specified GATT descriptor.

        Args:
            desc_specifier (BleakGATTDescriptorP4Android, str or UUID): The descriptor to read from,
                specified by either UUID or directly by the
                BleakGATTDescriptorP4Android object representing it.

        Returns:
            (bytearray) The read data.

        """
        if not isinstance(desc_specifier, BleakGATTDescriptorP4Android):
            descriptor = self.services.get_descriptor(desc_specifier)
        else:
            descriptor = desc_specifier

        if not descriptor:
            raise BleakError(
                f"Descriptor with UUID {desc_specifier} was not found!")

        (value, ) = await self.__callbacks.perform_and_wait(
            dispatchApi=self.__gatt.readDescriptor,
            dispatchParams=(descriptor.obj, ),
            resultApi=("onDescriptorRead", descriptor.uuid),
        )
        value = bytearray(value)

        logger.debug(
            f"Read Descriptor {descriptor.uuid} | {descriptor.handle}: {value}"
        )

        return value

    async def write_gatt_char(
        self,
        char_specifier: Union[BleakGATTCharacteristicP4Android, int, str,
                              uuid.UUID],
        data: bytearray,
        response: bool = False,
    ) -> None:
        """Perform a write operation on the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to write
                to, specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristicP4Android object representing it.
            data (bytes or bytearray): The data to send.
            response (bool): If write-with-response operation should be done. Defaults to `False`.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristicP4Android):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier

        if not characteristic:
            raise BleakError(f"Characteristic {char_specifier} was not found!")

        if ("write" not in characteristic.properties
                and "write-without-response" not in characteristic.properties):
            raise BleakError(
                f"Characteristic {str(characteristic.uuid)} does not support write operations!"
            )
        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(characteristic.uuid))

        if response:
            characteristic.obj.setWriteType(
                defs.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
        else:
            characteristic.obj.setWriteType(
                defs.BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE)

        characteristic.obj.setValue(data)

        await self.__callbacks.perform_and_wait(
            dispatchApi=self.__gatt.writeCharacteristic,
            dispatchParams=(characteristic.obj, ),
            resultApi=("onCharacteristicWrite", characteristic.handle),
        )

        logger.debug(
            f"Write Characteristic {characteristic.uuid} | {characteristic.handle}: {data}"
        )

    async def write_gatt_descriptor(
        self,
        desc_specifier: Union[BleakGATTDescriptorP4Android, str, uuid.UUID],
        data: bytearray,
    ) -> None:
        """Perform a write operation on the specified GATT descriptor.

        Args:
            desc_specifier (BleakGATTDescriptorP4Android, str or UUID): The descriptor to write
                to, specified by either UUID or directly by the
                BleakGATTDescriptorP4Android object representing it.
            data (bytes or bytearray): The data to send.

        """
        if not isinstance(desc_specifier, BleakGATTDescriptorP4Android):
            descriptor = self.services.get_descriptor(desc_specifier)
        else:
            descriptor = desc_specifier

        if not descriptor:
            raise BleakError(f"Descriptor {desc_specifier} was not found!")

        descriptor.obj.setValue(data)

        await self.__callbacks.perform_and_wait(
            dispatchApi=self.__gatt.writeDescriptor,
            dispatchParams=(descriptor.obj, ),
            resultApi=("onDescriptorWrite", descriptor.uuid),
        )

        logger.debug(
            f"Write Descriptor {descriptor.uuid} | {descriptor.handle}: {data}"
        )

    async def start_notify(
        self,
        char_specifier: Union[BleakGATTCharacteristicP4Android, int, str,
                              uuid.UUID],
        callback: Callable[[int, bytearray], None],
        **kwargs,
    ) -> None:
        """Activate notifications/indications on a characteristic.

        Callbacks must accept two inputs. The first will be an integer handle of the characteristic generating the
        data and the second will be a ``bytearray`` containing the data sent from the connected server.

        .. code-block:: python

            def callback(sender: int, data: bytearray):
                print(f"{sender}: {data}")
            client.start_notify(char_uuid, callback)

        Args:
            char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to activate
                notifications/indications on a characteristic, specified by either integer handle,
                UUID or directly by the BleakGATTCharacteristicP4Android object representing it.
            callback (function): The function to be called on notification.
        """
        if not isinstance(char_specifier, BleakGATTCharacteristicP4Android):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier

        if not characteristic:
            raise BleakError(
                f"Characteristic with UUID {char_specifier} could not be found!"
            )

        self._subscriptions[characteristic.handle] = callback

        if not self.__gatt.setCharacteristicNotification(
                characteristic.obj, True):
            raise BleakError(
                f"Failed to enable notification for characteristic {characteristic.uuid}"
            )

        await self.write_gatt_descriptor(
            characteristic.notification_descriptor,
            defs.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE,
        )

    async def stop_notify(
        self,
        char_specifier: Union[BleakGATTCharacteristicP4Android, int, str,
                              uuid.UUID],
    ) -> None:
        """Deactivate notification/indication on a specified characteristic.

        Args:
            char_specifier (BleakGATTCharacteristicP4Android, int, str or UUID): The characteristic to deactivate
                notification/indication on, specified by either integer handle, UUID or
                directly by the BleakGATTCharacteristicP4Android object representing it.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristicP4Android):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(f"Characteristic {char_specifier} not found!")

        await self.write_gatt_descriptor(
            characteristic.notification_descriptor,
            defs.BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE,
        )

        if not self.__gatt.setCharacteristicNotification(
                characteristic.obj, False):
            raise BleakError(
                f"Failed to disable notification for characteristic {characteristic.uuid}"
            )
        del self._subscriptions[characteristic.handle]
Esempio n. 6
0
class BleakClientCoreBluetooth(BaseBleakClient):
    """CoreBluetooth class interface for BleakClient

    Args:
        address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.

    Keyword Args:
        timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.

    """
    def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
        super(BleakClientCoreBluetooth, self).__init__(address_or_ble_device,
                                                       **kwargs)

        if isinstance(address_or_ble_device, BLEDevice):
            self._device_info = address_or_ble_device.details
            self._central_manager_delegate = address_or_ble_device.metadata[
                "delegate"]
        else:
            self._device_info = None
            self._central_manager_delegate = None
        self._requester = None
        self._callbacks = {}
        self._services = None

    def __str__(self):
        return "BleakClientCoreBluetooth ({})".format(self.address)

    async def connect(self, **kwargs) -> bool:
        """Connect to a specified Peripheral

        Keyword Args:
            timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.

        Returns:
            Boolean representing connection status.

        """
        timeout = kwargs.get("timeout", self._timeout)
        if self._device_info is None:
            device = await BleakScannerCoreBluetooth.find_device_by_address(
                self.address, timeout=timeout)

            if device:
                self._device_info = device.details
                self._central_manager_delegate = device.metadata["delegate"]
            else:
                raise BleakError("Device with address {} was not found".format(
                    self.address))
        # self._device_info.manager() should return a CBCentralManager

        manager = self._central_manager_delegate
        logger.debug("CentralManagerDelegate  at {}".format(manager))
        logger.debug("Connecting to BLE device @ {}".format(self.address))
        await manager.connect_(self._device_info, timeout=timeout)
        manager.disconnected_callback = self._disconnected_callback_client

        # Now get services
        await self.get_services()

        return True

    def _disconnected_callback_client(self):
        """
        Callback for device disconnection. Bleak callback sends one argument as client. This is wrapper function
        that gets called from the CentralManager and call actual disconnected_callback by sending client as argument
        """
        logger.debug("Received disconnection callback...")

        if self._disconnected_callback is not None:
            self._disconnected_callback(self)

    async def disconnect(self) -> bool:
        """Disconnect from the peripheral device"""
        manager = self._central_manager_delegate
        if manager is None:
            return False
        await manager.disconnect()
        self.services = BleakGATTServiceCollection()
        # Ensure that `get_services` retrieves services again, rather than using the cached object
        self._services_resolved = False
        self._services = None
        return True

    async def is_connected(self) -> bool:
        """Checks for current active connection"""
        manager = self._central_manager_delegate
        return manager.isConnected

    async def pair(self, *args, **kwargs) -> bool:
        """Attempt to pair with a peripheral.

        .. note::

            This is not available on macOS since there is not explicit method to do a pairing, Instead the docs
            state that it "auto-pairs" when trying to read a characteristic that requires encryption, something
            Bleak cannot do apparently.

        Reference:

            - `Apple Docs <https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral.html#//apple_ref/doc/uid/TP40013257-CH5-SW1>`_
            - `Stack Overflow post #1 <https://stackoverflow.com/questions/25254932/can-you-pair-a-bluetooth-le-device-in-an-ios-app>`_
            - `Stack Overflow post #2 <https://stackoverflow.com/questions/47546690/ios-bluetooth-pairing-request-dialog-can-i-know-the-users-choice>`_

        Returns:
            Boolean regarding success of pairing.

        """
        raise NotImplementedError(
            "Pairing is not available in Core Bluetooth.")

    async def unpair(self) -> bool:
        """

        Returns:

        """
        raise NotImplementedError(
            "Pairing is not available in Core Bluetooth.")

    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 is not None:
            return self.services

        logger.debug("Retrieving services...")
        manager = self._central_manager_delegate
        services = await manager.connected_peripheral_delegate.discoverServices(
        )

        for service in services:
            serviceUUID = service.UUID().UUIDString()
            logger.debug("Retrieving characteristics for service {}".format(
                serviceUUID))
            characteristics = (await manager.connected_peripheral_delegate.
                               discoverCharacteristics_(service))

            self.services.add_service(BleakGATTServiceCoreBluetooth(service))

            for characteristic in characteristics:
                cUUID = characteristic.UUID().UUIDString()
                logger.debug(
                    "Retrieving descriptors for characteristic {}".format(
                        cUUID))
                descriptors = (await manager.connected_peripheral_delegate.
                               discoverDescriptors_(characteristic))

                self.services.add_characteristic(
                    BleakGATTCharacteristicCoreBluetooth(characteristic))
                for descriptor in descriptors:
                    self.services.add_descriptor(
                        BleakGATTDescriptorCoreBluetooth(
                            descriptor,
                            cb_uuid_to_str(characteristic.UUID()),
                            int(characteristic.handle()),
                        ))
        logger.debug("Services resolved for %s", str(self))
        self._services_resolved = True
        self._services = services
        return self.services

    async def read_gatt_char(self,
                             char_specifier: Union[BleakGATTCharacteristic,
                                                   int, str, uuid.UUID],
                             use_cached=False,
                             **kwargs) -> bytearray:
        """Perform read operation on the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from,
                specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            use_cached (bool): `False` forces macOS to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        Returns:
            (bytearray) The read data.

        """
        manager = self._central_manager_delegate

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} was not found!".format(char_specifier))

        output = await manager.connected_peripheral_delegate.readCharacteristic_(
            characteristic.obj, use_cached=use_cached)
        value = bytearray(output)
        logger.debug("Read Characteristic {0} : {1}".format(
            characteristic.uuid, value))
        return value

    async def read_gatt_descriptor(self,
                                   handle: int,
                                   use_cached=False,
                                   **kwargs) -> bytearray:
        """Perform read operation on the specified GATT descriptor.

        Args:
            handle (int): The handle of the descriptor to read from.
            use_cached (bool): `False` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        Returns:
            (bytearray) The read data.
        """
        manager = self._central_manager_delegate

        descriptor = self.services.get_descriptor(handle)
        if not descriptor:
            raise BleakError("Descriptor {} was not found!".format(handle))

        output = await manager.connected_peripheral_delegate.readDescriptor_(
            descriptor.obj, use_cached=use_cached)
        if isinstance(
                output, str
        ):  # Sometimes a `pyobjc_unicode`or `__NSCFString` is returned and they can be used as regular Python strings.
            value = bytearray(output.encode("utf-8"))
        else:  # _NSInlineData
            value = bytearray(
                output)  # value.getBytes_length_(None, len(value))
        logger.debug("Read Descriptor {0} : {1}".format(handle, value))
        return value

    async def write_gatt_char(
        self,
        char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID],
        data: bytearray,
        response: bool = False,
    ) -> None:
        """Perform a write operation of the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write
                to, specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            data (bytes or bytearray): The data to send.
            response (bool): If write-with-response operation should be done. Defaults to `False`.

        """
        manager = self._central_manager_delegate

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} was not found!".format(char_specifier))

        value = NSData.alloc().initWithBytes_length_(data, len(data))
        success = (await manager.connected_peripheral_delegate.
                   writeCharacteristic_value_type_(
                       characteristic.obj,
                       value,
                       CBCharacteristicWriteWithResponse
                       if response else CBCharacteristicWriteWithoutResponse,
                   ))
        if success:
            logger.debug("Write Characteristic {0} : {1}".format(
                characteristic.uuid, data))
        else:
            raise BleakError(
                "Could not write value {0} to characteristic {1}: {2}".format(
                    data, characteristic.uuid, success))

    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.

        """
        manager = self._central_manager_delegate

        descriptor = self.services.get_descriptor(handle)
        if not descriptor:
            raise BleakError("Descriptor {} was not found!".format(handle))

        value = NSData.alloc().initWithBytes_length_(data, len(data))
        success = await manager.connected_peripheral_delegate.writeDescriptor_value_(
            descriptor.obj, value)
        if success:
            logger.debug("Write Descriptor {0} : {1}".format(handle, data))
        else:
            raise BleakError(
                "Could not write value {0} to descriptor {1}: {2}".format(
                    data, descriptor.uuid, success))

    async def start_notify(self, char_specifier: Union[BleakGATTCharacteristic,
                                                       int, str, uuid.UUID],
                           callback: Callable[[int, bytearray],
                                              None], **kwargs) -> None:
        """Activate notifications/indications on a characteristic.

        Callbacks must accept two inputs. The first will be a integer handle of the characteristic generating the
        data and the second will be a ``bytearray`` containing the data sent from the connected server.

        .. code-block:: python

            def callback(sender: int, data: bytearray):
                print(f"{sender}: {data}")
            client.start_notify(char_uuid, callback)

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to activate
                notifications/indications on a characteristic, specified by either integer handle,
                UUID or directly by the BleakGATTCharacteristic object representing it.
            callback (function): The function to be called on notification.

        """
        manager = self._central_manager_delegate

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {0} not found!".format(char_specifier))

        success = await manager.connected_peripheral_delegate.startNotify_cb_(
            characteristic.obj, callback)
        if not success:
            raise BleakError("Could not start notify on {0}: {1}".format(
                characteristic.uuid, success))

    async def stop_notify(
        self, char_specifier: Union[BleakGATTCharacteristic, int, str,
                                    uuid.UUID]
    ) -> None:
        """Deactivate notification/indication on a specified characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate
                notification/indication on, specified by either integer handle, UUID or
                directly by the BleakGATTCharacteristic object representing it.


        """
        manager = self._central_manager_delegate

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} not found!".format(char_specifier))

        success = await manager.connected_peripheral_delegate.stopNotify_(
            characteristic.obj)
        if not success:
            raise BleakError("Could not stop notify on {0}: {1}".format(
                characteristic.uuid, success))

    async def get_rssi(self) -> int:
        """To get RSSI value in dBm of the connected Peripheral"""

        self._device_info.readRSSI()
        manager = self._central_manager_delegate
        RSSI = manager.connected_peripheral.RSSI()
        for i in range(20):  # First time takes a little otherwise returns None
            RSSI = manager.connected_peripheral.RSSI()
            if not RSSI:
                await asyncio.sleep(0.1)
            else:
                return int(RSSI)

        if not RSSI:
            return None
Esempio n. 7
0
class BleakClientWinRT(BaseBleakClient):
    """Native Windows Bleak Client.

    Implemented using `winrt <https://github.com/Microsoft/xlang/tree/master/src/package/pywinrt/projection>`_,
    a package that enables Python developers to access Windows Runtime APIs directly from Python.

    Args:
        address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.

    Keyword Args:
        use_cached (bool): If set to `True`, then the OS level BLE cache is used for
                getting services, characteristics and descriptors. Defaults to ``True``.
        timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.

    """
    def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
        super(BleakClientWinRT, self).__init__(address_or_ble_device, **kwargs)

        # Backend specific. WinRT objects.
        if isinstance(address_or_ble_device, BLEDevice):
            self._device_info = address_or_ble_device.details.bluetooth_address
        else:
            self._device_info = None
        self._requester = None
        self._connect_events: List[asyncio.Event] = []
        self._disconnect_events: List[asyncio.Event] = []
        self._session: GattSession = None

        self._address_type = (
            kwargs["address_type"] if "address_type" in kwargs
            and kwargs["address_type"] in ("public", "random") else None)

        self._connection_status_changed_token = None
        self._use_cached = kwargs.get("use_cached", True)

    def __str__(self):
        return "BleakClientWinRT ({0})".format(self.address)

    # Connectivity methods

    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.
            use_cached (bool): If set to `True`, then the OS level BLE cache is used for
                getting services, characteristics and descriptors. Defaults to ``True``.

        Returns:
            Boolean representing connection status.

        """

        # Try to find the desired device.
        timeout = kwargs.get("timeout", self._timeout)
        use_cached = kwargs.get("use_cached", self._use_cached)
        if self._device_info is None:
            device = await BleakScannerWinRT.find_device_by_address(
                self.address, timeout=timeout)

            if device:
                self._device_info = device.details.bluetooth_address
            else:
                raise BleakError(
                    "Device with address {0} was not found.".format(
                        self.address))

        logger.debug("Connecting to BLE device @ {0}".format(self.address))

        args = [
            self._device_info,
        ]
        if self._address_type is not None:
            args.append(BluetoothAddressType.PUBLIC if self._address_type ==
                        "public" else BluetoothAddressType.RANDOM)
        self._requester = await BluetoothLEDevice.from_bluetooth_address_async(
            *args)

        # Called on disconnect event or on failure to connect.
        def handle_disconnect():
            if self._connection_status_changed_token:
                self._requester.remove_connection_status_changed(
                    self._connection_status_changed_token)
                self._connection_status_changed_token = None

            if self._requester:
                self._requester.close()
                self._requester = None

            if self._session:
                self._session.close()
                self._session = None

        def handle_connection_status_changed(
            connection_status: BluetoothConnectionStatus, ):
            if connection_status == BluetoothConnectionStatus.CONNECTED:
                for e in self._connect_events:
                    e.set()

            elif connection_status == BluetoothConnectionStatus.DISCONNECTED:
                if self._disconnected_callback:
                    self._disconnected_callback(self)

                for e in self._disconnect_events:
                    e.set()

                handle_disconnect()

        loop = asyncio.get_event_loop()

        def _ConnectionStatusChanged_Handler(sender, args):
            logger.debug("_ConnectionStatusChanged_Handler: %d",
                         sender.connection_status)
            loop.call_soon_threadsafe(handle_connection_status_changed,
                                      sender.connection_status)

        self._connection_status_changed_token = (
            self._requester.add_connection_status_changed(
                _ConnectionStatusChanged_Handler))

        # Start a GATT Session to connect
        event = asyncio.Event()
        self._connect_events.append(event)
        try:
            self._session = await GattSession.from_device_id_async(
                self._requester.bluetooth_device_id)
            # This keeps the device connected until we dispose the session or
            # until we set maintain_connection = False.
            self._session.maintain_connection = True
            await asyncio.wait_for(event.wait(), timeout=timeout)
        except BaseException:
            handle_disconnect()
            raise
        finally:
            self._connect_events.remove(event)

        # Obtain services, which also leads to connection being established.
        await self.get_services(use_cached=use_cached)

        return True

    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 notifications.
        for handle, event_handler_token in list(
                self._notification_callbacks.items()):
            char = self.services.get_characteristic(handle)
            char.obj.remove_value_changed(event_handler_token)
        self._notification_callbacks.clear()

        # Dispose all service components that we have requested and created.
        for service in self.services:
            service.obj.close()
        self.services = BleakGATTServiceCollection()
        self._services_resolved = False

        # Without this, disposing the BluetoothLEDevice won't disconnect it
        if self._session:
            self._session.close()

        # Dispose of the BluetoothLEDevice and see that the connection
        # status is now Disconnected.
        if self._requester:
            event = asyncio.Event()
            self._disconnect_events.append(event)
            try:
                self._requester.close()
                await asyncio.wait_for(event.wait(), timeout=10)
            finally:
                self._disconnect_events.remove(event)

        return True

    @property
    def is_connected(self) -> bool:
        """Check connection status between this client and the server.

        Returns:
            Boolean representing connection status.

        """
        return self._DeprecatedIsConnectedReturn(
            False if self._requester is None else self._requester.
            connection_status == BluetoothConnectionStatus.CONNECTED)

    @property
    def mtu_size(self) -> int:
        """Get ATT MTU size for active connection"""
        return self._session.max_pdu_size

    async def pair(self, protection_level: int = None, **kwargs) -> bool:
        """Attempts to pair with the device.

        Keyword Args:
            protection_level:
                    Windows.Devices.Enumeration.DevicePairingProtectionLevel
                        1: None - Pair the device using no levels of protection.
                        2: Encryption - Pair the device using encryption.
                        3: EncryptionAndAuthentication - Pair the device using
                           encryption and authentication. (This will not work in Bleak...)

        Returns:
            Boolean regarding success of pairing.

        """

        if (self._requester.device_information.pairing.can_pair
                and not self._requester.device_information.pairing.is_paired):

            # Currently only supporting Just Works solutions...
            ceremony = DevicePairingKinds.CONFIRM_ONLY
            custom_pairing = self._requester.device_information.pairing.custom

            def handler(sender, args):
                args.accept()

            pairing_requested_token = custom_pairing.add_pairing_requested(
                handler)
            try:
                if protection_level:
                    pairing_result = await custom_pairing.pair_async(
                        ceremony, protection_level)
                else:
                    pairing_result = await custom_pairing.pair_async(ceremony)

            except Exception as e:
                raise BleakError("Failure trying to pair with device!") from e
            finally:
                custom_pairing.remove_pairing_requested(
                    pairing_requested_token)

            if pairing_result.status not in (
                    DevicePairingResultStatus.PAIRED,
                    DevicePairingResultStatus.ALREADY_PAIRED,
            ):
                raise BleakError("Could not pair with device: {0}: {1}".format(
                    pairing_result.status,
                    _pairing_statuses.get(pairing_result.status),
                ))
            else:
                logger.info(
                    "Paired to device with protection level {0}.".format(
                        pairing_result.protection_level_used))
                return True
        else:
            return self._requester.device_information.pairing.is_paired

    async def unpair(self) -> bool:
        """Attempts to unpair from the device.

        N.B. unpairing also leads to disconnection in the Windows backend.

        Returns:
            Boolean on whether the unparing was successful.

        """

        if self._requester.device_information.pairing.is_paired:
            unpairing_result = (
                await
                self._requester.device_information.pairing.unpair_async())

            if unpairing_result.status not in (
                    DevicePairingResultStatus.PAIRED,
                    DevicePairingResultStatus.ALREADY_PAIRED,
            ):
                raise BleakError(
                    "Could not unpair with device: {0}: {1}".format(
                        unpairing_result.status,
                        _unpairing_statuses.get(unpairing_result.status),
                    ))
            else:
                logger.info("Unpaired with device.")
                return True

        return not self._requester.device_information.pairing.is_paired

    # GATT services methods

    async def get_services(self, **kwargs) -> BleakGATTServiceCollection:
        """Get all services registered for this GATT server.

        Keyword Args:

            use_cached (bool): If set to `True`, then the OS level BLE cache is used for
                getting services, characteristics and descriptors.

        Returns:
           A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree.

        """
        use_cached = kwargs.get("use_cached", self._use_cached)
        # Return the Service Collection.
        if self._services_resolved:
            return self.services
        else:
            logger.debug("Get Services...")
            services_result = await self._requester.get_gatt_services_async(
                BluetoothCacheMode.
                CACHED if use_cached else BluetoothCacheMode.UNCACHED)

            if services_result.status != GattCommunicationStatus.SUCCESS:
                if services_result.status == GattCommunicationStatus.PROTOCOL_ERROR:
                    raise BleakDotNetTaskError(
                        "Could not get GATT services: {0} (Error: 0x{1:02X}: {2})"
                        .format(
                            _communication_statues.get(services_result.status,
                                                       ""),
                            services_result.protocol_error,
                            CONTROLLER_ERROR_CODES.get(
                                services_result.protocol_error, "Unknown"),
                        ))
                else:
                    raise BleakDotNetTaskError(
                        "Could not get GATT services: {0}".format(
                            _communication_statues.get(services_result.status,
                                                       "")))

            for service in services_result.services:
                characteristics_result = await service.get_characteristics_async(
                    BluetoothCacheMode.
                    CACHED if use_cached else BluetoothCacheMode.UNCACHED)
                self.services.add_service(BleakGATTServiceWinRT(service))
                if characteristics_result.status != GattCommunicationStatus.SUCCESS:
                    if (characteristics_result.status ==
                            GattCommunicationStatus.PROTOCOL_ERROR):
                        raise BleakDotNetTaskError(
                            "Could not get GATT characteristics for {0}: {1} (Error: 0x{2:02X}: {3})"
                            .format(
                                service,
                                _communication_statues.get(
                                    characteristics_result.status, ""),
                                characteristics_result.protocol_error,
                                CONTROLLER_ERROR_CODES.get(
                                    characteristics_result.protocol_error,
                                    "Unknown"),
                            ))
                    else:
                        raise BleakDotNetTaskError(
                            "Could not get GATT characteristics for {0}: {1}".
                            format(
                                service,
                                _communication_statues.get(
                                    characteristics_result.status, ""),
                            ))
                for characteristic in characteristics_result.characteristics:
                    descriptors_result = await characteristic.get_descriptors_async(
                        BluetoothCacheMode.
                        CACHED if use_cached else BluetoothCacheMode.UNCACHED)
                    self.services.add_characteristic(
                        BleakGATTCharacteristicWinRT(characteristic))
                    if descriptors_result.status != GattCommunicationStatus.SUCCESS:
                        if (characteristics_result.status ==
                                GattCommunicationStatus.PROTOCOL_ERROR):
                            raise BleakDotNetTaskError(
                                "Could not get GATT descriptors for {0}: {1} (Error: 0x{2:02X}: {3})"
                                .format(
                                    service,
                                    _communication_statues.get(
                                        descriptors_result.status, ""),
                                    descriptors_result.protocol_error,
                                    CONTROLLER_ERROR_CODES.get(
                                        descriptors_result.protocol_error,
                                        "Unknown"),
                                ))
                        else:
                            raise BleakDotNetTaskError(
                                "Could not get GATT descriptors for {0}: {1}".
                                format(
                                    characteristic,
                                    _communication_statues.get(
                                        descriptors_result.status, ""),
                                ))
                    for descriptor in list(descriptors_result.descriptors):
                        self.services.add_descriptor(
                            BleakGATTDescriptorWinRT(
                                descriptor,
                                str(characteristic.uuid),
                                characteristic.attribute_handle,
                            ))

            logger.info("Services resolved for %s", str(self))
            self._services_resolved = True
            return self.services

    # I/O methods

    async def read_gatt_char(self,
                             char_specifier: Union[BleakGATTCharacteristic,
                                                   int, str, uuid.UUID],
                             **kwargs) -> bytearray:
        """Perform read operation on the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from,
                specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.

        Keyword Args:
            use_cached (bool): ``False`` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to ``False``.

        Returns:
            (bytearray) The read data.

        """
        use_cached = kwargs.get("use_cached", False)

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {0} was not found!".format(char_specifier))

        read_result = await characteristic.obj.read_value_async(
            BluetoothCacheMode.CACHED if use_cached else BluetoothCacheMode.
            UNCACHED)

        if read_result.status == GattCommunicationStatus.SUCCESS:
            value = bytearray(
                CryptographicBuffer.copy_to_byte_array(read_result.value))
            logger.debug("Read Characteristic {0} : {1}".format(
                characteristic.uuid, value))
        else:
            if read_result.status == GattCommunicationStatus.PROTOCOL_ERROR:
                raise BleakDotNetTaskError(
                    "Could not get GATT characteristics for {0}: {1} (Error: 0x{2:02X}: {3})"
                    .format(
                        characteristic.uuid,
                        _communication_statues.get(read_result.status, ""),
                        read_result.protocol_error,
                        CONTROLLER_ERROR_CODES.get(read_result.protocol_error,
                                                   "Unknown"),
                    ))
            else:
                raise BleakError(
                    "Could not read characteristic value for {0}: {1}".format(
                        characteristic.uuid,
                        _communication_statues.get(read_result.status, ""),
                    ))
        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.

        Keyword Args:
            use_cached (bool): `False` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        Returns:
            (bytearray) The read data.

        """
        use_cached = kwargs.get("use_cached", False)

        descriptor = self.services.get_descriptor(handle)
        if not descriptor:
            raise BleakError(
                "Descriptor with handle {0} was not found!".format(handle))

        read_result = await descriptor.obj.read_value_async(
            BluetoothCacheMode.CACHED if use_cached else BluetoothCacheMode.
            UNCACHED)

        if read_result.status == GattCommunicationStatus.SUCCESS:
            value = bytearray(
                CryptographicBuffer.copy_to_byte_array(read_result.value))
            logger.debug("Read Descriptor {0} : {1}".format(handle, value))
        else:
            if read_result.status == GattCommunicationStatus.PROTOCOL_ERROR:
                raise BleakDotNetTaskError(
                    "Could not get GATT characteristics for {0}: {1} (Error: 0x{2:02X}: {3})"
                    .format(
                        descriptor.uuid,
                        _communication_statues.get(read_result.status, ""),
                        read_result.protocol_error,
                        CONTROLLER_ERROR_CODES.get(read_result.protocol_error,
                                                   "Unknown"),
                    ))
            else:
                raise BleakError(
                    "Could not read Descriptor value for {0}: {1}".format(
                        descriptor.uuid,
                        _communication_statues.get(read_result.status, ""),
                    ))

        return value

    async def write_gatt_char(
        self,
        char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID],
        data: Union[bytes, bytearray, memoryview],
        response: bool = False,
    ) -> None:
        """Perform a write operation of the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write
                to, specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            data (bytes or bytearray): The data to send.
            response (bool): If write-with-response operation should be done. Defaults to `False`.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} was not found!".format(char_specifier))

        response = (GattWriteOption.WRITE_WITH_RESPONSE
                    if response else GattWriteOption.WRITE_WITHOUT_RESPONSE)
        write_result = await characteristic.obj.write_value_with_result_async(
            CryptographicBuffer.create_from_byte_array(list(data)), response)

        if write_result.status == GattCommunicationStatus.SUCCESS:
            logger.debug("Write Characteristic {0} : {1}".format(
                characteristic.uuid, data))
        else:
            if write_result.status == GattCommunicationStatus.PROTOCOL_ERROR:
                raise BleakError(
                    "Could not write value {0} to characteristic {1}: {2} (Error: 0x{3:02X}: {4})"
                    .format(
                        data,
                        characteristic.uuid,
                        _communication_statues.get(write_result.status, ""),
                        write_result.protocol_error,
                        CONTROLLER_ERROR_CODES.get(write_result.protocol_error,
                                                   "Unknown"),
                    ))
            else:
                raise BleakError(
                    "Could not write value {0} to characteristic {1}: {2}".
                    format(
                        data,
                        characteristic.uuid,
                        _communication_statues.get(write_result.status, ""),
                    ))

    async def write_gatt_descriptor(
            self, handle: int, data: Union[bytes, bytearray,
                                           memoryview]) -> 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))

        write_result = await descriptor.obj.write_value_async(
            CryptographicBuffer.create_from_byte_array(list(data)))

        if write_result.status == GattCommunicationStatus.SUCCESS:
            logger.debug("Write Descriptor {0} : {1}".format(handle, data))
        else:
            if write_result.status == GattCommunicationStatus.PROTOCOL_ERROR:
                raise BleakError(
                    "Could not write value {0} to characteristic {1}: {2} (Error: 0x{3:02X}: {4})"
                    .format(
                        data,
                        descriptor.uuid,
                        _communication_statues.get(write_result.status, ""),
                        write_result.protocol_error,
                        CONTROLLER_ERROR_CODES.get(write_result.protocol_error,
                                                   "Unknown"),
                    ))
            else:
                raise BleakError(
                    "Could not write value {0} to descriptor {1}: {2}".format(
                        data,
                        descriptor.uuid,
                        _communication_statues.get(write_result.status, ""),
                    ))

    async def start_notify(self, char_specifier: Union[BleakGATTCharacteristic,
                                                       int, str, uuid.UUID],
                           callback: Callable[[int, bytearray],
                                              None], **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:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to activate
                notifications/indications on a characteristic, specified by either integer handle,
                UUID or directly by the BleakGATTCharacteristic object representing it.
            callback (function): The function to be called on notification.

        """
        if inspect.iscoroutinefunction(callback):

            def bleak_callback(s, d):
                asyncio.ensure_future(callback(s, d))

        else:
            bleak_callback = callback

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {0} not found!".format(char_specifier))

        if self._notification_callbacks.get(characteristic.handle):
            await self.stop_notify(characteristic)

        characteristic_obj = characteristic.obj
        if (characteristic_obj.characteristic_properties
                & GattCharacteristicProperties.INDICATE):
            cccd = GattClientCharacteristicConfigurationDescriptorValue.INDICATE
        elif (characteristic_obj.characteristic_properties
              & GattCharacteristicProperties.NOTIFY):
            cccd = GattClientCharacteristicConfigurationDescriptorValue.NOTIFY
        else:
            cccd = GattClientCharacteristicConfigurationDescriptorValue.NONE

        fcn = _notification_wrapper(bleak_callback, asyncio.get_event_loop())
        event_handler_token = characteristic_obj.add_value_changed(fcn)
        self._notification_callbacks[
            characteristic.handle] = event_handler_token
        status = await characteristic_obj.write_client_characteristic_configuration_descriptor_async(
            cccd)

        if status != GattCommunicationStatus.SUCCESS:
            # This usually happens when a device reports that it supports indicate,
            # but it actually doesn't.
            if characteristic.handle in self._notification_callbacks:
                event_handler_token = self._notification_callbacks.pop(
                    characteristic.handle)
                characteristic_obj.remove_value_changed(event_handler_token)

            raise BleakError("Could not start notify on {0}: {1}".format(
                characteristic.uuid, _communication_statues.get(status, "")))

    async def stop_notify(
        self, char_specifier: Union[BleakGATTCharacteristic, int, str,
                                    uuid.UUID]
    ) -> None:
        """Deactivate notification/indication on a specified characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate
                notification/indication on, specified by either integer handle, UUID or
                directly by the BleakGATTCharacteristic object representing it.

        """
        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} not found!".format(char_specifier))

        status = await characteristic.obj.write_client_characteristic_configuration_descriptor_async(
            GattClientCharacteristicConfigurationDescriptorValue.NONE)

        if status != GattCommunicationStatus.SUCCESS:
            raise BleakError("Could not stop notify on {0}: {1}".format(
                characteristic.uuid, _communication_statues.get(status, "")))
        else:
            event_handler_token = self._notification_callbacks.pop(
                characteristic.handle)
            characteristic.obj.remove_value_changed(event_handler_token)
Esempio n. 8
0
class BleakClientCoreBluetooth(BaseBleakClient):
    """CoreBluetooth class interface for BleakClient

    Args:
        address (str): The uuid 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 during connect. Defaults to 2.0.

    """
    def __init__(self, address: str, loop: AbstractEventLoop = None, **kwargs):
        super(BleakClientCoreBluetooth, self).__init__(address, loop, **kwargs)

        self._device_info = None
        self._requester = None
        self._callbacks = {}
        self._services = None

        self._disconnected_callback = None

    def __str__(self):
        return "BleakClientCoreBluetooth ({})".format(self.address)

    async def connect(self, **kwargs) -> bool:
        """Connect to a specified Peripheral

        Keyword Args:
            timeout (float): Timeout for required ``discover`` call. Defaults to 2.0.

        Returns:
            Boolean representing connection status.

        """
        timeout = kwargs.get("timeout", self._timeout)
        devices = await discover(timeout=timeout, loop=self.loop)
        sought_device = list(
            filter(lambda x: x.address.upper() == self.address.upper(),
                   devices))

        if len(sought_device):
            self._device_info = sought_device[0].details
        else:
            raise BleakError("Device with address {} was not found".format(
                self.address))

        logger.debug("Connecting to BLE device @ {}".format(self.address))

        manager = self._device_info.manager().delegate()
        await manager.connect_(sought_device[0].details)

        # Now get services
        await self.get_services()

        return True

    async def disconnect(self) -> bool:
        """Disconnect from the peripheral device"""
        manager = self._device_info.manager().delegate()
        await manager.disconnect()
        self.services = BleakGATTServiceCollection()
        return True

    async def is_connected(self) -> bool:
        """Checks for current active connection"""
        manager = self._device_info.manager().delegate()
        return manager.isConnected

    def set_disconnected_callback(self, callback: Callable[[BaseBleakClient],
                                                           None],
                                  **kwargs) -> None:
        """Set the disconnected callback.
        Args:
            callback: callback to be called on disconnection.

        """
        manager = self._device_info.manager().delegate()
        self._disconnected_callback = callback
        manager.disconnected_callback = self._disconnect_callback_client

    def _disconnect_callback_client(self):
        """
        Callback for device disconnection. Bleak callback sends one argument as client. This is wrapper function
        that gets called from the CentralManager and call actual disconnected_callback by sending client as argument
        """
        logger.debug("Received disconnection callback...")

        if self._disconnected_callback is not None:
            self._disconnected_callback(self)

    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 is not None:
            return self._services

        logger.debug("Retrieving services...")
        manager = self._device_info.manager().delegate()
        services = await manager.connected_peripheral_delegate.discoverServices(
        )

        for service in services:
            serviceUUID = service.UUID().UUIDString()
            logger.debug("Retrieving characteristics for service {}".format(
                serviceUUID))
            characteristics = await manager.connected_peripheral_delegate.discoverCharacteristics_(
                service)

            self.services.add_service(BleakGATTServiceCoreBluetooth(service))

            for characteristic in characteristics:
                cUUID = characteristic.UUID().UUIDString()
                logger.debug(
                    "Retrieving descriptors for characteristic {}".format(
                        cUUID))
                descriptors = await manager.connected_peripheral_delegate.discoverDescriptors_(
                    characteristic)

                self.services.add_characteristic(
                    BleakGATTCharacteristicCoreBluetooth(characteristic))
                for descriptor in descriptors:
                    self.services.add_descriptor(
                        BleakGATTDescriptorCoreBluetooth(
                            descriptor,
                            characteristic.UUID().UUIDString(),
                            int(characteristic.handle()),
                        ))
        self._services_resolved = True
        self._services = services
        return self.services

    async def read_gatt_char(self,
                             char_specifier: Union[BleakGATTCharacteristic,
                                                   int, str, uuid.UUID],
                             use_cached=False,
                             **kwargs) -> bytearray:
        """Perform read operation on the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from,
                specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            use_cached (bool): `False` forces macOS to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        Returns:
            (bytearray) The read data.

        """
        manager = self._device_info.manager().delegate()

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} was not found!".format(char_specifier))

        output = await manager.connected_peripheral_delegate.readCharacteristic_(
            characteristic.obj, use_cached=use_cached)
        value = bytearray(output)
        logger.debug("Read Characteristic {0} : {1}".format(
            characteristic.uuid, value))
        return value

    async def read_gatt_descriptor(self,
                                   handle: int,
                                   use_cached=False,
                                   **kwargs) -> bytearray:
        """Perform read operation on the specified GATT descriptor.

        Args:
            handle (int): The handle of the descriptor to read from.
            use_cached (bool): `False` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        Returns:
            (bytearray) The read data.
        """
        manager = self._device_info.manager().delegate()

        descriptor = self.services.get_descriptor(handle)
        if not descriptor:
            raise BleakError("Descriptor {} was not found!".format(handle))

        output = await manager.connected_peripheral_delegate.readDescriptor_(
            descriptor.obj, use_cached=use_cached)
        if isinstance(
                output, str
        ):  # Sometimes a `pyobjc_unicode`or `__NSCFString` is returned and they can be used as regular Python strings.
            value = bytearray(output.encode("utf-8"))
        else:  # _NSInlineData
            value = bytearray(
                output)  # value.getBytes_length_(None, len(value))
        logger.debug("Read Descriptor {0} : {1}".format(handle, value))
        return value

    async def write_gatt_char(
        self,
        char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID],
        data: bytearray,
        response: bool = False,
    ) -> None:
        """Perform a write operation of the specified GATT characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write
                to, specified by either integer handle, UUID or directly by the
                BleakGATTCharacteristic object representing it.
            data (bytes or bytearray): The data to send.
            response (bool): If write-with-response operation should be done. Defaults to `False`.

        """
        manager = self._device_info.manager().delegate()

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} was not found!".format(char_specifier))

        value = NSData.alloc().initWithBytes_length_(data, len(data))
        success = await manager.connected_peripheral_delegate.writeCharacteristic_value_type_(
            characteristic.obj,
            value,
            CBCharacteristicWriteWithResponse
            if response else CBCharacteristicWriteWithoutResponse,
        )
        if success:
            logger.debug("Write Characteristic {0} : {1}".format(
                characteristic.uuid, data))
        else:
            raise BleakError(
                "Could not write value {0} to characteristic {1}: {2}".format(
                    data, characteristic.uuid, success))

    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.

        """
        manager = self._device_info.manager().delegate()

        descriptor = self.services.get_descriptor(handle)
        if not descriptor:
            raise BleakError("Descriptor {} was not found!".format(handle))

        value = NSData.alloc().initWithBytes_length_(data, len(data))
        success = await manager.connected_peripheral_delegate.writeDescriptor_value_(
            descriptor.obj, value)
        if success:
            logger.debug("Write Descriptor {0} : {1}".format(handle, data))
        else:
            raise BleakError(
                "Could not write value {0} to descriptor {1}: {2}".format(
                    data, descriptor.uuid, success))

    async def start_notify(self, char_specifier: Union[BleakGATTCharacteristic,
                                                       int, 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:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to activate
                notifications/indications on a characteristic, specified by either integer handle,
                UUID or directly by the BleakGATTCharacteristic object representing it.
            callback (function): The function to be called on notification.

        """
        manager = self._device_info.manager().delegate()

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {0} not found!".format(char_specifier))

        success = await manager.connected_peripheral_delegate.startNotify_cb_(
            characteristic.obj, callback)
        if not success:
            raise BleakError("Could not start notify on {0}: {1}".format(
                characteristic.uuid, success))

    async def stop_notify(
        self, char_specifier: Union[BleakGATTCharacteristic, int, str,
                                    uuid.UUID]
    ) -> None:
        """Deactivate notification/indication on a specified characteristic.

        Args:
            char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate
                notification/indication on, specified by either integer handle, UUID or
                directly by the BleakGATTCharacteristic object representing it.


        """
        manager = self._device_info.manager().delegate()

        if not isinstance(char_specifier, BleakGATTCharacteristic):
            characteristic = self.services.get_characteristic(char_specifier)
        else:
            characteristic = char_specifier
        if not characteristic:
            raise BleakError(
                "Characteristic {} not found!".format(char_specifier))

        success = await manager.connected_peripheral_delegate.stopNotify_(
            characteristic.obj)
        if not success:
            raise BleakError("Could not stop notify on {0}: {1}".format(
                characteristic.uuid, success))
Esempio n. 9
0
class BleakClientDotNet(BaseBleakClient):
    """The native Windows Bleak Client.

    Implemented using `pythonnet <https://pythonnet.github.io/>`_, a package that provides an integration to the .NET
    Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel.
    """
    def __init__(self, device, loop: AbstractEventLoop = None, **kwargs):
        super(BleakClientDotNet, self).__init__(device, loop, **kwargs)

        # Backend specific. Python.NET objects.
        self._device_info = device.details
        self._requester = None
        self._bridge = Bridge()
        self._callbacks = {}

    def __str__(self):
        return "BleakClientDotNet ({0})".format(self.address)

    # Connectivity methods

    async def connect(self) -> bool:
        """Connect to the specified GATT server.

        Returns:
            Boolean representing connection status.

        """
        # Try to find the desired device.
        # devices = await discover(2.0, loop=self.loop)
        # sought_device = list(
        #     filter(lambda x: x.address.upper() == self.address.upper(), devices)
        # )
        #
        # if len(sought_device):
        #     self._device_info = sought_device[0].details
        # else:
        #     raise BleakError(
        #         "Device with address {0} was " "not found.".format(self.address)
        #     )

        logger.debug("Connecting to BLE device @ {0}".format(self.address))

        self._requester = await wrap_IAsyncOperation(
            IAsyncOperation[BluetoothLEDevice](
                BluetoothLEDevice.FromBluetoothAddressAsync(
                    UInt64(self._device_info.BluetoothAddress))),
            return_type=BluetoothLEDevice,
            loop=self.loop,
        )

        def _ConnectionStatusChanged_Handler(sender, args):
            logger.debug("_ConnectionStatusChanged_Handler: " +
                         args.ToString())

        self._requester.ConnectionStatusChanged += _ConnectionStatusChanged_Handler

        # Obtain services, which also leads to connection being established.
        await self.get_services()
        await asyncio.sleep(0.2, loop=self.loop)
        connected = await self.is_connected()
        if connected:
            logger.debug("Connection successful.")
        else:
            raise BleakError("Connection to {0} was not successful!".format(
                self.address))

        return connected

    async def disconnect(self) -> bool:
        """Disconnect from the specified GATT server.

        Returns:
            Boolean representing connection status.

        """
        logger.debug("Disconnecting from BLE device...")
        # Remove notifications
        # TODO: Make sure all notifications are removed prior to Dispose.
        # Dispose all components that we have requested and created.
        for service in self.services:
            service.obj.Dispose()
        self.services = BleakGATTServiceCollection()
        self._requester.Dispose()
        self._requester = None

        return not await self.is_connected()

    async def is_connected(self) -> bool:
        """Check connection status between this client and the server.

        Returns:
            Boolean representing connection status.

        """
        if self._requester:
            return (self._requester.ConnectionStatus ==
                    BluetoothConnectionStatus.Connected)

        else:
            return False

    # 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.

        """
        # Return the Service Collection.
        if self._services_resolved:
            return self.services
        else:
            logger.debug("Get Services...")
            services_result = await wrap_IAsyncOperation(
                IAsyncOperation[GattDeviceServicesResult](
                    self._requester.GetGattServicesAsync()),
                return_type=GattDeviceServicesResult,
                loop=self.loop,
            )

            if services_result.Status != GattCommunicationStatus.Success:
                raise BleakDotNetTaskError("Could not get GATT services.")

            # TODO: Check if fetching yeilds failures...
            for service in services_result.Services:
                characteristics_result = await wrap_IAsyncOperation(
                    IAsyncOperation[GattCharacteristicsResult](
                        service.GetCharacteristicsAsync()),
                    return_type=GattCharacteristicsResult,
                    loop=self.loop,
                )
                self.services.add_service(BleakGATTServiceDotNet(service))
                if characteristics_result.Status != GattCommunicationStatus.Success:
                    raise BleakDotNetTaskError(
                        "Could not get GATT characteristics for {0}.".format(
                            service))
                for characteristic in characteristics_result.Characteristics:
                    descriptors_result = await wrap_IAsyncOperation(
                        IAsyncOperation[GattDescriptorsResult](
                            characteristic.GetDescriptorsAsync()),
                        return_type=GattDescriptorsResult,
                        loop=self.loop,
                    )
                    self.services.add_characteristic(
                        BleakGATTCharacteristicDotNet(characteristic))
                    if descriptors_result.Status != GattCommunicationStatus.Success:
                        raise BleakDotNetTaskError(
                            "Could not get GATT descriptors for {0}.".format(
                                characteristic))
                    for descriptor in list(descriptors_result.Descriptors):
                        self.services.add_descriptor(
                            BleakGATTDescriptorDotNet(
                                descriptor, characteristic.Uuid.ToString()))

            self._services_resolved = True
            return self.services

    # I/O methods

    async def read_gatt_char(self,
                             _uuid: str,
                             use_cached=False,
                             **kwargs) -> bytearray:
        """Perform read operation on the specified GATT characteristic.

        Args:
            _uuid (str or UUID): The uuid of the characteristics to read from.
            use_cached (bool): `False` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        Returns:
            (bytearray) The read data.

        """
        characteristic = self.services.get_characteristic(str(_uuid))
        if not characteristic:
            raise BleakError("Characteristic {0} was not found!".format(_uuid))

        read_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattReadResult](characteristic.obj.ReadValueAsync(
                BluetoothCacheMode.
                Cached if use_cached else BluetoothCacheMode.Uncached)),
            return_type=GattReadResult,
            loop=self.loop,
        )
        if read_result.Status == GattCommunicationStatus.Success:
            reader = DataReader.FromBuffer(IBuffer(read_result.Value))
            output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength)
            reader.ReadBytes(output)
            value = bytearray(output)
            logger.debug("Read Characteristic {0} : {1}".format(_uuid, value))
        else:
            raise BleakError(
                "Could not read characteristic value for {0}: {1}".format(
                    characteristic.uuid, read_result.Status))
        return value

    async def read_gatt_descriptor(self,
                                   handle: int,
                                   use_cached=False,
                                   **kwargs) -> bytearray:
        """Perform read operation on the specified GATT descriptor.

        Args:
            handle (int): The handle of the descriptor to read from.
            use_cached (bool): `False` forces Windows to read the value from the
                device again and not use its own cached value. Defaults to `False`.

        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))

        read_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattReadResult](descriptor.obj.ReadValueAsync(
                BluetoothCacheMode.
                Cached if use_cached else BluetoothCacheMode.Uncached)),
            return_type=GattReadResult,
            loop=self.loop,
        )
        if read_result.Status == GattCommunicationStatus.Success:
            reader = DataReader.FromBuffer(IBuffer(read_result.Value))
            output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength)
            reader.ReadBytes(output)
            value = bytearray(output)
            logger.debug("Read Descriptor {0} : {1}".format(handle, value))
        else:
            raise BleakError(
                "Could not read Descriptor value for {0}: {1}".format(
                    descriptor.uuid, read_result.Status))

        return value

    async def write_gatt_char(self,
                              _uuid: str,
                              data: bytearray,
                              response: bool = False) -> None:
        """Perform a write operation of the specified GATT characteristic.

        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))

        writer = DataWriter()
        writer.WriteBytes(Array[Byte](data))
        response = GattWriteOption.WriteWithResponse if response else GattWriteOption.WriteWithoutResponse
        write_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattWriteResult](
                characteristic.obj.WriteValueWithResultAsync(
                    writer.DetachBuffer(), response)),
            return_type=GattWriteResult,
            loop=self.loop,
        )
        if write_result.Status == GattCommunicationStatus.Success:
            logger.debug("Write Characteristic {0} : {1}".format(_uuid, data))
        else:
            raise BleakError(
                "Could not write value {0} to characteristic {1}: {2}".format(
                    data, characteristic.uuid, write_result.Status))

    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 {0} was not found!".format(handle))

        writer = DataWriter()
        writer.WriteBytes(Array[Byte](data))
        write_result = await wrap_IAsyncOperation(
            IAsyncOperation[GattWriteResult](descriptor.obj.WriteValueAsync(
                writer.DetachBuffer())),
            return_type=GattWriteResult,
            loop=self.loop,
        )
        if write_result.Status == GattCommunicationStatus.Success:
            logger.debug("Write Descriptor {0} : {1}".format(handle, data))
        else:
            raise BleakError(
                "Could not write value {0} to descriptor {1}: {2}".format(
                    data, descriptor.uuid, write_result.Status))

    async def start_notify(self, _uuid: str, 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/indication on.
            callback (function): The function to be called on notification.

        """
        characteristic = self.services.get_characteristic(str(_uuid))

        if self._notification_callbacks.get(str(_uuid)):
            await self.stop_notify(_uuid)

        status = await self._start_notify(characteristic.obj, callback)

        if status != GattCommunicationStatus.Success:
            raise BleakError("Could not start notify on {0}: {1}".format(
                characteristic.uuid, status))

    async def _start_notify(
        self,
        characteristic_obj: GattCharacteristic,
        callback: Callable[[str, Any], Any],
    ):
        """Internal method performing call to BleakUWPBridge method.

        Args:
            characteristic_obj: The Managed Windows.Devices.Bluetooth.GenericAttributeProfile.GattCharacteristic Object
            callback: The function to be called on notification.

        Returns:
            (int) The GattCommunicationStatus of the operation.

        """

        if (characteristic_obj.CharacteristicProperties
                & GattCharacteristicProperties.Indicate):
            cccd = GattClientCharacteristicConfigurationDescriptorValue.Indicate
        elif (characteristic_obj.CharacteristicProperties
              & GattCharacteristicProperties.Notify):
            cccd = GattClientCharacteristicConfigurationDescriptorValue.Notify
        else:
            cccd = getattr(
                GattClientCharacteristicConfigurationDescriptorValue, "None")

        try:
            # TODO: Enable adding multiple handlers!
            self._callbacks[characteristic_obj.Uuid.ToString(
            )] = TypedEventHandler[GattCharacteristic,
                                   GattValueChangedEventArgs](
                                       _notification_wrapper(callback))
            self._bridge.AddValueChangedCallback(
                characteristic_obj,
                self._callbacks[characteristic_obj.Uuid.ToString()])
        except Exception as e:
            logger.debug("Start Notify problem: {0}".format(e))
            if characteristic_obj.Uuid.ToString() in self._callbacks:
                callback = self._callbacks.pop(
                    characteristic_obj.Uuid.ToString())
                self._bridge.RemoveValueChangedCallback(
                    characteristic_obj, callback)

            return GattCommunicationStatus.AccessDenied

        status = await wrap_IAsyncOperation(
            IAsyncOperation[GattCommunicationStatus](
                characteristic_obj.
                WriteClientCharacteristicConfigurationDescriptorAsync(cccd)),
            return_type=GattCommunicationStatus,
            loop=self.loop,
        )

        if status != GattCommunicationStatus.Success:
            # This usually happens when a device reports that it support indicate, but it actually doesn't.
            if characteristic_obj.Uuid.ToString() in self._callbacks:
                callback = self._callbacks.pop(
                    characteristic_obj.Uuid.ToString())
                self._bridge.RemoveValueChangedCallback(
                    characteristic_obj, callback)

            return GattCommunicationStatus.AccessDenied
        return status

    async def stop_notify(self, _uuid: str) -> None:
        """Deactivate notification/indication on a specified characteristic.

        Args:
            _uuid: The characteristic to stop notifying/indicating on.

        """
        characteristic = self.services.get_characteristic(str(_uuid))

        status = await wrap_IAsyncOperation(
            IAsyncOperation[GattCommunicationStatus](
                characteristic.obj.
                WriteClientCharacteristicConfigurationDescriptorAsync(
                    getattr(
                        GattClientCharacteristicConfigurationDescriptorValue,
                        "None"))),
            return_type=GattCommunicationStatus,
            loop=self.loop,
        )

        if status != GattCommunicationStatus.Success:
            raise BleakError("Could not start notify on {0}: {1}".format(
                characteristic.uuid, status))
        else:
            callback = self._callbacks.pop(characteristic.uuid)
            self._bridge.RemoveValueChangedCallback(characteristic.obj,
                                                    callback)