diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index dec543cd..f6597e2f 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -17,6 +17,8 @@ jobs: exclude: - os: windows-latest python-version: 3.8 + - os: macos-latest + python-version: 3.5 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} diff --git a/.pyup.yml b/.pyup.yml new file mode 100644 index 00000000..d00892a9 --- /dev/null +++ b/.pyup.yml @@ -0,0 +1,8 @@ +update: all +pin: True +branch: develop +schedule: "every day" +search: True +assignees: + - hbldh +branch_prefix: pyup/ diff --git a/AUTHORS.rst b/AUTHORS.rst index 92d66ef9..2325f5e3 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -9,3 +9,5 @@ Development Lead Contributors ------------ + +* David Lechner diff --git a/HISTORY.rst b/CHANGELOG.rst similarity index 51% rename from HISTORY.rst rename to CHANGELOG.rst index e257ce14..1ec65873 100644 --- a/HISTORY.rst +++ b/CHANGELOG.rst @@ -1,34 +1,114 @@ -======= -History -======= - -0.6.4 (2020-05-20) ------------------- +========= +Changelog +========= + +All notable changes to this project will be documented in this file. + +The format is based on `Keep a Changelog `_, +and this project adheres to `Semantic Versioning `_. + + +`0.7.0`_ (2020-06-30) +--------------------- + +Added +~~~~~ + +* Better feedback of communication errors to user in .NET backend and implementing error details proposed in #174. +* Two devices example file to use for e.g. debugging. +* Detection/discovery callbacks in Core Bluetooth backend ``Scanner`` implemented. +* Characteristic handle printout in ``service_explorer.py``. +* Added scanning filters to .NET backend's ``discover`` method. + +Changed +~~~~~~~ + +* Replace ``NSRunLoop`` with dispatch queue in Core Bluetooth backend. This causes callbacks to be dispatched on a + background thread instead of on the main dispatch queue on the main thread. ``call_soon_threadsafe()`` is used to synchronize the events + with the event loop where the central manager was created. Fixes #111. +* The Central Manager is no longer global in the Core Bluetooth backend. A new one is created for each + ``BleakClient`` and ``BleakScanner``. Fixes #206 and #105. +* Merged #167 and reworked characteristics handling in Bleak. Implemented in all backends; + bleak now uses the characteristics' handle to identify and keep track of them. + Fixes #139 and #159 and allows connection for devices with multiple instances + of the same characteristic UUIDs. +* In ``requirements.txt`` and ``Pipfile``, the requirement on ``pythonnet`` + was bumped to version 2.5.1, which seems to solve issues described in #217 and #225. +* Renamed ``HISTORY.rst`` to ``CHANGELOG.rst`` and adopted + the `Keep a Changelog `_ format. +* Python 3.5 support from macOS is officially removed since pyobjc>6 requires 3.6+ +* Pin ``pyobjc`` dependencies to use at least version 6.2. (PR #194) +* Pin development requirement on `bump2version` to version 1.0.0 +* Added ``.pyup.yml`` for Pyup +* Using CBManagerState constants from pyobj instead of integers. + +Removed +~~~~~~~ + +* Removed documentation note about not using new event loops in Linux. This was fixed by #143. +* ``_central_manager_delegate_ready`` was removed in macOS backend. +* Removed the ``bleak.backends.bluez.utils.get_gatt_service_path`` method. It is not used by + bleak and possibly generates errors. + +Fixed +~~~~~ + +* Improved handling of the txdbus connection to avoid hanging of disconnection + clients in BlueZ backend. Fixes #216, #219 & #221. +* #150 hints at the device path not being possible to create as is done in the `get_device_object_path` method. + Now, we try to get it from BlueZ first. Otherwise, use the old fallback. +* Minor documentation errors corrected. +* ``CBManagerStatePoweredOn`` is now properly handled in Core Bluetooth. +* Device enumeration in ``discover``and ``Scanner`` corrected. Fixes #211 +* Updated documentation about scanning filters. +* Added workaround for ``isScanning`` attribute added in macOS 10.13. Fixes #234. + +`0.6.4`_ (2020-05-20) +--------------------- + +Fixed +~~~~~ * Fix for bumpversion usage -0.6.3 (2020-05-20) ------------------- +`0.6.3`_ (2020-05-20) +--------------------- + +Added +~~~~~ * Building and releasing from Github Actions -0.6.2 (2020-05-15) ------------------- +Removed +~~~~~~~ +* Building and releasing on Azure Pipelines + +`0.6.2`_ (2020-05-15) +--------------------- + +Added +~~~~~ +* Added ``disconnection_callback`` functionality for Core Bluetooth (#184 & #186) +* Added ``requirements.txt`` + +Fixed +~~~~~ * Better cleanup of Bluez notifications (#154) * Fix for ``read_gatt_char`` in Core Bluetooth (#177) * Fix for ``is_disconnected`` in Core Bluetooth (#187 & #185) -* Added ``disconnection_callback`` functionality for Core Bluetooth (#184 & #186) * Documentation fixes -* Added ``requirements.txt`` -0.6.1 (2020-03-09) ------------------- +`0.6.1`_ (2020-03-09) +--------------------- + +Fixed +~~~~~ * Including #156, lost notifications on macOS backend, which was accidentally missed on previous release. -0.6.0 (2020-03-09) ------------------- +`0.6.0`_ (2020-03-09) +--------------------- * New Scanner object to allow for async device scanning. * Updated ``txdbus`` requirement to version 1.1.1 (Merged #122) @@ -149,3 +229,12 @@ History ------------------ * Bleak created. + + +.. _Unreleased: https://github.com/hbldh/bleak/compare/v0.7.0...develop +.. _0.7.0: https://github.com/hbldh/bleak/compare/v0.7.0...v0.6.4 +.. _0.6.4: https://github.com/hbldh/bleak/compare/v0.6.3...v0.6.4 +.. _0.6.3: https://github.com/hbldh/bleak/compare/v0.6.2...v0.6.3 +.. _0.6.2: https://github.com/hbldh/bleak/compare/v0.6.1...v0.6.2 +.. _0.6.1: https://github.com/hbldh/bleak/compare/v0.6.0...v0.6.1 +.. _0.6.0: https://github.com/hbldh/bleak/compare/v0.5.1...v0.6.0 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index e0ecbe2c..d58c1e95 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -102,12 +102,9 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 3.5+. - -Tips ----- - -To run a subset of tests:: - - $ py.test tests.test_bleak +3. The pull request should work for Python 3.5+ on the following platforms: + - Windows 10, version 16299 (Fall Creators Update) and greater + - Linux distributions with BlueZ >= 5.43 + - OS X / macOS >= 10.11 +4. Feel free to add your name to the ``AUTHORS.rst`` in the root of the project! diff --git a/LICENSE b/LICENSE index 01acb899..3b7ffa12 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License -Copyright (c) 2019, Henrik Blidh +Copyright (c) 2020, Henrik Blidh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Pipfile b/Pipfile index 2458ec7a..9bacd05b 100644 --- a/Pipfile +++ b/Pipfile @@ -5,9 +5,10 @@ name = "pypi" [packages] txdbus = {version = ">=1.1.1", sys_platform = "== 'linux'"} -pyobjc = {version = "*", sys_platform = "== 'darwin'"} -pyobjc-framework-CoreBluetooth = {version = "*", sys_platform = "== 'darwin'"} -pythonnet = {version = ">=2.3.0", sys_platform = "== 'win32'"} +pyobjc-core = {version = ">=6.2", sys_platform = "== 'darwin'"} +pyobjc-framework-CoreBluetooth = {version = ">=6.2", sys_platform = "== 'darwin'"} +pyobjc-framework-libdispatch = {version = ">=6.2", sys_platform = "== 'darwin'"} +pythonnet = {version = ">=2.5.1", sys_platform = "== 'win32'"} [dev-packages] pytest = "*" diff --git a/README.rst b/README.rst index bffac399..61eab297 100644 --- a/README.rst +++ b/README.rst @@ -13,9 +13,6 @@ bleak :target: https://github.com/hbldh/bleak/actions?query=workflow%3A%22Build+and+Test%22 :alt: Build and Test -.. image:: https://dev.azure.com/hbldh/github/_apis/build/status/hbldh.bleak?branchName=master - :target: https://dev.azure.com/hbldh/github/_build/latest?definitionId=4&branchName=master - .. image:: https://img.shields.io/pypi/v/bleak.svg :target: https://pypi.python.org/pypi/bleak diff --git a/bleak/__version__.py b/bleak/__version__.py index 732e0893..e0abfd00 100644 --- a/bleak/__version__.py +++ b/bleak/__version__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = "0.6.4" +__version__ = "0.7.0" diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py index bd8b2e4d..614f5e03 100644 --- a/bleak/backends/bluezdbus/characteristic.py +++ b/bleak/backends/bluezdbus/characteristic.py @@ -1,3 +1,4 @@ +import re from uuid import UUID from typing import Union, List @@ -25,6 +26,8 @@ # "authorize" } +_handle_regex = re.compile('/char([0-9a-fA-F]*)') + class BleakGATTCharacteristicBlueZDBus(BleakGATTCharacteristic): """GATT Characteristic implementation for the BlueZ DBus backend""" @@ -35,11 +38,28 @@ def __init__(self, obj: dict, object_path: str, service_uuid: str): self.__path = object_path self.__service_uuid = service_uuid + # The `Handle` attribute is added in BlueZ Release 5.51. Empirically, + # it seems to hold true that the "/charYYYY" that is at the end of the + # DBUS path actually is the desired handle. Using regex to extract + # that and using as handle, since handle is mostly used for keeping + # track of characteristics (internally in bleak anyway). + self._handle = self.obj.get("Handle") + if not self._handle: + _handle_from_path = _handle_regex.search(self.path) + if _handle_from_path: + self._handle = int(_handle_from_path.groups()[0], 16) + self._handle = int(self._handle) + @property def service_uuid(self) -> str: """The uuid of the Service containing this characteristic""" return self.__service_uuid + @property + def handle(self) -> int: + """The handle of this characteristic""" + return self._handle + @property def uuid(self) -> str: """The uuid of this characteristic""" @@ -65,11 +85,14 @@ def descriptors(self) -> List: return self.__descriptors def get_descriptor( - self, _uuid: Union[str, UUID] + self, specifier: Union[int, str, UUID] ) -> Union[BleakGATTDescriptor, None]: - """Get a descriptor by UUID""" + """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" try: - return next(filter(lambda x: x.uuid == _uuid, self.descriptors)) + if isinstance(specifier, int): + return next(filter(lambda x: x.handle == specifier, self.descriptors)) + else: + return next(filter(lambda x: x.uuid == str(specifier), self.descriptors)) except StopIteration: return None diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 4ab43c10..43d578a0 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -6,11 +6,11 @@ import subprocess import uuid from asyncio import Future -from asyncio.events import AbstractEventLoop from functools import wraps, partial from typing import Callable, Any, Union from bleak.backends.service import BleakGATTServiceCollection +from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.exc import BleakError from bleak.backends.client import BaseBleakClient from bleak.backends.bluezdbus import defs, signals, utils, get_reactor @@ -103,11 +103,21 @@ async def connect(self, **kwargs) -> bool: 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) - await discover(timeout=timeout, device=self.device, loop=self.loop) + 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) @@ -115,8 +125,6 @@ async def connect(self, **kwargs) -> bool: self._bus = await txdbus_connect(self._reactor, busAddress="system").asFuture( self.loop ) - # TODO: Handle path errors from txdbus/dbus - self._device_path = get_device_object_path(self.device, self.address) def _services_resolved_callback(message): iface, changed, invalidated = message.body @@ -202,6 +210,11 @@ async def _cleanup_dbus_resources(self) -> None: 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: """ @@ -219,6 +232,11 @@ async def disconnect(self) -> bool: """ 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() @@ -234,11 +252,13 @@ async def disconnect(self) -> bool: except Exception as e: logger.error("Attempt to disconnect device failed: {0}".format(e)) - # See if it has been disconnected. 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: @@ -326,7 +346,7 @@ async def get_services(self) -> BleakGATTServiceCollection: ) ) self.services.add_descriptor( - BleakGATTDescriptorBlueZDBus(desc, object_path, _characteristic[0].uuid) + BleakGATTDescriptorBlueZDBus(desc, object_path, _characteristic[0].uuid, int(_characteristic[0].handle)) ) self._services_resolved = True @@ -334,21 +354,27 @@ async def get_services(self) -> BleakGATTServiceCollection: # IO methods - async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], **kwargs) -> bytearray: + 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: - _uuid (str or UUID): The uuid of the characteristics to read from. + 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. """ - characteristic = self.services.get_characteristic(str(_uuid)) + 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 _uuid == "00002a19-0000-1000-8000-00805f9b34fb" and ( + 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( @@ -358,11 +384,11 @@ async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], **kwargs) -> bytear value = bytearray([props.get("Percentage", "")]) logger.debug( "Read Battery Level {0} | {1}: {2}".format( - _uuid, self._device_path, value + char_specifier, self._device_path, value ) ) return value - if str(_uuid) == "00002a00-0000-1000-8000-00805f9b34fb" and ( + 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( @@ -372,13 +398,13 @@ async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], **kwargs) -> bytear value = bytearray(props.get("Name", "").encode("ascii")) logger.debug( "Read Device Name {0} | {1}: {2}".format( - _uuid, self._device_path, value + char_specifier, self._device_path, value ) ) return value raise BleakError( - "Characteristic with UUID {0} could not be found!".format(_uuid) + "Characteristic with UUID {0} could not be found!".format(char_specifier) ) value = bytearray( @@ -395,7 +421,7 @@ async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], **kwargs) -> bytear logger.debug( "Read Characteristic {0} | {1}: {2}".format( - _uuid, characteristic.path, value + characteristic.uuid, characteristic.path, value ) ) return value @@ -432,13 +458,13 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: return value async def write_gatt_char( - self, _uuid: Union[str, uuid.UUID], data: bytearray, response: bool = False + 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 - ttps://git.kernel.org/pub/scm/bluetooth/bluez.git/commit?id=fa9473bcc48417d69cc9ef81d41a72b18e34a55a + 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 @@ -446,21 +472,26 @@ async def write_gatt_char( of Bluez, it is not possible to "Write without response". Args: - _uuid (str or UUID): The uuid of the characteristics to write to. + 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`. """ - characteristic = self.services.get_characteristic(str(_uuid)) - if not characteristic: - raise BleakError("Characteristic {0} was not found!".format(_uuid)) + 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(_uuid) + "Characteristic %s does not support write operations!" % str(characteristic.uuid) ) if not response and "write-without-response" not in characteristic.properties: response = True @@ -473,7 +504,7 @@ async def write_gatt_char( response = False logger.warning( "Characteristic %s does not support Write with response. Trying without..." - % str(_uuid) + % str(characteristic.uuid) ) # See docstring for details about this handling. @@ -508,7 +539,7 @@ async def write_gatt_char( logger.debug( "Write Characteristic {0} | {1}: {2}".format( - _uuid, characteristic.path, data + characteristic.uuid, characteristic.path, data ) ) @@ -539,7 +570,7 @@ async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None: async def start_notify( self, - _uuid: Union[str, uuid.UUID], + char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], callback: Callable[[str, Any], Any], **kwargs ) -> None: @@ -555,7 +586,9 @@ def callback(sender, data): client.start_notify(char_uuid, callback) Args: - _uuid (str or UUID): The uuid of the characteristics to start notification on. + 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: @@ -564,22 +597,26 @@ def callback(sender, data): """ _wrap = kwargs.get("notification_wrapper", True) - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid) == "00002a19-0000-1000-8000-00805f9b34fb" and ( + 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(_uuid) + "possible in BlueZ >= 5.48. Use regular read instead.".format(char_specifier) ) raise BleakError( - "Characteristic with UUID {0} could not be found!".format(_uuid) + "Characteristic with UUID {0} could not be found!".format(char_specifier) ) await self._bus.callRemote( characteristic.path, @@ -604,18 +641,24 @@ def callback(sender, data): callback, self._char_path_to_uuid ) # noqa | E123 error in flake8... - self._subscriptions.append(str(_uuid)) + self._subscriptions.append(characteristic.handle) - async def stop_notify(self, _uuid: Union[str, uuid.UUID]) -> None: + async def stop_notify(self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID]) -> None: """Deactivate notification/indication on a specified characteristic. Args: - _uuid: The characteristic to stop notifying/indicating on. + 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. """ - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {} not found!".format(char_specifier)) + await self._bus.callRemote( characteristic.path, "StopNotify", @@ -627,25 +670,31 @@ async def stop_notify(self, _uuid: Union[str, uuid.UUID]) -> None: ).asFuture(self.loop) self._notification_callbacks.pop(characteristic.path, None) - self._subscriptions.remove(str(_uuid)) + self._subscriptions.remove(characteristic.handle) # DBUS introspection method for characteristics. - async def get_all_for_characteristic(self, _uuid: Union[str, uuid.UUID]) -> dict: + 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: - _uuid: The characteristic to get properties for. + 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 """ - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {} not found!".format(char_specifier)) + out = await self._bus.callRemote( characteristic.path, "GetAll", diff --git a/bleak/backends/bluezdbus/descriptor.py b/bleak/backends/bluezdbus/descriptor.py index 49735e62..5a7d09f7 100644 --- a/bleak/backends/bluezdbus/descriptor.py +++ b/bleak/backends/bluezdbus/descriptor.py @@ -4,12 +4,18 @@ class BleakGATTDescriptorBlueZDBus(BleakGATTDescriptor): """GATT Descriptor implementation for BlueZ DBus backend""" - def __init__(self, obj: dict, object_path: str, characteristic_uuid: str): + def __init__(self, obj: dict, object_path: str, characteristic_uuid: str, characteristic_handle: int): super(BleakGATTDescriptorBlueZDBus, self).__init__(obj) self.__path = object_path self.__characteristic_uuid = characteristic_uuid + self.__characteristic_handle = characteristic_handle self.__handle = int(self.path.split("/")[-1].replace("desc", ""), 16) + @property + def characteristic_handle(self) -> int: + """handle for the characteristic that this descriptor belongs to""" + return self.__characteristic_handle + @property def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" diff --git a/bleak/backends/bluezdbus/discovery.py b/bleak/backends/bluezdbus/discovery.py index e0c5b754..d31dc294 100644 --- a/bleak/backends/bluezdbus/discovery.py +++ b/bleak/backends/bluezdbus/discovery.py @@ -54,7 +54,7 @@ def _device_info(path, props): async def discover(timeout=5.0, loop=None, **kwargs): """Discover nearby Bluetooth Low Energy devices. - For possible values for `filter`, see the parameters to the + For possible values for `filters`, see the parameters to the ``SetDiscoveryFilter`` method in the `BlueZ docs `_ diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 94b30bf8..50312c50 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -59,10 +59,16 @@ def _device_info(path, props): class BleakScannerBlueZDBus(BaseBleakScanner): """The native Linux Bleak BLE Scanner. + For possible values for `filters`, see the parameters to the + ``SetDiscoveryFilter`` method in the `BlueZ docs + `_ + Args: loop (asyncio.events.AbstractEventLoop): The event loop to use. Keyword Args: + device (str): Bluetooth device to use for discovery. + filters (dict): A dict of filters to be applied on discovery. """ @@ -165,6 +171,16 @@ async def stop(self): self._reactor = None async def set_scanning_filter(self, **kwargs): + """Sets OS level scanning filters for the BleakScanner. + + For possible values for `filters`, see the parameters to the + ``SetDiscoveryFilter`` method in the `BlueZ docs + `_ + + Keyword Args: + filters (dict): A dict of filters to be applied on discovery. + + """ self._filters = kwargs.get("filters", {}) self._filters["Transport"] = "le" diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py index d26c0fe8..64078606 100644 --- a/bleak/backends/characteristic.py +++ b/bleak/backends/characteristic.py @@ -43,6 +43,12 @@ def service_uuid(self) -> str: """The UUID of the Service containing this characteristic""" raise NotImplementedError() + @property + @abc.abstractmethod + def handle(self) -> int: + """The handle for this characteristic""" + raise NotImplementedError() + @property @abc.abstractmethod def uuid(self) -> str: @@ -68,8 +74,8 @@ def descriptors(self) -> List: raise NotImplementedError() @abc.abstractmethod - def get_descriptor(self, _uuid: Union[str, UUID]) -> Union[BleakGATTDescriptor, None]: - """Get a descriptor by UUID""" + def get_descriptor(self, specifier: Union[int, str, UUID]) -> Union[BleakGATTDescriptor, None]: + """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" raise NotImplementedError() @abc.abstractmethod diff --git a/bleak/backends/client.py b/bleak/backends/client.py index a92d0ec7..83fb7c8d 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -11,6 +11,7 @@ from typing import Callable, Any, Union from bleak.backends.service import BleakGATTServiceCollection +from bleak.backends.characteristic import BleakGATTCharacteristic class BaseBleakClient(abc.ABC): @@ -122,11 +123,13 @@ async def get_services(self) -> BleakGATTServiceCollection: # I/O methods @abc.abstractmethod - async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], **kwargs) -> bytearray: + 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: - _uuid (str or UUID): The uuid of the characteristics to read from. + 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. @@ -149,12 +152,14 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray: @abc.abstractmethod async def write_gatt_char( - self, _uuid: Union[str, uuid.UUID], data: bytearray, response: bool = False + self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], data: bytearray, response: bool = False ) -> None: """Perform a write operation on the specified GATT characteristic. Args: - _uuid (str or UUID): The uuid of the characteristics to write to. + 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`. @@ -174,7 +179,7 @@ async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None: @abc.abstractmethod async def start_notify( - self, _uuid: Union[str, uuid.UUID], callback: Callable[[str, Any], Any], **kwargs + self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], callback: Callable[[str, Any], Any], **kwargs ) -> None: """Activate notifications/indications on a characteristic. @@ -188,18 +193,22 @@ def callback(sender, data): client.start_notify(char_uuid, callback) Args: - _uuid (str or UUID): The uuid of the characteristics to start notification/indication on. + 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. """ raise NotImplementedError() @abc.abstractmethod - async def stop_notify(self, _uuid: Union[str, uuid.UUID]) -> None: + async def stop_notify(self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID]) -> None: """Deactivate notification/indication on a specified characteristic. Args: - _uuid: The characteristic to stop notifying/indicating on. + 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. """ raise NotImplementedError() diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 040434aa..add08e12 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -1,6 +1,6 @@ """ CentralManagerDelegate will implement the CBCentralManagerDelegate protocol to -manage CoreBluetooth serivces and resources on the Central End +manage CoreBluetooth services and resources on the Central End Created on June, 25 2019 by kevincar @@ -8,10 +8,19 @@ import asyncio import logging +import platform from enum import Enum from typing import List import objc +from CoreBluetooth import ( + CBManagerStateUnknown, + CBManagerStateResetting, + CBManagerStateUnsupported, + CBManagerStateUnauthorized, + CBManagerStatePoweredOff, + CBManagerStatePoweredOn, +) from Foundation import ( NSObject, CBCentralManager, @@ -22,6 +31,7 @@ NSNumber, NSError, ) +from libdispatch import dispatch_queue_create, DISPATCH_QUEUE_SERIAL from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate from bleak.backends.corebluetooth.device import BLEDeviceCoreBluetooth @@ -31,6 +41,9 @@ CBCentralManagerDelegate = objc.protocolNamed("CBCentralManagerDelegate") +_mac_version = list(map(int, platform.mac_ver()[0].split('.'))) +_IS_PRE_10_13 = _mac_version[0] == 10 and _mac_version[1] < 13 + class CMDConnectionState(Enum): DISCONNECTED = 0 @@ -50,47 +63,47 @@ def init(self): if self is None: return None - self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_( - self, None - ) - + self.event_loop = asyncio.get_event_loop() self.connected_peripheral_delegate = None self.connected_peripheral = None self._connection_state = CMDConnectionState.DISCONNECTED - self.ready = False + self.powered_on_event = asyncio.Event() self.devices = {} + self.callbacks = {} self.disconnected_callback = None if not self.compliant(): logger.warning("CentralManagerDelegate is not compliant") + self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_( + self, dispatch_queue_create(b"bleak.corebluetooth", DISPATCH_QUEUE_SERIAL) + ) + return self # User defined functions def compliant(self): - """Determins whether the class adheres to the CBCentralManagerDelegate protocol""" + """Determines whether the class adheres to the CBCentralManagerDelegate protocol""" return CentralManagerDelegate.pyobjc_classMethods.conformsToProtocol_( CBCentralManagerDelegate ) - @property - def enabled(self): - """Check if the bluetooth device is on and running""" - return self.central_manager.state() == 5 - @property def isConnected(self) -> bool: return self._connection_state == CMDConnectionState.CONNECTED - async def is_ready(self): - """is_ready allows an asynchronous way to wait and ensure the - CentralManager has processed it's inputs before moving on""" - while not self.ready: - await asyncio.sleep(0) - return self.ready + @objc.python_method + async def wait_for_powered_on(self, timeout: float): + """ + Waits for state to be CBManagerStatePoweredOn. This must be done before + attempting to do anything else. + + Throws asyncio.TimeoutError if power on is not detected before timeout. + """ + await asyncio.wait_for(self.powered_on_event.wait(), timeout) async def scanForPeripherals_(self, scan_options) -> List[CBPeripheral]: """ @@ -118,8 +131,16 @@ async def scanForPeripherals_(self, scan_options) -> List[CBPeripheral]: await asyncio.sleep(timeout) self.central_manager.stopScan() - while self.central_manager.isScanning(): + + # Wait a while to allow central manager to stop scanning. + # The `isScanning` attribute is added in macOS 10.13, so before that + # just waiting some will have to do. In 10.13+ I have never seen + # bleak enter the while-loop, so this fix is most probably safe. + if _IS_PRE_10_13: await asyncio.sleep(0.1) + else: + while self.central_manager.isScanning(): + await asyncio.sleep(0.1) return [] @@ -145,23 +166,35 @@ async def disconnect(self) -> bool: # Protocol Functions - def centralManagerDidUpdateState_(self, centralManager): - if centralManager.state() == 0: + @objc.python_method + def did_update_state(self, centralManager): + if centralManager.state() == CBManagerStateUnknown: logger.debug("Cannot detect bluetooth device") - elif centralManager.state() == 1: + elif centralManager.state() == CBManagerStateResetting: logger.debug("Bluetooth is resetting") - elif centralManager.state() == 2: + elif centralManager.state() == CBManagerStateUnsupported: logger.debug("Bluetooth is unsupported") - elif centralManager.state() == 3: + elif centralManager.state() == CBManagerStateUnauthorized: logger.debug("Bluetooth is unauthorized") - elif centralManager.state() == 4: + elif centralManager.state() == CBManagerStatePoweredOff: logger.debug("Bluetooth powered off") - elif centralManager.state() == 5: + elif centralManager.state() == CBManagerStatePoweredOn: logger.debug("Bluetooth powered on") - self.ready = True + if centralManager.state() == CBManagerStatePoweredOn: + self.powered_on_event.set() + else: + self.powered_on_event.clear() - def centralManager_didDiscoverPeripheral_advertisementData_RSSI_( + def centralManagerDidUpdateState_(self, centralManager): + logger.debug("centralManagerDidUpdateState_") + self.event_loop.call_soon_threadsafe( + self.did_update_state, + centralManager, + ) + + @objc.python_method + def did_discover_peripheral( self, central: CBCentralManager, peripheral: CBPeripheral, @@ -194,10 +227,31 @@ def centralManager_didDiscoverPeripheral_advertisementData_RSSI_( device._rssi = float(RSSI) device._update(advertisementData) + for callback in self.callbacks.values(): + if callback: + callback(peripheral, advertisementData, RSSI) + logger.debug("Discovered device {}: {} @ RSSI: {} (kCBAdvData {})".format( uuid_string, device.name, RSSI, advertisementData.keys())) - def centralManager_didConnectPeripheral_(self, central, peripheral): + def centralManager_didDiscoverPeripheral_advertisementData_RSSI_( + self, + central: CBCentralManager, + peripheral: CBPeripheral, + advertisementData: NSDictionary, + RSSI: NSNumber, + ): + logger.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_") + self.event_loop.call_soon_threadsafe( + self.did_discover_peripheral, + central, + peripheral, + advertisementData, + RSSI, + ) + + @objc.python_method + def did_connect_peripheral(self, central, peripheral): logger.debug( "Successfully connected to device uuid {}".format( peripheral.identifier().UUIDString() @@ -207,7 +261,16 @@ def centralManager_didConnectPeripheral_(self, central, peripheral): self.connected_peripheral_delegate = peripheralDelegate self._connection_state = CMDConnectionState.CONNECTED - def centralManager_didFailToConnectPeripheral_error_( + def centralManager_didConnectPeripheral_(self, central, peripheral): + logger.debug("centralManager_didConnectPeripheral_") + self.event_loop.call_soon_threadsafe( + self.did_connect_peripheral, + central, + peripheral, + ) + + @objc.python_method + def did_fail_to_connect_peripheral( self, centralManager: CBCentralManager, peripheral: CBPeripheral, error: NSError ): logger.debug( @@ -217,7 +280,19 @@ def centralManager_didFailToConnectPeripheral_error_( ) self._connection_state = CMDConnectionState.DISCONNECTED - def centralManager_didDisconnectPeripheral_error_( + def centralManager_didFailToConnectPeripheral_error_( + self, centralManager: CBCentralManager, peripheral: CBPeripheral, error: NSError + ): + logger.debug("centralManager_didFailToConnectPeripheral_error_") + self.event_loop.call_soon_threadsafe( + self.did_fail_to_connect_peripheral, + centralManager, + peripheral, + error, + ) + + @objc.python_method + def did_disconnect_peripheral( self, central: CBCentralManager, peripheral: CBPeripheral, error: NSError ): logger.debug("Peripheral Device disconnected!") @@ -226,8 +301,18 @@ def centralManager_didDisconnectPeripheral_error_( if self.disconnected_callback is not None: self.disconnected_callback() + def centralManager_didDisconnectPeripheral_error_( + self, central: CBCentralManager, peripheral: CBPeripheral, error: NSError + ): + logger.debug("centralManager_didDisconnectPeripheral_error_") + self.event_loop.call_soon_threadsafe( + self.did_disconnect_peripheral, + central, + peripheral, + error, + ) + def string2uuid(uuid_str: str) -> CBUUID: """Convert a string to a uuid""" return CBUUID.UUIDWithString_(uuid_str) - diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index 907588ee..0f5aad4b 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -58,6 +58,7 @@ def initWithPeripheral_(self, peripheral: CBPeripheral): self.peripheral = peripheral self.peripheral.setDelegate_(self) + self._event_loop = asyncio.get_event_loop() self._services_discovered_event = asyncio.Event() self._service_characteristic_discovered_events = _EventDict() @@ -78,7 +79,7 @@ def initWithPeripheral_(self, peripheral: CBPeripheral): return self def compliant(self): - """Determins whether the class adheres to the CBCentralManagerDelegate protocol""" + """Determines whether the class adheres to the CBPeripheralDelegate protocol""" return PeripheralDelegate.pyobjc_classMethods.conformsToProtocol_( CBPeripheralDelegate ) @@ -208,7 +209,9 @@ async def stopNotify_(self, characteristic: CBCharacteristic) -> bool: return True # Protocol Functions - def peripheral_didDiscoverServices_( + + @objc.python_method + def did_discover_services( self, peripheral: CBPeripheral, error: NSError ) -> None: if error is not None: @@ -217,7 +220,18 @@ def peripheral_didDiscoverServices_( logger.debug("Services discovered") self._services_discovered_event.set() - def peripheral_didDiscoverCharacteristicsForService_error_( + def peripheral_didDiscoverServices_( + self, peripheral: CBPeripheral, error: NSError + ) -> None: + logger.debug("peripheral_didDiscoverServices_") + self._event_loop.call_soon_threadsafe( + self.did_discover_services, + peripheral, + error, + ) + + @objc.python_method + def did_discover_characteristics_for_service( self, peripheral: CBPeripheral, service: CBService, error: NSError ): sUUID = service.UUID().UUIDString() @@ -233,7 +247,19 @@ def peripheral_didDiscoverCharacteristicsForService_error_( else: logger.debug("Unexpected event didDiscoverCharacteristicsForService") - def peripheral_didDiscoverDescriptorsForCharacteristic_error_( + def peripheral_didDiscoverCharacteristicsForService_error_( + self, peripheral: CBPeripheral, service: CBService, error: NSError + ): + logger.debug("peripheral_didDiscoverCharacteristicsForService_error_") + self._event_loop.call_soon_threadsafe( + self.did_discover_characteristics_for_service, + peripheral, + service, + error, + ) + + @objc.python_method + def did_discover_descriptors_for_characteristic( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError ): cUUID = characteristic.UUID().UUIDString() @@ -251,7 +277,19 @@ def peripheral_didDiscoverDescriptorsForCharacteristic_error_( else: logger.warning("Unexpected event didDiscoverDescriptorsForCharacteristic") - def peripheral_didUpdateValueForCharacteristic_error_( + def peripheral_didDiscoverDescriptorsForCharacteristic_error_( + self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError + ): + logger.debug("peripheral_didDiscoverDescriptorsForCharacteristic_error_") + self._event_loop.call_soon_threadsafe( + self.did_discover_descriptors_for_characteristic, + peripheral, + characteristic, + error, + ) + + @objc.python_method + def did_update_value_for_characteristic( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError ): cUUID = characteristic.UUID().UUIDString() @@ -272,7 +310,19 @@ def peripheral_didUpdateValueForCharacteristic_error_( # only expected on read pass - def peripheral_didUpdateValueForDescriptor_error_( + def peripheral_didUpdateValueForCharacteristic_error_( + self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError + ): + logger.debug("peripheral_didUpdateValueForCharacteristic_error_") + self._event_loop.call_soon_threadsafe( + self.did_update_value_for_characteristic, + peripheral, + characteristic, + error, + ) + + @objc.python_method + def did_update_value_for_descriptor( self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError ): dUUID = descriptor.UUID().UUIDString() @@ -288,7 +338,19 @@ def peripheral_didUpdateValueForDescriptor_error_( else: logger.warning("Unexpected event didUpdateValueForDescriptor") - def peripheral_didWriteValueForCharacteristic_error_( + def peripheral_didUpdateValueForDescriptor_error_( + self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError + ): + logger.debug("peripheral_didUpdateValueForDescriptor_error_") + self._event_loop.call_soon_threadsafe( + self.did_update_value_for_descriptor, + peripheral, + descriptor, + error, + ) + + @objc.python_method + def did_write_value_for_characteristic( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError ): cUUID = characteristic.UUID().UUIDString() @@ -305,7 +367,19 @@ def peripheral_didWriteValueForCharacteristic_error_( # event only expected on write with response pass - def peripheral_didWriteValueForDescriptor_error_( + def peripheral_didWriteValueForCharacteristic_error_( + self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError + ): + logger.debug("peripheral_didWriteValueForCharacteristic_error_") + self._event_loop.call_soon_threadsafe( + self.did_write_value_for_characteristic, + peripheral, + characteristic, + error, + ) + + @objc.python_method + def did_write_value_for_descriptor( self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError ): dUUID = descriptor.UUID().UUIDString() @@ -319,7 +393,19 @@ def peripheral_didWriteValueForDescriptor_error_( else: logger.warning("Unexpected event didWriteValueForDescriptor") - def peripheral_didUpdateNotificationStateForCharacteristic_error_( + def peripheral_didWriteValueForDescriptor_error_( + self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError + ): + logger.debug("peripheral_didWriteValueForDescriptor_error_") + self._event_loop.call_soon_threadsafe( + self.did_write_value_for_descriptor, + peripheral, + descriptor, + error, + ) + + @objc.python_method + def did_update_notification_for_characteristic( self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError ): cUUID = characteristic.UUID().UUIDString() @@ -339,3 +425,13 @@ def peripheral_didUpdateNotificationStateForCharacteristic_error_( "Unexpected event didUpdateNotificationStateForCharacteristic" ) + def peripheral_didUpdateNotificationStateForCharacteristic_error_( + self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError + ): + logger.debug("peripheral_didUpdateNotificationStateForCharacteristic_error_") + self._event_loop.call_soon_threadsafe( + self.did_update_notification_for_characteristic, + peripheral, + characteristic, + error, + ) diff --git a/bleak/backends/corebluetooth/__init__.py b/bleak/backends/corebluetooth/__init__.py index dd4ef3bc..ed160a3d 100644 --- a/bleak/backends/corebluetooth/__init__.py +++ b/bleak/backends/corebluetooth/__init__.py @@ -6,47 +6,6 @@ """ -import asyncio -from Foundation import NSDate, NSDefaultRunLoopMode, NSRunLoop -from .CentralManagerDelegate import CentralManagerDelegate import objc objc.options.verbose = True - - -class Application: - """ - This is a temporary application class responsible for running the NSRunLoop - so that events within CoreBluetooth are appropriately handled - """ - - ns_run_loop_done = False - ns_run_loop_interval = 0.001 - - def __init__(self): - self.main_loop = asyncio.get_event_loop() - self.main_loop.create_task(self._handle_nsrunloop()) - self.main_loop.create_task(self._central_manager_delegate_ready()) - - self.nsrunloop = NSRunLoop.currentRunLoop() - - self.central_manager_delegate = CentralManagerDelegate.alloc().init() - - def __del__(self): - self.ns_run_loop_done = True - - async def _handle_nsrunloop(self): - while not self.ns_run_loop_done: - time_interval = NSDate.alloc().initWithTimeIntervalSinceNow_( - self.ns_run_loop_interval - ) - self.nsrunloop.runMode_beforeDate_(NSDefaultRunLoopMode, time_interval) - await asyncio.sleep(0) - - async def _central_manager_delegate_ready(self): - await self.central_manager_delegate.is_ready() - - -# Restructure this later: Global isn't the prettiest way of doing this... -global CBAPP -CBAPP = Application() diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py index 973c0b97..22bd72fc 100644 --- a/bleak/backends/corebluetooth/characteristic.py +++ b/bleak/backends/corebluetooth/characteristic.py @@ -12,7 +12,7 @@ from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.corebluetooth.descriptor import BleakGATTDescriptorCoreBluetooth from bleak.backends.descriptor import BleakGATTDescriptor - +from bleak.backends.corebluetooth.utils import cb_uuid_to_str class CBChacteristicProperties(Enum): BROADCAST = 0x1 @@ -66,6 +66,8 @@ def __init__(self, obj: CBCharacteristic): for v in [2 ** n for n in range(10)] if (self.obj.properties() & v) ] + uuid_string = self.obj.UUID().UUIDString() + self._uuid = cb_uuid_to_str(uuid_string) def __str__(self): return "{0}: {1}".format(self.uuid, self.description) @@ -75,10 +77,15 @@ def service_uuid(self) -> str: """The uuid of the Service containing this characteristic""" return self.obj.service().UUID().UUIDString() + @property + def handle(self) -> int: + """Integer handle for this characteristic""" + return int(self.obj.handle()) + @property def uuid(self) -> str: """The uuid of this characteristic""" - return self.obj.UUID().UUIDString() + return self._uuid @property def description(self) -> str: @@ -96,10 +103,13 @@ def descriptors(self) -> List[BleakGATTDescriptorCoreBluetooth]: """List of descriptors for this service""" return self.__descriptors - def get_descriptor(self, _uuid) -> Union[BleakGATTDescriptorCoreBluetooth, None]: - """Get a descriptor by UUID""" + def get_descriptor(self, specifier) -> Union[BleakGATTDescriptorCoreBluetooth, None]: + """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" try: - return next(filter(lambda x: x.uuid == _uuid, self.descriptors)) + if isinstance(specifier, int): + return next(filter(lambda x: x.handle == specifier, self.descriptors)) + else: + return next(filter(lambda x: x.uuid == str(specifier), self.descriptors)) except StopIteration: return None diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 59fea6ed..4c314812 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -13,7 +13,6 @@ from CoreBluetooth import CBCharacteristicWriteWithResponse, CBCharacteristicWriteWithoutResponse from bleak.backends.client import BaseBleakClient -from bleak.backends.corebluetooth import CBAPP as cbapp from bleak.backends.corebluetooth.characteristic import ( BleakGATTCharacteristicCoreBluetooth ) @@ -21,6 +20,8 @@ from bleak.backends.corebluetooth.discovery import discover from bleak.backends.corebluetooth.service import BleakGATTServiceCoreBluetooth from bleak.backends.service import BleakGATTServiceCollection +from bleak.backends.characteristic import BleakGATTCharacteristic + from bleak.exc import BleakError logger = logging.getLogger(__name__) @@ -76,7 +77,8 @@ async def connect(self, **kwargs) -> bool: logger.debug("Connecting to BLE device @ {}".format(self.address)) - await cbapp.central_manager_delegate.connect_(sought_device[0].details) + manager = self._device_info.manager().delegate() + await manager.connect_(sought_device[0].details) # Now get services await self.get_services() @@ -85,12 +87,15 @@ async def connect(self, **kwargs) -> bool: async def disconnect(self) -> bool: """Disconnect from the peripheral device""" - await cbapp.central_manager_delegate.disconnect() + 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""" - return cbapp.central_manager_delegate.isConnected + manager = self._device_info.manager().delegate() + return manager.isConnected def set_disconnected_callback( self, callback: Callable[[BaseBleakClient], None], **kwargs @@ -100,8 +105,9 @@ def set_disconnected_callback( callback: callback to be called on disconnection. """ + manager = self._device_info.manager().delegate() self._disconnected_callback = callback - cbapp.central_manager_delegate.disconnected_callback = self._disconnect_callback_client + manager.disconnected_callback = self._disconnect_callback_client def _disconnect_callback_client(self): """ @@ -124,8 +130,9 @@ async def get_services(self) -> BleakGATTServiceCollection: return self._services logger.debug("Retrieving services...") + manager = self._device_info.manager().delegate() services = ( - await cbapp.central_manager_delegate.connected_peripheral_delegate.discoverServices() + await manager.connected_peripheral_delegate.discoverServices() ) for service in services: @@ -133,7 +140,7 @@ async def get_services(self) -> BleakGATTServiceCollection: logger.debug( "Retrieving characteristics for service {}".format(serviceUUID) ) - characteristics = await cbapp.central_manager_delegate.connected_peripheral_delegate.discoverCharacteristics_( + characteristics = await manager.connected_peripheral_delegate.discoverCharacteristics_( service ) @@ -144,7 +151,7 @@ async def get_services(self) -> BleakGATTServiceCollection: logger.debug( "Retrieving descriptors for characteristic {}".format(cUUID) ) - descriptors = await cbapp.central_manager_delegate.connected_peripheral_delegate.discoverDescriptors_( + descriptors = await manager.connected_peripheral_delegate.discoverDescriptors_( characteristic ) @@ -154,18 +161,20 @@ async def get_services(self) -> BleakGATTServiceCollection: for descriptor in descriptors: self.services.add_descriptor( BleakGATTDescriptorCoreBluetooth( - descriptor, characteristic.UUID().UUIDString() + descriptor, characteristic.UUID().UUIDString(), int(characteristic.handle()) ) ) self._services_resolved = True self._services = services return self.services - async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], use_cached=False, **kwargs) -> bytearray: + 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: - _uuid (str or UUID): The uuid of the characteristics to read from. + 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`. @@ -173,16 +182,20 @@ async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], use_cached=False, * (bytearray) The read data. """ - _uuid = await self.get_appropriate_uuid(str(_uuid)) - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {} was not found!".format(char_specifier)) - output = await cbapp.central_manager_delegate.connected_peripheral_delegate.readCharacteristic_( + output = await manager.connected_peripheral_delegate.readCharacteristic_( characteristic.obj, use_cached=use_cached ) value = bytearray(output) - logger.debug("Read Characteristic {0} : {1}".format(_uuid, value)) + logger.debug("Read Characteristic {0} : {1}".format(characteristic.uuid, value)) return value async def read_gatt_descriptor( @@ -198,11 +211,13 @@ async def read_gatt_descriptor( 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 cbapp.central_manager_delegate.connected_peripheral_delegate.readDescriptor_( + output = await manager.connected_peripheral_delegate.readDescriptor_( descriptor.obj, use_cached=use_cached ) if isinstance( @@ -215,29 +230,35 @@ async def read_gatt_descriptor( return value async def write_gatt_char( - self, _uuid: Union[str, uuid.UUID], data: bytearray, response: bool = False + 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: - _uuid (str or UUID): The uuid of the characteristics to write to. + 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`. """ - _uuid = await self.get_appropriate_uuid(str(_uuid)) - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {} was not found!".format(char_specifier)) value = NSData.alloc().initWithBytes_length_(data, len(data)) - success = await cbapp.central_manager_delegate.connected_peripheral_delegate.writeCharacteristic_value_type_( + 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(_uuid, data)) + logger.debug("Write Characteristic {0} : {1}".format(characteristic.uuid, data)) else: raise BleakError( "Could not write value {0} to characteristic {1}: {2}".format( @@ -253,12 +274,14 @@ async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None: 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 cbapp.central_manager_delegate.connected_peripheral_delegate.writeDescriptor_value_( + success = await manager.connected_peripheral_delegate.writeDescriptor_value_( descriptor.obj, value ) if success: @@ -271,7 +294,7 @@ async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None: ) async def start_notify( - self, _uuid: Union[str, uuid.UUID], callback: Callable[[str, Any], Any], **kwargs + self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], callback: Callable[[str, Any], Any], **kwargs ) -> None: """Activate notifications/indications on a characteristic. @@ -285,16 +308,22 @@ def callback(sender, data): client.start_notify(char_uuid, callback) Args: - _uuid (str or UUID): The uuid of the characteristics to start notification/indication on. + 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. """ - _uuid = await self.get_appropriate_uuid(str(_uuid)) - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {0} not found!".format(char_specifier)) - success = await cbapp.central_manager_delegate.connected_peripheral_delegate.startNotify_cb_( + success = await manager.connected_peripheral_delegate.startNotify_cb_( characteristic.obj, callback ) if not success: @@ -304,51 +333,29 @@ def callback(sender, data): ) ) - async def stop_notify(self, _uuid: Union[str, uuid.UUID]) -> None: + async def stop_notify(self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID]) -> None: """Deactivate notification/indication on a specified characteristic. Args: - _uuid: The characteristic to stop notifying/indicating on. + 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. + """ - _uuid = await self.get_appropriate_uuid(str(_uuid)) - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {} not found!".format(char_specifier)) - success = await cbapp.central_manager_delegate.connected_peripheral_delegate.stopNotify_( + 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_appropriate_uuid(self, _uuid: str) -> str: - if len(_uuid) == 4: - return _uuid.upper() - - if await self.is_uuid_16bit_compatible(_uuid): - return _uuid[4:8].upper() - - return _uuid.upper() - - async def is_uuid_16bit_compatible(self, _uuid: str) -> bool: - test_uuid = "0000FFFF-0000-1000-8000-00805F9B34FB" - test_int = await self.convert_uuid_to_int(test_uuid) - uuid_int = await self.convert_uuid_to_int(_uuid) - result_int = uuid_int & test_int - return uuid_int == result_int - - async def convert_uuid_to_int(self, _uuid: str) -> int: - UUID_cb = CBUUID.alloc().initWithString_(_uuid) - UUID_data = UUID_cb.data() - UUID_bytes = UUID_data.getBytes_length_(None, len(UUID_data)) - UUID_int = int.from_bytes(UUID_bytes, byteorder="big") - return UUID_int - - async def convert_int_to_uuid(self, i: int) -> str: - UUID_bytes = i.to_bytes(length=16, byteorder="big") - UUID_data = NSData.alloc().initWithBytes_length_(UUID_bytes, len(UUID_bytes)) - UUID_cb = CBUUID.alloc().initWithData_(UUID_data) - return UUID_cb.UUIDString() diff --git a/bleak/backends/corebluetooth/descriptor.py b/bleak/backends/corebluetooth/descriptor.py index b2330bd8..de33e0b5 100644 --- a/bleak/backends/corebluetooth/descriptor.py +++ b/bleak/backends/corebluetooth/descriptor.py @@ -12,15 +12,20 @@ class BleakGATTDescriptorCoreBluetooth(BleakGATTDescriptor): """GATT Descriptor implementation for CoreBluetooth backend""" - def __init__(self, obj: CBDescriptor, characteristic_uuid: str): + def __init__(self, obj: CBDescriptor, characteristic_uuid: str, characteristic_handle: int): super(BleakGATTDescriptorCoreBluetooth, self).__init__(obj) - self.obj = obj self.__characteristic_uuid = characteristic_uuid + self.__characteristic_handle = characteristic_handle def __str__(self): return "{0}: (Handle: {1})".format(self.uuid, self.handle) + @property + def characteristic_handle(self) -> int: + """handle for the characteristic that this descriptor belongs to""" + return self.__characteristic_handle + @property def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" diff --git a/bleak/backends/corebluetooth/discovery.py b/bleak/backends/corebluetooth/discovery.py index 4f664f8f..3d80a444 100644 --- a/bleak/backends/corebluetooth/discovery.py +++ b/bleak/backends/corebluetooth/discovery.py @@ -11,7 +11,7 @@ from asyncio.events import AbstractEventLoop from typing import List -from bleak.backends.corebluetooth import CBAPP as cbapp +from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate from bleak.backends.device import BLEDevice from bleak.exc import BleakError @@ -27,19 +27,21 @@ async def discover( """ loop = loop if loop else asyncio.get_event_loop() - if not cbapp.central_manager_delegate.enabled: + manager = CentralManagerDelegate.alloc().init() + try: + await manager.wait_for_powered_on(0.1) + except asyncio.TimeoutError: raise BleakError("Bluetooth device is turned off") scan_options = {"timeout": timeout} - await cbapp.central_manager_delegate.scanForPeripherals_(scan_options) + await manager.scanForPeripherals_(scan_options) # CoreBluetooth doesn't explicitly use MAC addresses to identify peripheral # devices because private devices may obscure their MAC addresses. To cope # with this, CoreBluetooth utilizes UUIDs for each peripheral. We'll use # this for the BLEDevice address on macOS - - devices = cbapp.central_manager_delegate.devices + devices = manager.devices return list(devices.values()) diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index 8c51418a..64383771 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -5,7 +5,7 @@ from asyncio.events import AbstractEventLoop from typing import Callable, Any, Union, List -from bleak.backends.corebluetooth import CBAPP as cbapp +from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate from bleak.backends.device import BLEDevice from bleak.exc import BleakError from bleak.backends.scanner import BaseBleakScanner @@ -31,27 +31,41 @@ class BleakScannerCoreBluetooth(BaseBleakScanner): Keyword Args: timeout (double): The scanning timeout to be used, in case of missing - ``stopScan_`` metod. + ``stopScan_`` method. """ def __init__(self, loop: AbstractEventLoop = None, **kwargs): super(BleakScannerCoreBluetooth, self).__init__(loop, **kwargs) + self._callback = None + self._identifiers = None + self._manager = CentralManagerDelegate.alloc().init() + self._timeout = kwargs.get("timeout", 5.0) - if not cbapp.central_manager_delegate.enabled: + async def start(self): + try: + await self._manager.wait_for_powered_on(0.1) + except asyncio.TimeoutError: raise BleakError("Bluetooth device is turned off") - self._timeout = kwargs.get("timeout", 5.0) + self._identifiers = {} + + def callback(p, a, r): + self._identifiers[p.identifier()] = a + if self._callback: + self._callback((p, a, r)) + + self._manager.callbacks[id(self)] = callback - async def start(self): # TODO: Evaluate if newer macOS than 10.11 has stopScan. - if hasattr(cbapp.central_manager_delegate, "stopScan_"): - await cbapp.central_manager_delegate.scanForPeripherals_() + if hasattr(self._manager, "stopScan_"): + await self._manager.scanForPeripherals_() else: - await cbapp.central_manager_delegate.scanForPeripherals_({"timeout": self._timeout}) + await self._manager.scanForPeripherals_({"timeout": self._timeout}) async def stop(self): + del self._manager.callbacks[id(self)] try: - await cbapp.central_manager_delegate.stopScan_() + await self._manager.stopScan_() except Exception as e: logger.warning("stopScan method could not be called: {0}".format(e)) @@ -60,14 +74,16 @@ async def set_scanning_filter(self, **kwargs): async def get_discovered_devices(self) -> List[BLEDevice]: found = [] - peripherals = cbapp.central_manager_delegate.peripheral_list + peripherals = self._manager.central_manager.retrievePeripheralsWithIdentifiers_( + self._identifiers.keys(), + ) for i, peripheral in enumerate(peripherals): address = peripheral.identifier().UUIDString() name = peripheral.name() or "Unknown" details = peripheral - advertisementData = cbapp.central_manager_delegate.advertisement_data_list[i] + advertisementData = self._identifiers[peripheral.identifier()] manufacturer_binary_data = advertisementData.get("kCBAdvDataManufacturerData") manufacturer_data = {} if manufacturer_binary_data: @@ -92,7 +108,7 @@ async def get_discovered_devices(self) -> List[BLEDevice]: return found def register_detection_callback(self, callback: Callable): - raise NotImplementedError("This cannot be used in the macOS backend.") + self._callback = callback # macOS specific methods @@ -100,7 +116,7 @@ def register_detection_callback(self, callback: Callable): def is_scanning(self): # TODO: Evaluate if newer macOS than 10.11 has isScanning. try: - return cbapp.central_manager_delegate.isScanning_ + return self._manager.isScanning_ except: return None diff --git a/bleak/backends/corebluetooth/utils.py b/bleak/backends/corebluetooth/utils.py new file mode 100644 index 00000000..bb1c92b4 --- /dev/null +++ b/bleak/backends/corebluetooth/utils.py @@ -0,0 +1,34 @@ +from Foundation import NSData, CBUUID + + +def cb_uuid_to_str(_uuid: str) -> str: + if len(_uuid) == 4: + return '0000{0}-0000-1000-8000-00805f9b34fb'.format(_uuid.lower()) + # TODO: Evaluate if this is a necessary method... + # elif _is_uuid_16bit_compatible(_uuid): + # return _uuid[4:8].lower() + else: + return _uuid.lower() + + +def _is_uuid_16bit_compatible(_uuid: str) -> bool: + test_uuid = "0000FFFF-0000-1000-8000-00805F9B34FB" + test_int = _convert_uuid_to_int(test_uuid) + uuid_int = _convert_uuid_to_int(_uuid) + result_int = uuid_int & test_int + return uuid_int == result_int + + +def _convert_uuid_to_int(_uuid: str) -> int: + UUID_cb = CBUUID.alloc().initWithString_(_uuid) + UUID_data = UUID_cb.data() + UUID_bytes = UUID_data.getBytes_length_(None, len(UUID_data)) + UUID_int = int.from_bytes(UUID_bytes, byteorder="big") + return UUID_int + + +def _convert_int_to_uuid(i: int) -> str: + UUID_bytes = i.to_bytes(length=16, byteorder="big") + UUID_data = NSData.alloc().initWithBytes_length_(UUID_bytes, len(UUID_bytes)) + UUID_cb = CBUUID.alloc().initWithData_(UUID_data) + return UUID_cb.UUIDString() diff --git a/bleak/backends/descriptor.py b/bleak/backends/descriptor.py index d25fbfdf..b8e4c707 100644 --- a/bleak/backends/descriptor.py +++ b/bleak/backends/descriptor.py @@ -8,27 +8,7 @@ import abc from typing import Any - -_ = """Characteristic Aggregate Format org.bluetooth.descriptor.gatt.characteristic_aggregate_format 0x2905 GSS -Characteristic Extended Properties org.bluetooth.descriptor.gatt.characteristic_extended_properties 0x2900 GSS -Characteristic Presentation Format org.bluetooth.descriptor.gatt.characteristic_presentation_format 0x2904 GSS -Characteristic User Description org.bluetooth.descriptor.gatt.characteristic_user_description 0x2901 GSS -Client Characteristic Configuration org.bluetooth.descriptor.gatt.client_characteristic_configuration 0x2902 GSS -Environmental Sensing Configuration org.bluetooth.descriptor.es_configuration 0x290B GSS -Environmental Sensing Measurement org.bluetooth.descriptor.es_measurement 0x290C GSS -Environmental Sensing Trigger Setting org.bluetooth.descriptor.es_trigger_setting 0x290D GSS -External Report Reference org.bluetooth.descriptor.external_report_reference 0x2907 GSS -Number of Digitals org.bluetooth.descriptor.number_of_digitals 0x2909 GSS -Report Reference org.bluetooth.descriptor.report_reference 0x2908 GSS -Server Characteristic Configuration org.bluetooth.descriptor.gatt.server_characteristic_configuration 0x2903 GSS -Time Trigger Setting org.bluetooth.descriptor.time_trigger_setting 0x290E GSS -Valid Range org.bluetooth.descriptor.valid_range 0x2906 GSS -Value Trigger Setting org.bluetooth.descriptor.value_trigger_setting 0x290A GSS -""" -_descriptor_descriptions = { - "0000{0}-0000-1000-8000-00805f9b34fb".format(v[2][2:]): v - for v in [x.split("\t") for x in _.splitlines()] -} +_descriptor_descriptions = {'00002905-0000-1000-8000-00805f9b34fb': ['Characteristic Aggregate Format', 'org.bluetooth.descriptor.gatt.characteristic_aggregate_format', '0x2905', 'GSS'], '00002900-0000-1000-8000-00805f9b34fb': ['Characteristic Extended Properties', 'org.bluetooth.descriptor.gatt.characteristic_extended_properties', '0x2900', 'GSS'], '00002904-0000-1000-8000-00805f9b34fb': ['Characteristic Presentation Format', 'org.bluetooth.descriptor.gatt.characteristic_presentation_format', '0x2904', 'GSS'], '00002901-0000-1000-8000-00805f9b34fb': ['Characteristic User Description', 'org.bluetooth.descriptor.gatt.characteristic_user_description', '0x2901', 'GSS'], '00002902-0000-1000-8000-00805f9b34fb': ['Client Characteristic Configuration', 'org.bluetooth.descriptor.gatt.client_characteristic_configuration', '0x2902', 'GSS'], '0000290B-0000-1000-8000-00805f9b34fb': ['Environmental Sensing Configuration', 'org.bluetooth.descriptor.es_configuration', '0x290B', 'GSS'], '0000290C-0000-1000-8000-00805f9b34fb': ['Environmental Sensing Measurement', 'org.bluetooth.descriptor.es_measurement', '0x290C', 'GSS'], '0000290D-0000-1000-8000-00805f9b34fb': ['Environmental Sensing Trigger Setting', 'org.bluetooth.descriptor.es_trigger_setting', '0x290D', 'GSS'], '00002907-0000-1000-8000-00805f9b34fb': ['External Report Reference', 'org.bluetooth.descriptor.external_report_reference', '0x2907', 'GSS'], '00002909-0000-1000-8000-00805f9b34fb': ['Number of Digitals', 'org.bluetooth.descriptor.number_of_digitals', '0x2909', 'GSS'], '00002908-0000-1000-8000-00805f9b34fb': ['Report Reference', 'org.bluetooth.descriptor.report_reference', '0x2908', 'GSS'], '00002903-0000-1000-8000-00805f9b34fb': ['Server Characteristic Configuration', 'org.bluetooth.descriptor.gatt.server_characteristic_configuration', '0x2903', 'GSS'], '0000290E-0000-1000-8000-00805f9b34fb': ['Time Trigger Setting', 'org.bluetooth.descriptor.time_trigger_setting', '0x290E', 'GSS'], '00002906-0000-1000-8000-00805f9b34fb': ['Valid Range', 'org.bluetooth.descriptor.valid_range', '0x2906', 'GSS'], '0000290A-0000-1000-8000-00805f9b34fb': ['Value Trigger Setting', 'org.bluetooth.descriptor.value_trigger_setting', '0x290A', 'GSS']} class BleakGATTDescriptor(abc.ABC): @@ -46,6 +26,12 @@ def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" raise NotImplementedError() + @property + @abc.abstractmethod + def characteristic_handle(self) -> int: + """handle for the characteristic that this descriptor belongs to""" + raise NotImplementedError() + @property @abc.abstractmethod def uuid(self) -> str: diff --git a/bleak/backends/dotnet/characteristic.py b/bleak/backends/dotnet/characteristic.py index 2cb319ea..fc662366 100644 --- a/bleak/backends/dotnet/characteristic.py +++ b/bleak/backends/dotnet/characteristic.py @@ -52,13 +52,18 @@ def __init__(self, obj: GattCharacteristic): ] def __str__(self): - return "{0}: {1}".format(self.uuid, self.description) + return "[{0}] {1}: {2}".format(self.handle, self.uuid, self.description) @property def service_uuid(self) -> str: """The uuid of the Service containing this characteristic""" return self.obj.Service.Uuid.ToString() + @property + def handle(self) -> int: + """The handle of this characteristic""" + return int(self.obj.AttributeHandle) + @property def uuid(self) -> str: """The uuid of this characteristic""" @@ -79,10 +84,13 @@ def descriptors(self) -> List[BleakGATTDescriptorDotNet]: """List of descriptors for this service""" return self.__descriptors - def get_descriptor(self, _uuid: Union[str, UUID]) -> Union[BleakGATTDescriptorDotNet, None]: - """Get a descriptor by UUID""" + def get_descriptor(self, specifier: Union[int, str, UUID]) -> Union[BleakGATTDescriptorDotNet, None]: + """Get a descriptor by handle (int) or UUID (str or uuid.UUID)""" try: - return next(filter(lambda x: x.uuid == str(_uuid), self.descriptors)) + if isinstance(specifier, int): + return next(filter(lambda x: x.handle == specifier, self.descriptors)) + else: + return next(filter(lambda x: x.uuid == str(specifier), self.descriptors)) except StopIteration: return None diff --git a/bleak/backends/dotnet/client.py b/bleak/backends/dotnet/client.py index 9fefda9e..edef7380 100644 --- a/bleak/backends/dotnet/client.py +++ b/bleak/backends/dotnet/client.py @@ -20,6 +20,7 @@ wrap_IAsyncOperation, IAsyncOperationAwaitable, ) +from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.service import BleakGATTServiceCollection from bleak.backends.dotnet.service import BleakGATTServiceDotNet from bleak.backends.dotnet.characteristic import BleakGATTCharacteristicDotNet @@ -58,6 +59,11 @@ logger = logging.getLogger(__name__) +_communication_statues = { + getattr(GattCommunicationStatus, k): k + for k in ["Success", "Unreachable", "ProtocolError", "AccessDenied"] +} + class BleakClientDotNet(BaseBleakClient): """The native Windows Bleak Client. @@ -81,7 +87,6 @@ def __init__(self, address: str, loop: AbstractEventLoop = None, **kwargs): self._device_info = None self._requester = None self._bridge = Bridge() - self._callbacks = {} self._address_type = ( kwargs["address_type"] @@ -233,7 +238,19 @@ async def get_services(self) -> BleakGATTServiceCollection: ) if services_result.Status != GattCommunicationStatus.Success: - raise BleakDotNetTaskError("Could not get GATT services.") + 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: @@ -246,9 +263,28 @@ async def get_services(self) -> BleakGATTServiceCollection: ) self.services.add_service(BleakGATTServiceDotNet(service)) if characteristics_result.Status != GattCommunicationStatus.Success: - raise BleakDotNetTaskError( - "Could not get GATT characteristics for {0}.".format(service) - ) + 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]( @@ -261,15 +297,32 @@ async def get_services(self) -> BleakGATTServiceCollection: BleakGATTCharacteristicDotNet(characteristic) ) if descriptors_result.Status != GattCommunicationStatus.Success: - raise BleakDotNetTaskError( - "Could not get GATT descriptors for {0}.".format( - characteristic + 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() + descriptor, characteristic.Uuid.ToString(), int(characteristic.AttributeHandle) ) ) @@ -278,11 +331,15 @@ async def get_services(self) -> BleakGATTServiceCollection: # I/O methods - async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], use_cached=False, **kwargs) -> bytearray: + 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: - _uuid (str or UUID): The uuid of the characteristics to read from. + 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`. @@ -290,9 +347,12 @@ async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], use_cached=False, * (bytearray) The read data. """ - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {0} was not found!".format(char_specifier)) read_result = await wrap_IAsyncOperation( IAsyncOperation[GattReadResult]( @@ -310,13 +370,23 @@ async def read_gatt_char(self, _uuid: Union[str, uuid.UUID], use_cached=False, * output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength) reader.ReadBytes(output) value = bytearray(output) - logger.debug("Read Characteristic {0} : {1}".format(_uuid, value)) + logger.debug("Read Characteristic {0} : {1}".format(characteristic.uuid, value)) else: - raise BleakError( - "Could not read characteristic value for {0}: {1}".format( - characteristic.uuid, read_result.Status + 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( @@ -355,28 +425,43 @@ async def read_gatt_descriptor( 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 + 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, _uuid: Union[str, uuid.UUID], data: bytearray, response: bool = False + 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: - _uuid (str or UUID): The uuid of the characteristics to write to. + 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`. """ - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {} was not found!".format(char_specifier)) writer = DataWriter() writer.WriteBytes(Array[Byte](data)) @@ -395,13 +480,25 @@ async def write_gatt_char( loop=self.loop, ) if write_result.Status == GattCommunicationStatus.Success: - logger.debug("Write Characteristic {0} : {1}".format(_uuid, data)) + 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, write_result.Status + 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. @@ -427,14 +524,29 @@ async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None: 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 + 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, _uuid: Union[str, uuid.UUID], callback: Callable[[str, Any], Any], **kwargs + self, + char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID], + callback: Callable[[str, Any], Any], + **kwargs ) -> None: """Activate notifications/indications on a characteristic. @@ -448,40 +560,49 @@ def callback(sender, data): client.start_notify(char_uuid, callback) Args: - _uuid (str or UUID): The uuid of the characteristics to start notification/indication on. + 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. """ - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {0} not found!".format(char_specifier)) - if self._notification_callbacks.get(str(_uuid)): - await self.stop_notify(_uuid) + if self._notification_callbacks.get(characteristic.handle): + await self.stop_notify(characteristic) - status = await self._start_notify(characteristic.obj, callback) + 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, status) + "Could not start notify on {0}: {1}".format( + characteristic.uuid, _communication_statues.get(status, "") + ) ) async def _start_notify( self, - characteristic_obj: GattCharacteristic, + characteristic: BleakGATTCharacteristic, callback: Callable[[str, Any], Any], ): """Internal method performing call to BleakUWPBridge method. Args: - characteristic_obj: The Managed Windows.Devices.Bluetooth.GenericAttributeProfile.GattCharacteristic Object + 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 @@ -497,16 +618,16 @@ async def _start_notify( try: # TODO: Enable adding multiple handlers! - self._callbacks[characteristic_obj.Uuid.ToString()] = TypedEventHandler[ + self._notification_callbacks[characteristic.handle] = TypedEventHandler[ GattCharacteristic, GattValueChangedEventArgs ](_notification_wrapper(self.loop, callback)) self._bridge.AddValueChangedCallback( - characteristic_obj, self._callbacks[characteristic_obj.Uuid.ToString()] + 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._callbacks: - callback = self._callbacks.pop(characteristic_obj.Uuid.ToString()) + 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 @@ -522,24 +643,30 @@ async def _start_notify( ) 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()) + # 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, _uuid: Union[str, uuid.UUID]) -> None: + async def stop_notify(self, char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID]) -> None: """Deactivate notification/indication on a specified characteristic. Args: - _uuid: The characteristic to stop notifying/indicating on. + 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. """ - characteristic = self.services.get_characteristic(str(_uuid)) + 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(_uuid)) + raise BleakError("Characteristic {} not found!".format(char_specifier)) status = await wrap_IAsyncOperation( IAsyncOperation[GattCommunicationStatus]( @@ -555,10 +682,12 @@ async def stop_notify(self, _uuid: Union[str, uuid.UUID]) -> None: if status != GattCommunicationStatus.Success: raise BleakError( - "Could not stop notify on {0}: {1}".format(characteristic.uuid, status) + "Could not stop notify on {0}: {1}".format( + characteristic.uuid, _communication_statues.get(status, "") + ) ) else: - callback = self._callbacks.pop(characteristic.uuid) + callback = self._notification_callbacks.pop(characteristic.handle) self._bridge.RemoveValueChangedCallback(characteristic.obj, callback) @@ -571,6 +700,8 @@ def dotnet_notification_parser(sender: Any, args: Any): output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength) reader.ReadBytes(output) - return loop.call_soon_threadsafe(func, sender.Uuid.ToString(), bytearray(output)) + return loop.call_soon_threadsafe( + func, sender.Uuid.ToString(), bytearray(output) + ) return dotnet_notification_parser diff --git a/bleak/backends/dotnet/descriptor.py b/bleak/backends/dotnet/descriptor.py index aed5695f..27a16f2a 100644 --- a/bleak/backends/dotnet/descriptor.py +++ b/bleak/backends/dotnet/descriptor.py @@ -7,14 +7,20 @@ class BleakGATTDescriptorDotNet(BleakGATTDescriptor): """GATT Descriptor implementation for .NET backend""" - def __init__(self, obj: GattDescriptor, characteristic_uuid: str): + def __init__(self, obj: GattDescriptor, characteristic_uuid: str, characteristic_handle: int): super(BleakGATTDescriptorDotNet, self).__init__(obj) self.obj = obj self.__characteristic_uuid = characteristic_uuid + self.__characteristic_handle = characteristic_handle def __str__(self): return "{0}: (Handle: {1})".format(self.uuid, self.handle) + @property + def characteristic_handle(self) -> int: + """handle for the characteristic that this descriptor belongs to""" + return self.__characteristic_handle + @property def characteristic_uuid(self) -> str: """UUID for the characteristic that this descriptor belongs to""" diff --git a/bleak/backends/dotnet/discovery.py b/bleak/backends/dotnet/discovery.py index e31084a8..59b11484 100644 --- a/bleak/backends/dotnet/discovery.py +++ b/bleak/backends/dotnet/discovery.py @@ -36,6 +36,12 @@ async def discover( loop (Event Loop): The event loop to use. Keyword Args: + SignalStrengthFilter (Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter): A + BluetoothSignalStrengthFilter object used for configuration of Bluetooth + LE advertisement filtering that uses signal strength-based filtering. + AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A + BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE + advertisement filtering that uses payload section-based filtering. string_output (bool): If set to false, ``discover`` returns .NET device objects instead. @@ -43,6 +49,9 @@ async def discover( List of strings or objects found. """ + signal_strength_filter = kwargs.get("SignalStrengthFilter", None) + advertisement_filter = kwargs.get("AdvertisementFilter", None) + loop = loop if loop else asyncio.get_event_loop() watcher = BluetoothLEAdvertisementWatcher() @@ -85,6 +94,11 @@ def AdvertisementWatcher_Stopped(sender, e): watcher.ScanningMode = BluetoothLEScanningMode.Active + if signal_strength_filter is not None: + watcher.SignalStrengthFilter = signal_strength_filter + if advertisement_filter is not None: + watcher.AdvertisementFilter = advertisement_filter + # Watcher works outside of the Python process. watcher.Start() await asyncio.sleep(timeout, loop=loop) diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 47e2a237..73c7f436 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -7,7 +7,7 @@ class BaseBleakScanner(abc.ABC): - """Interface for Bleak Bluetooth LE Scanners + """Interface for Bleak Bluetooth LE Scanners. Args: loop (Event Loop): The event loop to use. diff --git a/bleak/backends/service.py b/bleak/backends/service.py index f9d77bf3..26d7d80b 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -6,6 +6,7 @@ """ import abc +import uuid from uuid import UUID from typing import List, Union, Iterator @@ -64,11 +65,11 @@ def __init__(self): self.__descriptors = {} def __getitem__( - self, item: Union[str, int] + self, item: Union[str, int, uuid.UUID] ) -> Union[BleakGATTService, BleakGATTCharacteristic, BleakGATTDescriptor]: """Get a service, characteristic or descriptor from uuid or handle""" return self.services.get( - item, self.characteristics.get(item, self.descriptors.get(item, None)) + str(item), self.characteristics.get(item, self.descriptors.get(item, None)) ) def __iter__(self) -> Iterator[BleakGATTService]: @@ -82,7 +83,7 @@ def services(self) -> dict: @property def characteristics(self) -> dict: - """Returns dictionary of UUID strings to BleakGATTCharacteristic""" + """Returns dictionary of handles to BleakGATTCharacteristic""" return self.__characteristics @property @@ -111,8 +112,8 @@ def add_characteristic(self, characteristic: BleakGATTCharacteristic): Should not be used by end user, but rather by `bleak` itself. """ - if characteristic.uuid not in self.__characteristics: - self.__characteristics[characteristic.uuid] = characteristic + if characteristic.handle not in self.__characteristics: + self.__characteristics[characteristic.handle] = characteristic self.__services[characteristic.service_uuid].add_characteristic( characteristic ) @@ -121,9 +122,17 @@ def add_characteristic(self, characteristic: BleakGATTCharacteristic): "This characteristic is already present in this BleakGATTServiceCollection!" ) - def get_characteristic(self, _uuid: Union[str, UUID]) -> BleakGATTCharacteristic: - """Get a characteristic by UUID string""" - return self.characteristics.get(str(_uuid), None) + def get_characteristic(self, specifier: Union[int, str, UUID]) -> BleakGATTCharacteristic: + """Get a characteristic by handle (int) or UUID (str or uuid.UUID)""" + if isinstance(specifier, int): + return self.characteristics.get(specifier, None) + else: + # Assume uuid usage. + x = list(filter(lambda x: x.uuid == str(specifier), self.characteristics.values())) + if len(x) > 1: + raise BleakError("Multiple Characteristics with this UUID, refer to your desired characteristic by the `handle` attribute instead.") + else: + return x[0] if x else None def add_descriptor(self, descriptor: BleakGATTDescriptor): """Add a :py:class:`~BleakGATTDescriptor` to the service collection. @@ -132,7 +141,7 @@ def add_descriptor(self, descriptor: BleakGATTDescriptor): """ if descriptor.handle not in self.__descriptors: self.__descriptors[descriptor.handle] = descriptor - self.__characteristics[descriptor.characteristic_uuid].add_descriptor( + self.__characteristics[descriptor.characteristic_handle].add_descriptor( descriptor ) else: diff --git a/docs/api.rst b/docs/api.rst index cdd4d084..8dbbdb47 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -8,9 +8,9 @@ Connection Client Interface :members: Scanning Client Interface ---------------------------- +------------------------- -.. automodule:: bleak.backends.scanning +.. automodule:: bleak.backends.scanner :members: Interface for BLE devices diff --git a/docs/backends/linux.rst b/docs/backends/linux.rst index ecad46d4..a7dcb19c 100644 --- a/docs/backends/linux.rst +++ b/docs/backends/linux.rst @@ -8,17 +8,7 @@ The Linux backend of Bleak is written using the package. It is written for `Twisted `_, but by using the `twisted.internet.asyncioreactor `_ -one can use it in the `asyncio` way. - -.. note:: - - You should not create any new event loops when using Bleak with the BlueZ backend, only use the - ``asyncio.get_event_loop``. This is due to the way that the - `asyncioreactor `_ - is used right now. - - If more clients are needed, run these in separate processes right now, until a better recommendation - is available. +one can use it with `asyncio`. Special handling for ``write_gatt_char`` diff --git a/docs/backends/windows.rst b/docs/backends/windows.rst index eaf40b95..445f09b7 100644 --- a/docs/backends/windows.rst +++ b/docs/backends/windows.rst @@ -9,7 +9,7 @@ The Windows backend implements a ``BleakClient`` in the module ``bleak.backends. method in the ``bleak.backends.dotnet.discovery`` module. There are also backend-specific implementations of the ``BleakGATTService``, ``BleakGATTCharacteristic`` and ``BleakGATTDescriptor`` classes. -FInally, some .NET/``asyncio``-connectivity methods are available in the ``bleak.backends.dotnet.utils`` module. +Finally, some .NET/``asyncio``-connectivity methods are available in the ``bleak.backends.dotnet.utils`` module. Specific features for the Windows backend ----------------------------------------- @@ -17,5 +17,5 @@ Specific features for the Windows backend Client ~~~~~~ - The constructor keyword ``address_type`` which can have the values ``"public"`` or ``"random"``. This value - makes sure that the connect + makes sure that the connection is made in a fashion that suits the peripheral. diff --git a/docs/history.rst b/docs/history.rst index 25064996..565b0521 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1 +1 @@ -.. include:: ../HISTORY.rst +.. include:: ../CHANGELOG.rst diff --git a/docs/index.rst b/docs/index.rst index 05f0ae96..318d3054 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,9 +11,6 @@ bleak :target: https://github.com/hbldh/bleak/actions?query=workflow%3A%22Build+and+Test%22 :alt: Build and Test -.. image:: https://dev.azure.com/hbldh/github/_apis/build/status/hbldh.bleak?branchName=master - :target: https://dev.azure.com/hbldh/github/_build/latest?definitionId=4&branchName=master - .. image:: https://img.shields.io/pypi/v/bleak.svg :target: https://pypi.python.org/pypi/bleak diff --git a/docs/scanning.rst b/docs/scanning.rst index 6960b1e6..3210cee0 100644 --- a/docs/scanning.rst +++ b/docs/scanning.rst @@ -31,6 +31,7 @@ and ``rssi`` attributes, as well as a ``metadata`` attribute, a dict with keys ` which potentially contains a list of all service UUIDs on the device and a binary string of data from the manufacturer of the device respectively. + BleakScanner ------------ @@ -95,3 +96,19 @@ or separately, calling ``start`` and ``stop`` methods on the scanner manually: In the manual mode, it is possible to add an own callback that you want to call upon each scanner detection, as can be seen above. There is also possibilities of adding scanning filters, but these differ so widely between implementations, so these details are recorded there instead. + +Scanning Filters +---------------- + +There are some scanning filters that can be applied, that will reduce your scanning +results prior to them getting to bleak. These are pretty quite backend specific, but +they are generally used like this: + +- On the `discover` method, send in keyword arguments according to what is + described in the docstring of the method. +- On the backend's `BleakScanner` implementation, either send in keyword arguments + according to what is described in the docstring of the class or use the + ``set_scanning_filter`` method to set them after the instance has been created. + +Scanning filters are currently implemented in Windows and BlueZ backends, but not yet +in the macOS backend. diff --git a/docs/usage.rst b/docs/usage.rst index 95a57f6c..d98694d1 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -2,6 +2,14 @@ Usage ===== +.. note:: + + A Bluetooth peripheral may have several characteristics with the same UUID, so + the means of specifying characteristics by UUID or string representation of it + might not always work in bleak version > 0.7.0. One can now also use the characteristic's + handle or even the ``BleakGATTCharacteristic`` object itself in + ``read_gatt_char``, ``write_gatt_char``, ``start_notify``, and ``stop_notify``. + One can use the ``BleakClient`` to connect to a Bluetooth device and read its model number via the asyncronous context manager like this: @@ -46,7 +54,7 @@ or one can do it without the context manager like this: loop = asyncio.get_event_loop() loop.run_until_complete(run(address, loop)) -Try to make sure you always get to call the disconnect method for a client before discarding it; +Make sure you always get to call the disconnect method for a client before discarding it; the Bluetooth stack on the OS might need to be cleared of residual data which is cached in the ``BleakClient``. diff --git a/examples/enable_notifications.py b/examples/enable_notifications.py index dda2cf75..c1f6c0c7 100644 --- a/examples/enable_notifications.py +++ b/examples/enable_notifications.py @@ -55,7 +55,7 @@ async def run(address, loop, debug=False): address = ( "24:71:89:cc:09:05" # <--- Change to your device's address here if you are using Windows or Linux if platform.system() != "Darwin" - else "243E23AE-4A99-406C-B317-18F1BD7B4CBE" # <--- Change to your device's address here if you are using macOS + else "B9EA5233-37EF-4DD6-87A8-2A875E821C46" # <--- Change to your device's address here if you are using macOS ) loop = asyncio.get_event_loop() loop.run_until_complete(run(address, loop, True)) diff --git a/examples/get_services.py b/examples/get_services.py index 746aef61..d01a248c 100644 --- a/examples/get_services.py +++ b/examples/get_services.py @@ -23,7 +23,7 @@ async def print_services(mac_addr: str, loop: asyncio.AbstractEventLoop): mac_addr = ( "24:71:89:cc:09:05" if platform.system() != "Darwin" - else "243E23AE-4A99-406C-B317-18F1BD7B4CBE" + else "B9EA5233-37EF-4DD6-87A8-2A875E821C46" ) loop = asyncio.get_event_loop() loop.run_until_complete(print_services(mac_addr, loop)) diff --git a/examples/sensortag.py b/examples/sensortag.py index e32ece9f..c5821a60 100644 --- a/examples/sensortag.py +++ b/examples/sensortag.py @@ -104,7 +104,7 @@ async def run(address, loop, debug=False): # h.setLevel(logging.DEBUG) # l.addHandler(h) - async with BleakClient(address, loop=loop) as client: + async with BleakClient(address, timeout=1.0, loop=loop) as client: x = await client.is_connected() logger.info("Connected: {0}".format(x)) @@ -164,7 +164,7 @@ def keypress_handler(sender, data): address = ( "24:71:89:cc:09:05" if platform.system() != "Darwin" - else "243E23AE-4A99-406C-B317-18F1BD7B4CBE" + else "B9EA5233-37EF-4DD6-87A8-2A875E821C46" ) loop = asyncio.get_event_loop() loop.run_until_complete(run(address, loop, True)) diff --git a/examples/service_explorer.py b/examples/service_explorer.py index 753343bf..5b7bbe6d 100644 --- a/examples/service_explorer.py +++ b/examples/service_explorer.py @@ -41,8 +41,8 @@ async def run(address, loop, debug=False): else: value = None log.info( - "\t[Characteristic] {0}: ({1}) | Name: {2}, Value: {3} ".format( - char.uuid, ",".join(char.properties), char.description, value + "\t[Characteristic] {0}: (Handle: {1}) ({2}) | Name: {3}, Value: {4} ".format( + char.uuid, char.handle, ",".join(char.properties), char.description, value ) ) for descriptor in char.descriptors: @@ -58,7 +58,7 @@ async def run(address, loop, debug=False): address = ( "24:71:89:cc:09:05" if platform.system() != "Darwin" - else "243E23AE-4A99-406C-B317-18F1BD7B4CBE" + else "B9EA5233-37EF-4DD6-87A8-2A875E821C46" ) loop = asyncio.get_event_loop() loop.run_until_complete(run(address, loop, True)) diff --git a/examples/two_devices.py b/examples/two_devices.py new file mode 100644 index 00000000..2db89273 --- /dev/null +++ b/examples/two_devices.py @@ -0,0 +1,37 @@ +from bleak import BleakClient, discover, BleakError +import asyncio + +temperatureUUID = "45366e80-cf3a-11e1-9ab4-0002a5d5c51b" +ecgUUID = "46366e80-cf3a-11e1-9ab4-0002a5d5c51b" + +notify_uuid = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0xffe1) + + +def callback(sender, data): + print(sender, data) + +def run(addresses): + loop = asyncio.get_event_loop() + + tasks = asyncio.gather( + *(connect_to_device(address, loop) for address in addresses) + ) + + loop.run_until_complete(tasks) + +async def connect_to_device(address, loop): + print("starting", address, "loop") + async with BleakClient(address, loop=loop, timeout=5.0) as client: + + print("connect to", address) + try: + await client.start_notify(notify_uuid, callback) + await asyncio.sleep(10.0, loop=loop) + await client.stop_notify(notify_uuid) + except Exception as e: + print(e) + + print("disconnect from", address) + +if __name__ == "__main__": + run(["B9EA5233-37EF-4DD6-87A8-2A875E821C46", "F0CBEBD3-299B-4139-A9FC-44618C720157" ]) diff --git a/requirements.txt b/requirements.txt index 5bf794d3..d2f61802 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ txdbus>=1.1.1; sys_platform=="linux" -pyobjc;sys_platform == 'darwin' -pyobjc-framework-CoreBluetooth;sys_platform == 'darwin' -pythonnet>=2.3.0; sys_platform == 'win32' +pyobjc-core>=6.2;sys_platform == 'darwin' +pyobjc-framework-CoreBluetooth>=6.2;sys_platform == 'darwin' +pyobjc-framework-libdispatch>=6.2;sys_platform == 'darwin' +pythonnet>=2.5.1; sys_platform == 'win32' diff --git a/requirements_dev.txt b/requirements_dev.txt index 5e1da5f0..59ed53cd 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,5 +1,5 @@ pip>=18.0 -bump2version +bump2version==1.0.0 wheel>=0.32.2 watchdog>=0.8.3 flake8>=3.5.0 diff --git a/setup.cfg b/setup.cfg index 44dc36e8..26077cd0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.6.4 +current_version = 0.7.0 commit = False tag = False diff --git a/setup.py b/setup.py index 57d7b11c..9dae2bd3 100644 --- a/setup.py +++ b/setup.py @@ -22,8 +22,9 @@ # Linux reqs 'txdbus;platform_system=="Linux"', # macOS reqs - 'pyobjc;platform_system=="Darwin"', + 'pyobjc-core;platform_system=="Darwin"', 'pyobjc-framework-CoreBluetooth;platform_system=="Darwin"', + 'pyobjc-framework-libdispatch;platform_system=="Darwin"', # Windows reqs 'pythonnet;platform_system=="Windows"', ] @@ -33,7 +34,7 @@ here = os.path.abspath(os.path.dirname(__file__)) with io.open(os.path.join(here, "README.rst"), encoding="utf-8") as f: long_description = "\n" + f.read() -with io.open(os.path.join(here, "HISTORY.rst"), encoding="utf-8") as f: +with io.open(os.path.join(here, "CHANGELOG.rst"), encoding="utf-8") as f: long_description += "\n\n" + f.read() # Load the package's __version__.py module as a dictionary. @@ -103,11 +104,13 @@ def run(self): "Natural Language :: English", "Operating System :: Microsoft :: Windows :: Windows 10", "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", ], # $ setup.py publish support.