Esempio n. 1
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._bridge = 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.

        """
        # Create a new BleakBridge here.
        self._bridge = Bridge()

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

        loop = asyncio.get_event_loop()

        def _ConnectionStatusChanged_Handler(sender, args):
            logger.debug("_ConnectionStatusChanged_Handler: %d",
                         sender.ConnectionStatus)
            if (sender.ConnectionStatus
                    == BluetoothConnectionStatus.Disconnected
                    and self._disconnected_callback):
                loop.call_soon_threadsafe(self._disconnected_callback, self)

        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)
                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 if device is disconnected.

        """
        logger.debug("Disconnecting from BLE device...")
        # Remove notifications. Remove them first in the BleakBridge and then clear
        # remaining notifications in Python as well.
        for characteristic in self.services.characteristics.values():
            self._bridge.RemoveValueChangedCallback(characteristic.obj)
        self._notification_callbacks.clear()

        # 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

        # Dispose of the BluetoothLEDevice and see that the connection
        # status is now Disconnected.
        self._requester.Dispose()
        is_disconnected = (self._requester.ConnectionStatus ==
                           BluetoothConnectionStatus.Disconnected)
        self._requester = None

        # Set device info to None as well.
        self._device_info = None

        # Finally, dispose of the Bleak Bridge as well.
        self._bridge.Dispose()
        self._bridge = None

        return is_disconnected

    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 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(
                        callback, asyncio.get_event_loop()))
            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)
                del callback

            return GattCommunicationStatus.AccessDenied

        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.
            if characteristic.handle in self._notification_callbacks:
                callback = self._notification_callbacks.pop(
                    characteristic.handle)
                self._bridge.RemoveValueChangedCallback(characteristic_obj)
                del 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,
        )

        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)
            del callback
Esempio n. 2
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. 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.
    """
    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)