diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index bc4bac3c..16008896 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -13,10 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: [3.5, 3.6, 3.7, 3.8] - exclude: - - os: macos-latest - python-version: 3.5 + python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -32,12 +29,14 @@ jobs: run: pip install -r requirements.txt - name: Install develoment dependencies run: pip install -r requirements_dev.txt + - name: Check code formatting with black + run: black . --diff - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + # exit-zero treats all errors as warnings. Default line length of black is 88 + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics - name: Test with pytest run: | pytest tests --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=com --cov-report=xml --cov-report=html diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 119b94c3..34bb2f96 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,12 +7,14 @@ 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.8.0`_ (2020-09-02) + +`0.8.0`_ (2020-09-22) --------------------- Added ~~~~~ +* Implemented ``set_disconnected_callback`` in the .NET backend ``BleakClient`` implementation. * Added ``find_device_by_address`` method to the ``BleakScanner`` interface, for stopping scanning when a desired address is found. * Implemented ``find_device_by_address`` in the .NET backend ``BleakScanner`` implementation and @@ -26,15 +28,24 @@ Added * Implemented pairing method in .NET backend. * Implemented pairing method in the BlueZ backend. * Added stumps and ``NotImplementedError`` on pairing in macOS backend. +* Added the possibility to connect using ``BLEDevice`` instead of a string address. This + allows for skipping the discovery call when connecting. + +Removed +~~~~~~~ + +* Support for Python 3.5. Changed ~~~~~~~ * **BREAKING CHANGE** All notifications now have the characteristic's integer **handle** instead of its UUID as a string as the first argument ``sender`` sent to notification callbacks. This provides the uniqueness of sender in notifications as well. +* Renamed ``BleakClient`` argument ``address`` to ``address_or_ble_device``. * Version 0.5.0 of BleakUWPBridge, with some modified methods and implementing ``IDisposable``. * Merged #224. All storing and passing of event loops in bleak is removed. * Removed Objective C delegate compliance checks. Merged #253. +* Made context managers for .NET ``DataReader`` and ``DataWriter``. Fixed ~~~~~ @@ -56,7 +67,7 @@ Fixed Changed ~~~~~~~ -* Improved, more explantory error on BlueZ backend when ``BleakClient`` cannot find the desired device when trying to connect. (#238) +* Improved, more explanatory error on BlueZ backend when ``BleakClient`` cannot find the desired device when trying to connect. (#238) * Better-than-nothing documentation about scanning filters added (#230). * Ran black on code which was forgotten in 0.7.0. Large diffs due to that. * Re-adding Python 3.8 CI "tests" on Windows again. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d58c1e95..82e558b0 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -102,7 +102,7 @@ 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+ on the following platforms: +3. The pull request should work for Python 3.6+ on the following platforms: - Windows 10, version 16299 (Fall Creators Update) and greater - Linux distributions with BlueZ >= 5.43 - OS X / macOS >= 10.11 diff --git a/README.rst b/README.rst index dfbcf46b..d765fdac 100644 --- a/README.rst +++ b/README.rst @@ -2,13 +2,12 @@ bleak ===== -.. image:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo.png +.. figure:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo.png :target: https://github.com/hbldh/bleak :alt: Bleak Logo :scale: 50% - .. image:: https://github.com/hbldh/bleak/workflows/Build%20and%20Test/badge.svg :target: https://github.com/hbldh/bleak/actions?query=workflow%3A%22Build+and+Test%22 :alt: Build and Test diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6e8b752e..d5203fba 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,8 +13,6 @@ jobs: vmImage: 'Ubuntu 16.04' strategy: matrix: - Python35-x64: - python.version: '3.5' python.architecture: 'x64' Python36-x64: python.version: '3.6' @@ -53,8 +51,6 @@ jobs: vmImage: 'windows-2019' strategy: matrix: - Python35-x64: - python.version: '3.5' python.architecture: 'x64' Python36-x64: python.version: '3.6' @@ -92,8 +88,6 @@ jobs: strategy: matrix: - Python35-x64: - python.version: '3.5' python.architecture: 'x64' Python36-x64: python.version: '3.6' @@ -131,8 +125,6 @@ jobs: strategy: matrix: - Python35-x64: - python.version: '3.5' python.architecture: 'x64' Python36-x64: python.version: '3.6' diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py index badcfcfc..46454333 100644 --- a/bleak/backends/bluezdbus/characteristic.py +++ b/bleak/backends/bluezdbus/characteristic.py @@ -1,4 +1,3 @@ -import re from uuid import UUID from typing import Union, List @@ -27,8 +26,6 @@ # "authorize" } -_handle_regex = re.compile("/char([0-9a-fA-F]*)") - class BleakGATTCharacteristicBlueZDBus(BleakGATTCharacteristic): """GATT Characteristic implementation for the BlueZ DBus backend""" @@ -39,17 +36,8 @@ 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) + # D-Bus object path contains handle as last 4 characters of 'charYYYY' + self._handle = int(object_path[-4:], 16) @property def service_uuid(self) -> str: diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 4742ebaf..908d3e74 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +""" +BLE Client for BlueZ on Linux +""" import logging import asyncio import os @@ -12,6 +15,7 @@ from twisted.internet.error import ConnectionDone +from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTServiceCollection from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.exc import BleakError @@ -36,27 +40,28 @@ class BleakClientBlueZDBus(BaseBleakClient): Implemented by using the `BlueZ DBUS API `_. Args: - address (str): The address of the BLE peripheral to connect to. + address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it. Keyword Args: timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. """ - def __init__(self, address, **kwargs): - super(BleakClientBlueZDBus, self).__init__(address, **kwargs) + def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): + super(BleakClientBlueZDBus, self).__init__(address_or_ble_device, **kwargs) self.device = kwargs.get("device") if kwargs.get("device") else "hci0" - self.address = address + self.address = address_or_ble_device # Backend specific, TXDBus objects and data - self._device_path = None + if isinstance(address_or_ble_device, BLEDevice): + self._device_path = address_or_ble_device.details["path"] + else: + self._device_path = None self._bus = None self._reactor = None self._rules = {} self._subscriptions = list() - self._disconnected_callback = None - # This maps DBus paths of GATT Characteristics to their BLE handles. self._char_path_to_handle = {} @@ -69,34 +74,6 @@ def __init__(self, address, **kwargs): # Connectivity methods - def set_disconnected_callback( - self, callback: Callable[[BaseBleakClient, Future], None], **kwargs - ) -> None: - """Set the disconnected callback. - - The callback will be called on DBus PropChanged event with - the 'Connected' key set to False. - - A disconnect callback must accept two positional arguments, - the BleakClient and the Future that called it. - - Example: - - .. code-block::python - - async with BleakClient(mac_addr) as client: - def disconnect_callback(client, future): - print(f"Disconnected callback called on {client}!") - - client.set_disconnected_callback(disconnect_callback) - - Args: - callback: callback to be called on disconnection. - - """ - - self._disconnected_callback = callback - async def connect(self, **kwargs) -> bool: """Connect to the specified GATT server. @@ -109,17 +86,19 @@ async def connect(self, **kwargs) -> bool: """ # A Discover must have been run before connecting to any devices. # Find the desired device before trying to connect. - timeout = kwargs.get("timeout", self._timeout) - device = await BleakScannerBlueZDBus.find_device_by_address( - self.address, timeout=timeout, device=self.device) - - if device: - self._device_path = device.details["path"] - else: - raise BleakError( - "Device with address {0} was not found.".format(self.address) + if self._device_path is None: + timeout = kwargs.get("timeout", self._timeout) + device = await BleakScannerBlueZDBus.find_device_by_address( + self.address, timeout=timeout, device=self.device ) + if device: + self._device_path = device.details["path"] + else: + raise BleakError( + "Device with address {0} was not found.".format(self.address) + ) + loop = asyncio.get_event_loop() self._reactor = get_reactor(loop) @@ -341,7 +320,9 @@ async def unpair(self) -> bool: Boolean regarding success of unpairing. """ - warnings.warn("Unpairing is seemingly unavailable in the BlueZ DBus API at the moment.") + warnings.warn( + "Unpairing is seemingly unavailable in the BlueZ DBus API at the moment." + ) return False async def is_connected(self) -> bool: @@ -429,7 +410,9 @@ async def get_services(self) -> BleakGATTServiceCollection: self.services.add_characteristic( BleakGATTCharacteristicBlueZDBus(char, object_path, _service[0].uuid) ) - self._char_path_to_handle[object_path] = char.get("Handle") + + # D-Bus object path contains handle as last 4 characters of 'charYYYY' + self._char_path_to_handle[object_path] = int(object_path[-4:], 16) for desc, object_path in _descs: _characteristic = list( @@ -569,14 +552,16 @@ async def write_gatt_char( ) -> 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.51 - https://git.kernel.org/pub/scm/bluetooth/bluez.git/commit?id=fa9473bcc48417d69cc9ef81d41a72b18e34a55a - Before that commit, "Characteristic.WriteValue" was only "Write with - response". "Characteristic.AcquireWrite" was added in Bluez 5.46 - https://git.kernel.org/pub/scm/bluetooth/bluez.git/commit/doc/gatt-api.txt?id=f59f3dedb2c79a75e51a3a0d27e2ae06fefc603e - which can be used to "Write without response", but for older versions - of Bluez, it is not possible to "Write without response". + .. note:: + + The version check below is for the "type" option to the + "Characteristic.WriteValue" method that was added to `Bluez in 5.51 + `_ + Before that commit, ``Characteristic.WriteValue`` was only "Write with + response". ``Characteristic.AcquireWrite`` was `added in Bluez 5.46 + `_ + which can be used to "Write without response", but for older versions + of Bluez, it is not possible to "Write without response". Args: char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to write diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 2a333b95..01d2e46d 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -217,7 +217,9 @@ def register_detection_callback(self, callback: Callable): self._callback = callback @classmethod - async def find_device_by_address(cls, device_identifier: str, timeout: float = 10.0, **kwargs) -> BLEDevice: + async def find_device_by_address( + cls, device_identifier: str, timeout: float = 10.0, **kwargs + ) -> BLEDevice: """A convenience method for obtaining a ``BLEDevice`` object specified by Bluetooth address. Args: @@ -237,10 +239,15 @@ async def find_device_by_address(cls, device_identifier: str, timeout: float = 1 scanner = cls(timeout=timeout) def stop_if_detected(message): - if any(device.get("Address", "").lower() == device_identifier for device in scanner._devices.values()): + if any( + device.get("Address", "").lower() == device_identifier + for device in scanner._devices.values() + ): loop.call_soon_threadsafe(stop_scanning_event.set) - return await scanner._find_device_by_address(device_identifier, stop_scanning_event, stop_if_detected, timeout) + return await scanner._find_device_by_address( + device_identifier, stop_scanning_event, stop_if_detected, timeout + ) # Helper methods diff --git a/bleak/backends/bluezdbus/utils.py b/bleak/backends/bluezdbus/utils.py index ab985f1a..997f3583 100644 --- a/bleak/backends/bluezdbus/utils.py +++ b/bleak/backends/bluezdbus/utils.py @@ -50,21 +50,21 @@ def get_device_object_path(hci_device, address): def get_gatt_service_path(hci_device, address, service_id): """Get object path for a GATT Service for a Bluetooth device. - Service org.bluez - Service org.bluez - Interface org.bluez.GattService1 - Object path [variable prefix]/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/serviceXX + Service org.bluez + Service org.bluez + Interface org.bluez.GattService1 + Object path [variable prefix]/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/serviceXX - Args: - hci_device (str): Which bluetooth adapter to connect with. - address (str): The Bluetooth address of the bluetooth device. - service_id (int): + Args: + hci_device (str): Which bluetooth adapter to connect with. + address (str): The Bluetooth address of the bluetooth device. + service_id (int): - Returns: - String representation of GATT service object path on format - `/org/bluez/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/serviceXX`. + Returns: + String representation of GATT service object path on format + `/org/bluez/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/serviceXX`. - """ + """ base = get_device_object_path(hci_device, address) return base + "{0}/service{1:02d}".format(base, service_id) diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py index fb3c81c5..d7bfc230 100644 --- a/bleak/backends/characteristic.py +++ b/bleak/backends/characteristic.py @@ -27,9 +27,7 @@ class GattCharacteristicsFlags(enum.Enum): class BleakGATTCharacteristic(abc.ABC): - """Interface for the Bleak representation of a GATT Characteristic - - """ + """Interface for the Bleak representation of a GATT Characteristic""" def __init__(self, obj: Any): self.obj = obj diff --git a/bleak/backends/client.py b/bleak/backends/client.py index 9933aab0..b93fd2f2 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -12,6 +12,7 @@ from bleak.backends.service import BleakGATTServiceCollection from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.device import BLEDevice class BaseBleakClient(abc.ABC): @@ -19,13 +20,21 @@ class BaseBleakClient(abc.ABC): The documentation of this interface should thus be safe to use as a reference for your implementation. - Keyword Args: - timeout (float): Timeout for required ``discover`` call. Defaults to 2.0. + Args: + address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it. + Keyword Args: + timeout (float): Timeout for required ``discover`` call. Defaults to 10.0. + disconnected_callback (callable): Callback that will be scheduled in the + event loop when the client is disconnected. The callable must take one + argument, which will be this client object. """ - def __init__(self, address, **kwargs): - self.address = address + def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): + if isinstance(address_or_ble_device, BLEDevice): + self.address = address_or_ble_device.address + else: + self.address = address_or_ble_device self.services = BleakGATTServiceCollection() @@ -33,12 +42,17 @@ def __init__(self, address, **kwargs): self._notification_callbacks = {} self._timeout = kwargs.get("timeout", 10.0) + self._disconnected_callback = kwargs.get("disconnected_callback") def __str__(self): return "{0}, {1}".format(self.__class__.__name__, self.address) def __repr__(self): - return "<{0}, {1}, {2}>".format(self.__class__.__name__, self.address, super(BaseBleakClient, self).__repr__()) + return "<{0}, {1}, {2}>".format( + self.__class__.__name__, + self.address, + super(BaseBleakClient, self).__repr__(), + ) # Async Context managers @@ -51,15 +65,16 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): # Connectivity methods - @abc.abstractmethod - async def set_disconnected_callback( - self, callback: Callable[["BaseBleakClient"], None], **kwargs + def set_disconnected_callback( + self, callback: Union[Callable[["BaseBleakClient"], None], None], **kwargs ) -> None: """Set the disconnect callback. The callback will only be called on unsolicited disconnect event. Callbacks must accept one input which is the client object itself. + Set the callback to ``None`` to remove any existing callback. + .. code-block:: python def callback(client): @@ -72,8 +87,7 @@ def callback(client): callback: callback to be called on disconnection. """ - - raise NotImplementedError() + self._disconnected_callback = callback @abc.abstractmethod async def connect(self, **kwargs) -> bool: diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 62b37417..3a205c08 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -41,8 +41,12 @@ 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 +try: + _mac_version = list(map(int, platform.mac_ver()[0].split("."))) + _IS_PRE_10_13 = _mac_version[0] == 10 and _mac_version[1] < 13 +except: # noqa For building docs + _mac_version = "" + _IS_PRE_10_13 = False class CMDConnectionState(Enum): @@ -195,6 +199,8 @@ def did_discover_peripheral( RSSI: NSNumber, ): # Note: this function might be called several times for same device. + # This can happen for instance when an active scan is done, and the + # second call with contain the data from the BLE scan response. # Example a first time with the following keys in advertisementData: # ['kCBAdvDataLocalName', 'kCBAdvDataIsConnectable', 'kCBAdvDataChannel'] # ... and later a second time with other keys (and values) such as: @@ -210,6 +216,9 @@ def did_discover_peripheral( if uuid_string in self.devices: device = self.devices[uuid_string] + # It could be the device did not have a name previously but now it does. + if peripheral.name(): + device.name = peripheral.name() else: address = uuid_string name = peripheral.name() or None @@ -250,7 +259,9 @@ def did_connect_peripheral(self, central, peripheral): ) ) if self._connection_state != CMDConnectionState.CONNECTED: - peripheralDelegate = PeripheralDelegate.alloc().initWithPeripheral_(peripheral) + peripheralDelegate = PeripheralDelegate.alloc().initWithPeripheral_( + peripheral + ) self.connected_peripheral_delegate = peripheralDelegate self._connection_state = CMDConnectionState.CONNECTED diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py index a347b463..2ba1390e 100644 --- a/bleak/backends/corebluetooth/PeripheralDelegate.py +++ b/bleak/backends/corebluetooth/PeripheralDelegate.py @@ -35,7 +35,7 @@ class _EventDict(dict): def get_cleared(self, xUUID) -> asyncio.Event: - """ Convenience method. + """Convenience method. Returns a cleared (False) event. Creates it if doesn't exits. """ if xUUID not in self: @@ -128,9 +128,9 @@ async def readCharacteristic_( self.peripheral.readValueForCharacteristic_(characteristic) await asyncio.wait_for(event.wait(), timeout=5) if characteristic.value(): - return characteristic.value() + return characteristic.value() else: - return b'' + return b"" async def readDescriptor_( self, descriptor: CBDescriptor, use_cached=True diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 302fa840..32cf4fed 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -1,7 +1,7 @@ """ BLE Client for CoreBluetooth on macOS -Created on 2019-6-26 by kevincar +Created on 2019-06-26 by kevincar """ import logging @@ -20,9 +20,9 @@ BleakGATTCharacteristicCoreBluetooth, ) from bleak.backends.corebluetooth.descriptor import BleakGATTDescriptorCoreBluetooth -from bleak.backends.corebluetooth.discovery import discover from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth from bleak.backends.corebluetooth.service import BleakGATTServiceCoreBluetooth +from bleak.backends.device import BLEDevice from bleak.backends.service import BleakGATTServiceCollection from bleak.backends.characteristic import BleakGATTCharacteristic @@ -35,22 +35,26 @@ class BleakClientCoreBluetooth(BaseBleakClient): """CoreBluetooth class interface for BleakClient Args: - address (str): The uuid of the BLE peripheral to connect to. + address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it. Keyword Args: - timeout (float): Timeout for required ``discover`` call during connect. Defaults to 10.0. + timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. """ - def __init__(self, address: str, **kwargs): - super(BleakClientCoreBluetooth, self).__init__(address, **kwargs) + + def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): + super(BleakClientCoreBluetooth, self).__init__(address_or_ble_device, **kwargs) + + if isinstance(address_or_ble_device, BLEDevice): + self._device_info = address_or_ble_device.details + else: + self._device_info = None self._device_info = None self._requester = None self._callbacks = {} self._services = None - self._disconnected_callback = None - def __str__(self): return "BleakClientCoreBluetooth ({})".format(self.address) @@ -58,34 +62,46 @@ async def connect(self, **kwargs) -> bool: """Connect to a specified Peripheral Keyword Args: - timeout (float): Timeout for required ``discover`` call. Defaults to 10.0. + timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. Returns: Boolean representing connection status. """ - timeout = kwargs.get("timeout", self._timeout) - device = await BleakScannerCoreBluetooth.find_device_by_address( - self.address, timeout=timeout) - - if device: - self._device_info = device.details - else: - raise BleakError( - "Device with address {} was not found".format(self.address) + if self._device_info is None: + timeout = kwargs.get("timeout", self._timeout) + device = await BleakScannerCoreBluetooth.find_device_by_address( + self.address, timeout=timeout ) + if device: + self._device_info = device.details + else: + raise BleakError( + "Device with address {} was not found".format(self.address) + ) + logger.debug("Connecting to BLE device @ {}".format(self.address)) manager = self._device_info.manager().delegate() await manager.connect_(self._device_info) - manager.disconnected_callback = self._disconnect_callback_client + manager.disconnected_callback = self._disconnected_callback_client # Now get services await self.get_services() return True + def _disconnected_callback_client(self): + """ + Callback for device disconnection. Bleak callback sends one argument as client. This is wrapper function + that gets called from the CentralManager and call actual disconnected_callback by sending client as argument + """ + logger.debug("Received disconnection callback...") + + if self._disconnected_callback is not None: + self._disconnected_callback(self) + async def disconnect(self) -> bool: """Disconnect from the peripheral device""" manager = self._device_info.manager().delegate() @@ -101,26 +117,6 @@ async def is_connected(self) -> bool: manager = self._device_info.manager().delegate() return manager.isConnected - def set_disconnected_callback( - self, callback: Callable[[BaseBleakClient], None], **kwargs - ) -> None: - """Set the disconnected callback. - Args: - callback: callback to be called on disconnection. - - """ - self._disconnected_callback = callback - - def _disconnect_callback_client(self): - """ - Callback for device disconnection. Bleak callback sends one argument as client. This is wrapper function - that gets called from the CentralManager and call actual disconnected_callback by sending client as argument - """ - logger.debug("Received disconnection callback...") - - if self._disconnected_callback is not None: - self._disconnected_callback(self) - async def pair(self, *args, **kwargs) -> bool: """Attempt to pair with a peripheral. @@ -132,10 +128,9 @@ async def pair(self, *args, **kwargs) -> bool: Reference: - - https://stackoverflow.com/questions/25254932/can-you-pair-a-bluetooth-le-device-in-an-ios-app - - https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral.html#//apple_ref/doc/uid/TP40013257-CH5-SW1 - - https://stackoverflow.com/questions/47546690/ios-bluetooth-pairing-request-dialog-can-i-know-the-users-choice - + - `Apple Docs `_ + - `Stack Overflow post #1 `_ + - `Stack Overflow post #2 `_ Returns: Boolean regarding success of pairing. diff --git a/bleak/backends/corebluetooth/device.py b/bleak/backends/corebluetooth/device.py index 453c437b..2e0e022e 100644 --- a/bleak/backends/corebluetooth/device.py +++ b/bleak/backends/corebluetooth/device.py @@ -47,7 +47,13 @@ def _update_uuids(self, advertisementData: NSDictionary): if not cbuuids: return # converting to lower case to match other platforms - self.metadata["uuids"] = [str(u).lower() for u in cbuuids] + chuuids = [str(u).lower() for u in cbuuids] + if "uuids" in self.metadata: + for uuid in chuuids: + if not uuid in self.metadata["uuids"]: + self.metadata["uuids"].append(uuid) + else: + self.metadata["uuids"] = chuuids def _update_manufacturer(self, advertisementData: NSDictionary): mfg_bytes = advertisementData.get("kCBAdvDataManufacturerData") diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index 6951803b..9faf3841 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -62,6 +62,17 @@ async def stop(self): logger.warning("stopScan method could not be called: {0}".format(e)) async def set_scanning_filter(self, **kwargs): + """Set scanning filter for the scanner. + + .. note:: + + This is not implemented for macOS yet. + + Raises: + + ``NotImplementedError`` + + """ raise NotImplementedError( "Need to evaluate which macOS versions to support first..." ) @@ -142,7 +153,9 @@ def stop_if_detected(peripheral, advertisement_data, rssi): if str(peripheral.identifier().UUIDString()).lower() == device_identifier: loop.call_soon_threadsafe(stop_scanning_event.set) - return await scanner._find_device_by_address(device_identifier, stop_scanning_event, stop_if_detected, timeout) + return await scanner._find_device_by_address( + device_identifier, stop_scanning_event, stop_if_detected, timeout + ) # macOS specific methods diff --git a/bleak/backends/device.py b/bleak/backends/device.py index 275bd12f..bdd45a1e 100644 --- a/bleak/backends/device.py +++ b/bleak/backends/device.py @@ -14,19 +14,23 @@ class BLEDevice(object): a `discover` call. - When using Windows backend, `details` attribute is a - `Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisement` object, unless + ``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisement`` object, unless it is created with the Windows.Devices.Enumeration discovery method, then is is a - `Windows.Devices.Enumeration.DeviceInformation` - - When using Linux backend, `details` attribute is a - dict with keys `path` which has the string path to the DBus device object and `props` + ``Windows.Devices.Enumeration.DeviceInformation``. + - When using Linux backend, ``details`` attribute is a + dict with keys ``path`` which has the string path to the DBus device object and ``props`` which houses the properties dictionary of the D-Bus Device. - - When using macOS backend, `details` attribute will be a CBPeripheral object + - When using macOS backend, ``details`` attribute will be a CBPeripheral object. """ def __init__(self, address, name, details=None, **kwargs): + #: The Bluetooth address of the device on this machine. self.address = address + #: The advertised name of the device. self.name = name if name else "Unknown" + #: The OS native details required for connecting to the device. self.details = details + #: Device specific details. Contains a ``uuids`` key which is a list of service UUIDs and a ``manufacturer_data`` field with a bytes-object from the advertised data. self.metadata = kwargs @property diff --git a/bleak/backends/dotnet/client.py b/bleak/backends/dotnet/client.py index 11258aa5..64e16a85 100644 --- a/bleak/backends/dotnet/client.py +++ b/bleak/backends/dotnet/client.py @@ -11,11 +11,15 @@ from functools import wraps from typing import Callable, Any, Union +from bleak.backends.device import BLEDevice from bleak.backends.dotnet.scanner import BleakScannerDotNet from bleak.exc import BleakError, BleakDotNetTaskError, CONTROLLER_ERROR_CODES from bleak.backends.client import BaseBleakClient -from bleak.backends.dotnet.discovery import discover -from bleak.backends.dotnet.utils import wrap_IAsyncOperation +from bleak.backends.dotnet.utils import ( + BleakDataReader, + BleakDataWriter, + wrap_IAsyncOperation, +) from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.service import BleakGATTServiceCollection @@ -29,7 +33,7 @@ from BleakBridge import Bridge # Import of other CLR components needed. -from System import Array, Byte, UInt64 +from System import UInt64, Object from Windows.Foundation import IAsyncOperation, TypedEventHandler from Windows.Storage.Streams import DataReader, DataWriter, IBuffer from Windows.Devices.Enumeration import ( @@ -91,18 +95,21 @@ class BleakClientDotNet(BaseBleakClient): Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel. Args: - address (str): The Bluetooth address of the BLE peripheral to connect to. + address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it. Keyword Args: - timeout (float): Timeout for required ``discover`` call. Defaults to 2.0. + timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. """ - def __init__(self, address: str, **kwargs): - super(BleakClientDotNet, self).__init__(address, **kwargs) + def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): + super(BleakClientDotNet, self).__init__(address_or_ble_device, **kwargs) # Backend specific. Python.NET objects. - self._device_info = None + if isinstance(address_or_ble_device, BLEDevice): + self._device_info = address_or_ble_device.details.BluetoothAddress + else: + self._device_info = None self._requester = None self._bridge = None @@ -122,7 +129,7 @@ async def connect(self, **kwargs) -> bool: """Connect to the specified GATT server. Keyword Args: - timeout (float): Timeout for required ``find_device_by_address`` call. Defaults to maximally 10.0 seconds. + timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. Returns: Boolean representing connection status. @@ -132,17 +139,19 @@ async def connect(self, **kwargs) -> bool: self._bridge = Bridge() # Try to find the desired device. - timeout = kwargs.get("timeout", self._timeout) - device = await BleakScannerDotNet.find_device_by_address( - self.address, timeout=timeout) - - if device: - self._device_info = device.details.BluetoothAddress - else: - raise BleakError( - "Device with address {0} was not found.".format(self.address) + if self._device_info is None: + timeout = kwargs.get("timeout", self._timeout) + device = await BleakScannerDotNet.find_device_by_address( + self.address, timeout=timeout ) + if device: + self._device_info = device.details.BluetoothAddress + else: + raise BleakError( + "Device with address {0} was not found.".format(self.address) + ) + logger.debug("Connecting to BLE device @ {0}".format(self.address)) args = [UInt64(self._device_info)] @@ -159,8 +168,17 @@ async def connect(self, **kwargs) -> bool: return_type=BluetoothLEDevice, ) + loop = asyncio.get_event_loop() + def _ConnectionStatusChanged_Handler(sender, args): - logger.debug("_ConnectionStatusChanged_Handler: " + args.ToString()) + logger.debug( + "_ConnectionStatusChanged_Handler: %d", sender.ConnectionStatus + ) + if ( + sender.ConnectionStatus == BluetoothConnectionStatus.Disconnected + and self._disconnected_callback + ): + loop.call_soon_threadsafe(self._disconnected_callback, self) self._requester.ConnectionStatusChanged += _ConnectionStatusChanged_Handler @@ -238,19 +256,6 @@ async def is_connected(self) -> bool: else: return False - def set_disconnected_callback( - self, callback: Callable[[BaseBleakClient], None], **kwargs - ) -> None: - """Set the disconnected callback. - - N.B. This is not implemented in the .NET backend yet. - - Args: - callback: callback to be called on disconnection. - - """ - raise NotImplementedError("This is not implemented in the .NET backend yet") - async def pair(self, protection_level=None, **kwargs) -> bool: """Attempts to pair with the device. @@ -376,7 +381,9 @@ async def get_services(self) -> BleakGATTServiceCollection: "Could not get GATT services: {0} (Error: 0x{1:02X}: {2})".format( _communication_statues.get(services_result.Status, ""), services_result.ProtocolError, - CONTROLLER_ERROR_CODES.get(services_result.ProtocolError, "Unknown") + CONTROLLER_ERROR_CODES.get( + services_result.ProtocolError, "Unknown" + ), ) ) else: @@ -406,7 +413,9 @@ async def get_services(self) -> BleakGATTServiceCollection: characteristics_result.Status, "" ), characteristics_result.ProtocolError, - CONTROLLER_ERROR_CODES.get(characteristics_result.ProtocolError, "Unknown") + CONTROLLER_ERROR_CODES.get( + characteristics_result.ProtocolError, "Unknown" + ), ) ) else: @@ -441,8 +450,8 @@ async def get_services(self) -> BleakGATTServiceCollection: ), descriptors_result.ProtocolError, CONTROLLER_ERROR_CODES.get( - descriptors_result.ProtocolError, - "Unknown") + descriptors_result.ProtocolError, "Unknown" + ), ) ) else: @@ -506,11 +515,8 @@ async def read_gatt_char( return_type=GattReadResult, ) if read_result.Status == GattCommunicationStatus.Success: - reader = DataReader.FromBuffer(IBuffer(read_result.Value)) - output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength) - reader.ReadBytes(output) - value = bytearray(output) - reader.Dispose() + with BleakDataReader(read_result.Value) as reader: + value = bytearray(reader.read()) logger.debug( "Read Characteristic {0} : {1}".format(characteristic.uuid, value) ) @@ -522,8 +528,8 @@ async def read_gatt_char( _communication_statues.get(read_result.Status, ""), read_result.ProtocolError, CONTROLLER_ERROR_CODES.get( - read_result.ProtocolError, - "Unknown") + read_result.ProtocolError, "Unknown" + ), ) ) else: @@ -564,11 +570,8 @@ async def read_gatt_descriptor( return_type=GattReadResult, ) if read_result.Status == GattCommunicationStatus.Success: - reader = DataReader.FromBuffer(IBuffer(read_result.Value)) - output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength) - reader.ReadBytes(output) - value = bytearray(output) - reader.Dispose() + with BleakDataReader(read_result.Value) as reader: + value = bytearray(reader.read()) logger.debug("Read Descriptor {0} : {1}".format(handle, value)) else: if read_result.Status == GattCommunicationStatus.ProtocolError: @@ -578,8 +581,8 @@ async def read_gatt_descriptor( _communication_statues.get(read_result.Status, ""), read_result.ProtocolError, CONTROLLER_ERROR_CODES.get( - read_result.ProtocolError, - "Unknown") + read_result.ProtocolError, "Unknown" + ), ) ) else: @@ -615,21 +618,21 @@ async def write_gatt_char( if not characteristic: raise BleakError("Characteristic {} was not found!".format(char_specifier)) - writer = DataWriter() - writer.WriteBytes(Array[Byte](data)) - response = ( - GattWriteOption.WriteWithResponse - if response - else GattWriteOption.WriteWithoutResponse - ) - write_result = await wrap_IAsyncOperation( - IAsyncOperation[GattWriteResult]( - characteristic.obj.WriteValueWithResultAsync( - writer.DetachBuffer(), response - ) - ), - return_type=GattWriteResult, - ) + with BleakDataWriter(data) as writer: + response = ( + GattWriteOption.WriteWithResponse + if response + else GattWriteOption.WriteWithoutResponse + ) + write_result = await wrap_IAsyncOperation( + IAsyncOperation[GattWriteResult]( + characteristic.obj.WriteValueWithResultAsync( + writer.detach_buffer(), response + ) + ), + return_type=GattWriteResult, + ) + if write_result.Status == GattCommunicationStatus.Success: logger.debug( "Write Characteristic {0} : {1}".format(characteristic.uuid, data) @@ -643,8 +646,8 @@ async def write_gatt_char( _communication_statues.get(write_result.Status, ""), write_result.ProtocolError, CONTROLLER_ERROR_CODES.get( - write_result.ProtocolError, - "Unknown") + write_result.ProtocolError, "Unknown" + ), ) ) else: @@ -668,14 +671,14 @@ async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None: if not descriptor: raise BleakError("Descriptor with handle {0} was not found!".format(handle)) - writer = DataWriter() - writer.WriteBytes(Array[Byte](data)) - write_result = await wrap_IAsyncOperation( - IAsyncOperation[GattWriteResult]( - descriptor.obj.WriteValueAsync(writer.DetachBuffer()) - ), - return_type=GattWriteResult, - ) + with BleakDataWriter(data) as writer: + write_result = await wrap_IAsyncOperation( + IAsyncOperation[GattWriteResult]( + descriptor.obj.WriteValueAsync(writer.DetachBuffer()) + ), + return_type=GattWriteResult, + ) + if write_result.Status == GattCommunicationStatus.Success: logger.debug("Write Descriptor {0} : {1}".format(handle, data)) else: @@ -687,8 +690,8 @@ async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None: _communication_statues.get(write_result.Status, ""), write_result.ProtocolError, CONTROLLER_ERROR_CODES.get( - write_result.ProtocolError, - "Unknown") + write_result.ProtocolError, "Unknown" + ), ) ) else: @@ -856,10 +859,8 @@ def _notification_wrapper(func: Callable, loop: asyncio.AbstractEventLoop): def dotnet_notification_parser(sender: Any, args: Any): # Return only the UUID string representation as sender. # Also do a conversion from System.Bytes[] to bytearray. - reader = DataReader.FromBuffer(args.CharacteristicValue) - output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength) - reader.ReadBytes(output) - reader.Dispose() + with BleakDataReader(args.CharacteristicValue) as reader: + output = reader.read() return loop.call_soon_threadsafe( func, sender.AttributeHandle, bytearray(output) diff --git a/bleak/backends/dotnet/discovery.py b/bleak/backends/dotnet/discovery.py index 5a105335..56e39c46 100644 --- a/bleak/backends/dotnet/discovery.py +++ b/bleak/backends/dotnet/discovery.py @@ -23,7 +23,8 @@ BluetoothLEScanningMode, BluetoothLEAdvertisementType, ) -from Windows.Storage.Streams import DataReader, IBuffer + +from bleak.backends.dotnet.utils import BleakDataReader logger = logging.getLogger(__name__) _here = pathlib.Path(__file__).parent @@ -116,12 +117,8 @@ def AdvertisementWatcher_Stopped(sender, e): uuids.append(u.ToString()) data = {} for m in d.Advertisement.ManufacturerData: - md = IBuffer(m.Data) - b = Array.CreateInstance(Byte, md.Length) - reader = DataReader.FromBuffer(md) - reader.ReadBytes(b) - data[m.CompanyId] = bytes(b) - reader.Dispose() + with BleakDataReader(m.Data) as reader: + data[m.CompanyId] = reader.read() local_name = d.Advertisement.LocalName if not local_name and d.BluetoothAddress in scan_responses: local_name = scan_responses[d.BluetoothAddress].Advertisement.LocalName diff --git a/bleak/backends/dotnet/scanner.py b/bleak/backends/dotnet/scanner.py index a63b3c32..066097e1 100644 --- a/bleak/backends/dotnet/scanner.py +++ b/bleak/backends/dotnet/scanner.py @@ -6,20 +6,19 @@ from typing import Callable, Any, Union, List from bleak.backends.device import BLEDevice +from bleak.backends.dotnet.utils import BleakDataReader from bleak.exc import BleakError, BleakDotNetTaskError from bleak.backends.scanner import BaseBleakScanner # Import of Bleak CLR->UWP Bridge. It is not needed here, but it enables loading of Windows.Devices from BleakBridge import Bridge -from System import Array, Byte -from Windows.Devices import Enumeration from Windows.Devices.Bluetooth.Advertisement import ( BluetoothLEAdvertisementWatcher, BluetoothLEScanningMode, BluetoothLEAdvertisementType, ) -from Windows.Storage.Streams import DataReader, IBuffer +from Windows.Foundation import TypedEventHandler logger = logging.getLogger(__name__) _here = pathlib.Path(__file__).parent @@ -41,17 +40,20 @@ def _format_event_args(e): class BleakScannerDotNet(BaseBleakScanner): """The native Windows Bleak BLE Scanner. - Implemented using `pythonnet `_, a package that provides an integration to the .NET - Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel. + Implemented using `pythonnet `_, a package that provides an integration to + the .NET Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel. Keyword Args: - scanning mode (str): Set to "Passive" to avoid the "Active" scanning mode. - 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. + + scanning mode (str): Set to ``Passive`` to avoid the ``Active`` scanning mode. + + 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. """ @@ -120,12 +122,12 @@ async def set_scanning_filter(self, **kwargs): """Set a scanning filter for the BleakScanner. 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. + 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. """ if "SignalStrengthFilter" in kwargs: @@ -158,11 +160,8 @@ def parse_eventargs(event_args): uuids.append(u.ToString()) data = {} for m in event_args.Advertisement.ManufacturerData: - md = IBuffer(m.Data) - b = Array.CreateInstance(Byte, md.Length) - reader = DataReader.FromBuffer(md) - reader.ReadBytes(b) - data[m.CompanyId] = bytes(b) + with BleakDataReader(m.Data) as reader: + data[m.CompanyId] = reader.read() local_name = event_args.Advertisement.LocalName return BLEDevice( bdaddr, local_name, event_args, uuids=uuids, manufacturer_data=data @@ -176,8 +175,8 @@ def register_detection_callback(self, callback: Callable): Args: callback: Function accepting two arguments: - sender (Windows.Devices.Bluetooth.AdvertisementBluetoothLEAdvertisementWatcher) and - eventargs (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementReceivedEventArgs) + sender (``Windows.Devices.Bluetooth.AdvertisementBluetoothLEAdvertisementWatcher``) and + eventargs (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementReceivedEventArgs``) """ self._callback = callback @@ -215,19 +214,26 @@ async def find_device_by_address( """A convenience method for obtaining a ``BLEDevice`` object specified by Bluetooth address. Args: + device_identifier (str): The Bluetooth address of the Bluetooth peripheral. - timeout (float): Optional timeout to wait for detection of specified peripheral before giving up. Defaults to 10.0 seconds. + + timeout (float): Optional timeout to wait for detection of specified peripheral + before giving up. Defaults to 10.0 seconds. Keyword Args: - scanning mode (str): Set to "Passive" to avoid the "Active" scanning mode. - 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. + + scanning mode (str): Set to ``Passive`` to avoid the ``Active`` scanning mode. + + 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. Returns: + The ``BLEDevice`` sought or ``None`` if not detected. """ @@ -241,4 +247,6 @@ def stop_if_detected(sender, event_args): if event_args.BluetoothAddress == ulong_id: loop.call_soon_threadsafe(stop_scanning_event.set) - return await scanner._find_device_by_address(device_identifier, stop_scanning_event, stop_if_detected, timeout) + return await scanner._find_device_by_address( + device_identifier, stop_scanning_event, stop_if_detected, timeout + ) diff --git a/bleak/backends/dotnet/utils.py b/bleak/backends/dotnet/utils.py index 1b43cc02..f8af8acc 100644 --- a/bleak/backends/dotnet/utils.py +++ b/bleak/backends/dotnet/utils.py @@ -18,6 +18,8 @@ IAsyncOperation, AsyncStatus, ) +from System import Array, Byte +from Windows.Storage.Streams import DataReader, DataWriter, IBuffer async def wrap_Task(task): @@ -74,3 +76,48 @@ async def wrap_IAsyncOperation(op, return_type): else: # TODO: Handle IsCancelled. raise BleakDotNetTaskError("IAsyncOperation Status: {0}".format(op.Status)) + + +class BleakDataReader: + def __init__(self, buffer_com_object): + + self.reader = None + self.buffer = IBuffer(buffer_com_object) + + def __enter__(self): + self.reader = DataReader.FromBuffer(self.buffer) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.reader.DetachBuffer() + self.reader.Dispose() + self.reader = None + self.buffer = None + + def read(self) -> bytes: + b = Array.CreateInstance(Byte, self.reader.UnconsumedBufferLength) + self.reader.ReadBytes(b) + py_b = bytes(b) + del b + return py_b + + +class BleakDataWriter: + def __init__(self, data): + self.data = data + + def __enter__(self): + self.writer = DataWriter() + self.writer.WriteBytes(Array[Byte](self.data)) + return self + + def detach_buffer(self): + return self.writer.DetachBuffer() + + def __exit__(self, exc_type, exc_val, exc_tb): + try: + self.writer.Dispose() + except: + pass + del self.writer + self.writer = None diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py index 8bde1f6e..cf77b3bf 100644 --- a/bleak/backends/scanner.py +++ b/bleak/backends/scanner.py @@ -74,7 +74,9 @@ async def get_discovered_devices(self) -> List[BLEDevice]: @classmethod @abc.abstractmethod - async def find_device_by_address(cls, device_identifier: str, timeout: float = 10.0) -> BLEDevice: + async def find_device_by_address( + cls, device_identifier: str, timeout: float = 10.0 + ) -> BLEDevice: """A convenience method for obtaining a ``BLEDevice`` object specified by Bluetooth address or (macOS) UUID address. Args: @@ -87,7 +89,9 @@ async def find_device_by_address(cls, device_identifier: str, timeout: float = 1 """ raise NotImplementedError() - async def _find_device_by_address(self, device_identifier, stop_scanning_event, stop_if_detected_callback, timeout): + async def _find_device_by_address( + self, device_identifier, stop_scanning_event, stop_if_detected_callback, timeout + ): """Internal method for performing find by address work.""" self.register_detection_callback(stop_if_detected_callback) @@ -107,4 +111,3 @@ async def _find_device_by_address(self, device_identifier, stop_scanning_event, await self.stop() return device - diff --git a/bleak/backends/service.py b/bleak/backends/service.py index 79c678aa..1865ff71 100644 --- a/bleak/backends/service.py +++ b/bleak/backends/service.py @@ -147,8 +147,8 @@ def get_characteristic( def add_descriptor(self, descriptor: BleakGATTDescriptor): """Add a :py:class:`~BleakGATTDescriptor` to the service collection. - Should not be used by end user, but rather by `bleak` itself. - """ + Should not be used by end user, but rather by `bleak` itself. + """ if descriptor.handle not in self.__descriptors: self.__descriptors[descriptor.handle] = descriptor self.__characteristics[descriptor.characteristic_handle].add_descriptor( diff --git a/bleak/uuids.py b/bleak/uuids.py index 8d800379..0e02bdaf 100644 --- a/bleak/uuids.py +++ b/bleak/uuids.py @@ -636,71 +636,71 @@ 0xFFFE: "Alliance for Wireless Power (A4WP)", 0xFFFD: "Fast IDentity Online Alliance (FIDO)", # Mesh Characteristics - 0x2AE0: 'Average Current', - 0x2AE1: 'Average Voltage', - 0x2AE2: 'Boolean', - 0x2AE3: 'Chromatic Distance From Planckian', - 0x2B1C: 'Chromaticity Coordinate', - 0x2AE4: 'Chromaticity Coordinates', - 0x2AE5: 'Chromaticity In CCT And Duv Values', - 0x2AE6: 'Chromaticity Tolerance', - 0x2AE7: 'CIE 13.3-1995 Color Rendering Index', - 0x2AE8: 'Coefficient', - 0x2AE9: 'Correlated Color Temperature', - 0x2AEA: 'Count 16', - 0x2AEB: 'Count 24', - 0x2AEC: 'Country Code', - 0x2AED: 'Date UTC', - 0x2AEE: 'Electric Current', - 0x2AEF: 'Electric Current Range', - 0x2AF0: 'Electric Current Specification', - 0x2AF1: 'Electric Current Statistics', - 0x2AF2: 'Energy', - 0x2AF3: 'Energy In A Period Of Day', - 0x2AF4: 'Event Statistics', - 0x2AF5: 'Fixed String 16', - 0x2AF6: 'Fixed String 24', - 0x2AF7: 'Fixed String 36', - 0x2AF8: 'Fixed String 8', - 0x2AF9: 'Generic Level', - 0x2AFA: 'Global Trade Item Number', - 0x2AFB: 'Illuminance', - 0x2AFC: 'Luminous Efficacy', - 0x2AFD: 'Luminous Energy', - 0x2AFE: 'Luminous Exposure', - 0x2AFF: 'Luminous Flux', - 0x2B00: 'Luminous Flux Range', - 0x2B01: 'Luminous Intensity', - 0x2B02: 'Mass Flow', - 0x2ADB: 'Mesh Provisioning Data In', - 0x2ADC: 'Mesh Provisioning Data Out', - 0x2ADD: 'Mesh Proxy Data In', - 0x2ADE: 'Mesh Proxy Data Out', - 0x2B03: 'Perceived Lightness', - 0x2B04: 'Percentage 8', - 0x2B05: 'Power', - 0x2B06: 'Power Specification', - 0x2B07: 'Relative Runtime In A Current Range', - 0x2B08: 'Relative Runtime In A Generic Level Range', - 0x2B0B: 'Relative Value In A Period of Day', - 0x2B0C: 'Relative Value In A Temperature Range', - 0x2B09: 'Relative Value In A Voltage Range', - 0x2B0A: 'Relative Value In An Illuminance Range', - 0x2B0D: 'Temperature 8', - 0x2B0E: 'Temperature 8 In A Period Of Day', - 0x2B0F: 'Temperature 8 Statistics', - 0x2B10: 'Temperature Range', - 0x2B11: 'Temperature Statistics', - 0x2B12: 'Time Decihour 8', - 0x2B13: 'Time Exponential 8', - 0x2B14: 'Time Hour 24', - 0x2B15: 'Time Millisecond 24', - 0x2B16: 'Time Second 16', - 0x2B17: 'Time Second 8', - 0x2B18: 'Voltage', - 0x2B19: 'Voltage Specification', - 0x2B1A: 'Voltage Statistics', - 0x2B1B: 'Volume Flow', + 0x2AE0: "Average Current", + 0x2AE1: "Average Voltage", + 0x2AE2: "Boolean", + 0x2AE3: "Chromatic Distance From Planckian", + 0x2B1C: "Chromaticity Coordinate", + 0x2AE4: "Chromaticity Coordinates", + 0x2AE5: "Chromaticity In CCT And Duv Values", + 0x2AE6: "Chromaticity Tolerance", + 0x2AE7: "CIE 13.3-1995 Color Rendering Index", + 0x2AE8: "Coefficient", + 0x2AE9: "Correlated Color Temperature", + 0x2AEA: "Count 16", + 0x2AEB: "Count 24", + 0x2AEC: "Country Code", + 0x2AED: "Date UTC", + 0x2AEE: "Electric Current", + 0x2AEF: "Electric Current Range", + 0x2AF0: "Electric Current Specification", + 0x2AF1: "Electric Current Statistics", + 0x2AF2: "Energy", + 0x2AF3: "Energy In A Period Of Day", + 0x2AF4: "Event Statistics", + 0x2AF5: "Fixed String 16", + 0x2AF6: "Fixed String 24", + 0x2AF7: "Fixed String 36", + 0x2AF8: "Fixed String 8", + 0x2AF9: "Generic Level", + 0x2AFA: "Global Trade Item Number", + 0x2AFB: "Illuminance", + 0x2AFC: "Luminous Efficacy", + 0x2AFD: "Luminous Energy", + 0x2AFE: "Luminous Exposure", + 0x2AFF: "Luminous Flux", + 0x2B00: "Luminous Flux Range", + 0x2B01: "Luminous Intensity", + 0x2B02: "Mass Flow", + 0x2ADB: "Mesh Provisioning Data In", + 0x2ADC: "Mesh Provisioning Data Out", + 0x2ADD: "Mesh Proxy Data In", + 0x2ADE: "Mesh Proxy Data Out", + 0x2B03: "Perceived Lightness", + 0x2B04: "Percentage 8", + 0x2B05: "Power", + 0x2B06: "Power Specification", + 0x2B07: "Relative Runtime In A Current Range", + 0x2B08: "Relative Runtime In A Generic Level Range", + 0x2B0B: "Relative Value In A Period of Day", + 0x2B0C: "Relative Value In A Temperature Range", + 0x2B09: "Relative Value In A Voltage Range", + 0x2B0A: "Relative Value In An Illuminance Range", + 0x2B0D: "Temperature 8", + 0x2B0E: "Temperature 8 In A Period Of Day", + 0x2B0F: "Temperature 8 Statistics", + 0x2B10: "Temperature Range", + 0x2B11: "Temperature Statistics", + 0x2B12: "Time Decihour 8", + 0x2B13: "Time Exponential 8", + 0x2B14: "Time Hour 24", + 0x2B15: "Time Millisecond 24", + 0x2B16: "Time Second 16", + 0x2B17: "Time Second 8", + 0x2B18: "Voltage", + 0x2B19: "Voltage Specification", + 0x2B1A: "Voltage Statistics", + 0x2B1B: "Volume Flow", } uuid128_dict = { diff --git a/docs/api.rst b/docs/api.rst index 8dbbdb47..8bfa4fd2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,28 +1,59 @@ Interfaces, exceptions and utils ================================ -Connection Client Interface ---------------------------- +Connection Clients +------------------ -.. automodule:: bleak.backends.client +Windows +~~~~~~~ + +.. automodule:: bleak.backends.dotnet.client + :members: + +macOS +~~~~~ + +.. automodule:: bleak.backends.corebluetooth.client :members: -Scanning Client Interface -------------------------- +Linux Distributions with BlueZ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: bleak.backends.bluezdbus.client + :members: + +Scanning Clients +---------------- + +Windows +~~~~~~~ -.. automodule:: bleak.backends.scanner +.. automodule:: bleak.backends.dotnet.scanner :members: -Interface for BLE devices -------------------------- +macOS +~~~~~ + +.. automodule:: bleak.backends.corebluetooth.scanner + :members: + +Linux Distributions with BlueZ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: bleak.backends.bluezdbus.scanner + :members: + + +Class representing BLE devices +------------------------------ Generated by :py:meth:`bleak.discover` and :py:class:`bleak.backends.scanning.BaseBleakScanner`. .. automodule:: bleak.backends.device :members: -Interfaces for GATT objects ---------------------------- +GATT objects +------------ .. automodule:: bleak.backends.service :members: diff --git a/docs/conf.py b/docs/conf.py index 37ed91f0..451b463a 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,26 @@ # All configuration values have a default; values that are commented out # serve to show the default. + +windows_autodoc_mock_import = ["clr", "Windows", "System"] +linux_autodoc_mock_import = [ + "twisted", + "txdbus", +] +macos_autodoc_mock_import = [ + "objc", + "Foundation", + "CoreBluetooth", + "libdispatch", +] +autodoc_mock_imports = list( + set( + windows_autodoc_mock_import + + macos_autodoc_mock_import + + linux_autodoc_mock_import + ) +) + import sys import os @@ -40,8 +60,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] - +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.napoleon"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -56,7 +75,7 @@ # General information about the project. project = u"bleak" -copyright = u"2018, Henrik Blidh" +copyright = u"2020, Henrik Blidh" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout @@ -112,6 +131,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "default" +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the diff --git a/docs/index.rst b/docs/index.rst index 318d3054..989a49ac 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ bleak ===== -.. image:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo.png +.. figure:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo.png :target: https://github.com/hbldh/bleak :alt: Bleak Logo :width: 50% diff --git a/docs/scanning.rst b/docs/scanning.rst index 036ad602..dc3dff80 100644 --- a/docs/scanning.rst +++ b/docs/scanning.rst @@ -128,6 +128,6 @@ To be written. In the meantime, check Scanning filter examples in Core Bluetooth backend -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To be implemented. Exists in a draft in `PR #209 `_. diff --git a/examples/connect_by_bledevice.py b/examples/connect_by_bledevice.py new file mode 100644 index 00000000..fe9978e5 --- /dev/null +++ b/examples/connect_by_bledevice.py @@ -0,0 +1,24 @@ +""" +Connect by BLEDevice +""" + +import asyncio +import platform + +from bleak import BleakClient, BleakScanner + + +async def print_services(mac_addr: str): + device = await BleakScanner.find_device_by_address(mac_addr) + async with BleakClient(device) as client: + svcs = await client.get_services() + print("Services:", svcs) + + +mac_addr = ( + "24:71:89:cc:09:05" + if platform.system() != "Darwin" + else "B9EA5233-37EF-4DD6-87A8-2A875E821C46" +) +loop = asyncio.get_event_loop() +loop.run_until_complete(print_services(mac_addr)) diff --git a/examples/disconnect_callback.py b/examples/disconnect_callback.py index 15002e56..14cbd51c 100644 --- a/examples/disconnect_callback.py +++ b/examples/disconnect_callback.py @@ -2,7 +2,7 @@ Disconnect callback ------------------- -An example showing how the `set_disconnect_callback` can be used on BlueZ backend. +An example showing how the `set_disconnected_callback` can be used on BlueZ backend. Updated on 2019-09-07 by hbldh @@ -10,19 +10,25 @@ import asyncio -from bleak import BleakClient +from bleak import BleakClient, discover -async def show_disconnect_handling(mac_addr: str): - async with BleakClient(mac_addr) as client: - disconnected_event = asyncio.Event() +async def show_disconnect_handling(): + devs = await discover() + if not devs: + print("No devices found, try again later.") + return - def disconnect_callback(client, future): - print("Disconnected callback called!") - asyncio.get_event_loop().call_soon_threadsafe(disconnected_event.set) + disconnected_event = asyncio.Event() - client.set_disconnected_callback(disconnect_callback) - print("Sleeping until device disconnects according to BlueZ...") + def disconnected_callback(client): + print("Disconnected callback called!") + disconnected_event.set() + + async with BleakClient( + devs[0], disconnected_callback=disconnected_callback + ) as client: + print("Sleeping until device disconnects...") await disconnected_event.wait() print("Connected: {0}".format(await client.is_connected())) await asyncio.sleep( @@ -30,6 +36,5 @@ def disconnect_callback(client, future): ) # Sleep a bit longer to allow _cleanup to remove all BlueZ notifications nicely... -mac_addr = "24:71:89:cc:09:05" loop = asyncio.get_event_loop() -loop.run_until_complete(show_disconnect_handling(mac_addr)) +loop.run_until_complete(show_disconnect_handling()) diff --git a/examples/sensortag.py b/examples/sensortag.py index b8472ec5..32982313 100644 --- a/examples/sensortag.py +++ b/examples/sensortag.py @@ -87,7 +87,7 @@ BATTERY_LEVEL_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format( uuid16_dict.get("Battery Level") ) -KEY_PRESS_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0xffe1) +KEY_PRESS_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0xFFE1) # I/O test points on SensorTag. IO_DATA_CHAR_UUID = "f000aa65-0451-4000-b000-000000000000" IO_CONFIG_CHAR_UUID = "f000aa66-0451-4000-b000-000000000000" @@ -141,7 +141,7 @@ async def run(address, debug=False): def keypress_handler(sender, data): print("{0}: {1}".format(sender, data)) - write_value = bytearray([0xa0]) + write_value = bytearray([0xA0]) value = await client.read_gatt_char(IO_DATA_CHAR_UUID) print("I/O Data Pre-Write Value: {0}".format(value)) diff --git a/requirements_dev.txt b/requirements_dev.txt index 59ed53cd..789fe38a 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -2,6 +2,7 @@ pip>=18.0 bump2version==1.0.0 wheel>=0.32.2 watchdog>=0.8.3 +black>=20.8b1 flake8>=3.5.0 tox>=3.1.3 coverage>=4.5.1 diff --git a/setup.py b/setup.py index 9dae2bd3..adcb0a1e 100644 --- a/setup.py +++ b/setup.py @@ -107,7 +107,6 @@ def run(self): "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",