diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e4aef087..1e2b7e09 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,23 @@ 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.14.3`_ (2022-04-29) +====================== + +Changed +------- + +* Suppress macOS 12 scanner bug error message for macOS 12.3 and higher. Fixes #720. +* Added filters ``Discoverable`` and ``Pattern`` to BlueZ D-Bus scanner. Fixes #790. + +Fixed +----- + +* Fixed reading the battery level returns a zero-filled ``bytearray`` on BlueZ >= 5.48. Fixes #750. +* Fixed unpairing does not work on windows with WinRT. Fixes #699 +* Fixed leak of ``_disconnect_futures`` in ``CentralManagerDelegate``. +* Fixed callback not removed from ``_disconnect_callbacks`` on disconnect in ``CentralManagerDelegate``. + `0.14.2`_ (2022-01-26) ====================== @@ -648,7 +665,8 @@ Fixed * Bleak created. -.. _Unreleased: https://github.com/hbldh/bleak/compare/v0.14.2...develop +.. _Unreleased: https://github.com/hbldh/bleak/compare/v0.14.3...develop +.. _0.14.3: https://github.com/hbldh/bleak/compare/v0.14.2...v0.14.3 .. _0.14.2: https://github.com/hbldh/bleak/compare/v0.14.1...v0.14.2 .. _0.14.1: https://github.com/hbldh/bleak/compare/v0.14.0...v0.14.1 .. _0.14.0: https://github.com/hbldh/bleak/compare/v0.13.0...v0.14.0 diff --git a/Pipfile b/Pipfile index 25053cb5..632498ee 100644 --- a/Pipfile +++ b/Pipfile @@ -23,7 +23,7 @@ pyyaml = "*" wheel = "*" watchdog = "*" coverage = "*" -black = "*" +black = ">=22.1.0" [pipenv] allow_prereleases = true diff --git a/README.rst b/README.rst index 7a695439..fdad0440 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,10 @@ bleak .. image:: https://img.shields.io/pypi/v/bleak.svg :target: https://pypi.python.org/pypi/bleak + +.. image:: https://img.shields.io/pypi/dm/bleak.svg + :target: https://pypi.python.org/pypi/bleak + :alt: PyPI - Downloads .. image:: https://readthedocs.org/projects/bleak/badge/?version=latest :target: https://bleak.readthedocs.io/en/latest/?badge=latest diff --git a/bleak/__version__.py b/bleak/__version__.py index 5c9fd149..fcf568e9 100644 --- a/bleak/__version__.py +++ b/bleak/__version__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = "0.14.2" +__version__ = "0.14.3" diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 2f1c8499..f8053e7d 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -640,7 +640,7 @@ async def read_gatt_char( ) assert_reply(reply) # Simulate regular characteristics read to be consistent over all platforms. - value = bytearray(reply.body[0]["Percentage"].value) + value = bytearray([reply.body[0]["Percentage"].value]) logger.debug( "Read Battery Level {0} | {1}: {2}".format( char_specifier, self._device_path, value diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py index 2d5d72bf..d241c027 100644 --- a/bleak/backends/bluezdbus/scanner.py +++ b/bleak/backends/bluezdbus/scanner.py @@ -207,12 +207,16 @@ def set_scanning_filter(self, **kwargs): self._filters[k] = Variant("as", v) elif k == "RSSI": self._filters[k] = Variant("n", v) - elif k == "DuplicateData": - self._filters[k] = Variant("b", v) elif k == "Pathloss": self._filters[k] = Variant("n", v) elif k == "Transport": self._filters[k] = Variant("s", v) + elif k == "DuplicateData": + self._filters[k] = Variant("b", v) + elif k == "Discoverable": + self._filters[k] = Variant("b", v) + elif k == "Pattern": + self._filters[k] = Variant("s", v) else: logger.warning("Filter '%s' is not currently supported." % k) diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py index 5fc3e6dd..ac83c9f2 100644 --- a/bleak/backends/corebluetooth/CentralManagerDelegate.py +++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py @@ -185,7 +185,7 @@ async def disconnect(self, peripheral: CBPeripheral) -> None: self.central_manager.cancelPeripheralConnection_(peripheral) await future finally: - del self._disconnect_callbacks[peripheral.identifier()] + del self._disconnect_futures[peripheral.identifier()] @objc.python_method def _changed_is_scanning(self, is_scanning: bool) -> None: @@ -353,7 +353,8 @@ def did_disconnect_peripheral( else: future.set_result(None) - callback = self._disconnect_callbacks.get(peripheral.identifier()) + callback = self._disconnect_callbacks.pop(peripheral.identifier(), None) + if callback is not None: callback() diff --git a/bleak/backends/corebluetooth/characteristic.py b/bleak/backends/corebluetooth/characteristic.py index c6c76627..477543c9 100644 --- a/bleak/backends/corebluetooth/characteristic.py +++ b/bleak/backends/corebluetooth/characteristic.py @@ -63,7 +63,7 @@ def __init__(self, obj: CBCharacteristic): # self.__props = obj.properties() self.__props: List[str] = [ _GattCharacteristicsPropertiesEnum[v][0] - for v in [2 ** n for n in range(10)] + for v in [2**n for n in range(10)] if (self.obj.properties() & v) ] self._uuid: str = cb_uuid_to_str(self.obj.UUID()) diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py index cd28afa7..cf1fc3fe 100644 --- a/bleak/backends/corebluetooth/scanner.py +++ b/bleak/backends/corebluetooth/scanner.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional import objc -from Foundation import NSArray, NSUUID +from Foundation import NSArray, NSUUID, NSBundle from CoreBluetooth import CBPeripheral from bleak.backends.corebluetooth.CentralManagerDelegate import CentralManagerDelegate @@ -35,7 +35,7 @@ class BleakScannerCoreBluetooth(BaseBleakScanner): **service_uuids (List[str]): Optional list of service UUIDs to filter on. Only advertisements containing this advertising data will be received. Required on - macOS 12 and later. + macOS 12 and later (unless you create an app with ``py2app``). """ def __init__(self, **kwargs): @@ -43,10 +43,16 @@ def __init__(self, **kwargs): self._identifiers: Optional[Dict[NSUUID, Dict[str, Any]]] = None self._manager = CentralManagerDelegate.alloc().init() self._timeout: float = kwargs.get("timeout", 5.0) - if objc.macos_available(12, 0) and not self._service_uuids: - logger.error( - "macOS 12 requires non-empty service_uuids kwarg, otherwise no advertisement data will be received" - ) + if ( + objc.macos_available(12, 0) + and not objc.macos_available(12, 3) + and not self._service_uuids + ): + # See https://github.com/hbldh/bleak/issues/720 + if NSBundle.mainBundle().bundleIdentifier() == "org.python.python": + logger.error( + "macOS 12.0, 12.1 and 12.2 require non-empty service_uuids kwarg, otherwise no advertisement data will be received" + ) async def start(self): self._identifiers = {} diff --git a/bleak/backends/winrt/characteristic.py b/bleak/backends/winrt/characteristic.py index 8796112b..7847422b 100644 --- a/bleak/backends/winrt/characteristic.py +++ b/bleak/backends/winrt/characteristic.py @@ -67,7 +67,7 @@ def __init__(self, obj: GattCharacteristicProperties): self.__descriptors = [] self.__props = [ _GattCharacteristicsPropertiesMap[v][0] - for v in [2 ** n for n in range(10)] + for v in [2**n for n in range(10)] if (self.obj.characteristic_properties & v) ] diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index e2e30aaf..0005e9b6 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -31,6 +31,7 @@ GattSession, ) from bleak_winrt.windows.devices.enumeration import ( + DeviceInformation, DevicePairingKinds, DevicePairingResultStatus, DeviceUnpairingResultStatus, @@ -350,15 +351,18 @@ async def pair(self, protection_level: int = None, **kwargs) -> bool: Boolean regarding success of pairing. """ - + # New local device information object created since the object from the requester isn't updated + device_information = await DeviceInformation.create_from_id_async( + self._requester.device_information.id + ) if ( - self._requester.device_information.pairing.can_pair - and not self._requester.device_information.pairing.is_paired + device_information.pairing.can_pair + and not device_information.pairing.is_paired ): # Currently only supporting Just Works solutions... ceremony = DevicePairingKinds.CONFIRM_ONLY - custom_pairing = self._requester.device_information.pairing.custom + custom_pairing = device_information.pairing.custom def handler(sender, args): args.accept() @@ -395,7 +399,7 @@ def handler(sender, args): ) return True else: - return self._requester.device_information.pairing.is_paired + return device_information.pairing.is_paired async def unpair(self) -> bool: """Attempts to unpair from the device. @@ -407,10 +411,12 @@ async def unpair(self) -> bool: """ - if self._requester.device_information.pairing.is_paired: - unpairing_result = ( - await self._requester.device_information.pairing.unpair_async() - ) + # New local device information object created since the object from the requester isn't updated + device_information = await DeviceInformation.create_from_id_async( + self._requester.device_information.id + ) + if device_information.pairing.is_paired: + unpairing_result = await device_information.pairing.unpair_async() if unpairing_result.status not in ( DevicePairingResultStatus.PAIRED, @@ -426,7 +432,7 @@ async def unpair(self) -> bool: logger.info("Unpaired with device.") return True - return not self._requester.device_information.pairing.is_paired + return not device_information.pairing.is_paired # GATT services methods diff --git a/docs/conf.py b/docs/conf.py index a1ebce93..f4bc9f0a 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,8 +73,8 @@ master_doc = "index" # General information about the project. -project = u"bleak" -copyright = u"2020, Henrik Blidh" +project = "bleak" +copyright = "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 @@ -225,7 +225,7 @@ # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ - ("index", "bleak.tex", u"bleak Documentation", u"Henrik Blidh", "manual") + ("index", "bleak.tex", "bleak Documentation", "Henrik Blidh", "manual") ] # The name of an image file (relative to this directory) to place at @@ -253,7 +253,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "bleak", u"bleak Documentation", [u"Henrik Blidh"], 1)] +man_pages = [("index", "bleak", "bleak Documentation", ["Henrik Blidh"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -268,8 +268,8 @@ ( "index", "bleak", - u"bleak Documentation", - u"Henrik Blidh", + "bleak Documentation", + "Henrik Blidh", "bleak", "One line description of project.", "Miscellaneous", diff --git a/docs/images/macos-privacy-bluetooth.png b/docs/images/macos-privacy-bluetooth.png new file mode 100644 index 00000000..7e8744c5 Binary files /dev/null and b/docs/images/macos-privacy-bluetooth.png differ diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 6c9fa0a5..523420d3 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -15,6 +15,50 @@ crash with an ``ImportError`` similar to:: To fix the error, change the name of the script to something other than ``bleak.py``. +---------- +macOS Bugs +---------- + +Bleak crashes with SIGABRT on macOS +=================================== + +If you see a crash similar to this:: + + Crashed Thread: 1 Dispatch queue: com.apple.root.default-qos + + Exception Type: EXC_CRASH (SIGABRT) + Exception Codes: 0x0000000000000000, 0x0000000000000000 + Exception Note: EXC_CORPSE_NOTIFY + + Termination Reason: Namespace TCC, Code 0 + This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSBluetoothAlwaysUsageDescription key with a string value explaining to the user how the app uses this data. + +It is not a problem with Bleak. It is a problem with your terminal application. + +Ideally, the terminal application should be fixed by adding ``NSBluetoothAlwaysUsageDescription`` +to the ``Info.plist`` file (`example `_). + +It is also possible to manually add the app to the list of Bluetooth apps in +the *Privacy* settings in the macOS *System Preferences*. + +.. image:: images/macos-privacy-bluetooth.png + + +No devices found when scanning on macOS 12 +========================================== + +A bug was introduced in macOS 12.0 that causes scanning to not work unless a +list of service UUIDs is provided to ``BleakScanner``. This bug was fixed in +macOS 12.3. On the affected version, users of bleak will see the following +error logged: + +.. code-block:: none + + macOS 12.0, 12.1 and 12.2 require non-empty service_uuids kwarg, otherwise no advertisement data will be received + +See `#635 `_ and +`#720 `_ for more information +including some partial workarounds if you need to support these macOS versions. -------------- Enable Logging @@ -38,6 +82,63 @@ Windows Command Prompt:: Then run your Python script in the same terminal. +----------------------------------------------- +Connecting to multiple devices at the same time +----------------------------------------------- + +If you're having difficulty connecting to multiple devices, try to do a scan first and +pass the returned ``BLEDevice`` objects to ``BleakClient`` calls. + +Python:: + + import asyncio + from typing import Sequence + + from bleak import BleakClient, BleakScanner + from bleak.backends.device import BLEDevice + + + async def find_all_devices_services() + scanner = BleakScanner() + devices: Sequence[BLEDevice] = scanner.discover(timeout=5.0) + for d in devices: + async with BleakClient(d) as client: + print(await client.get_services()) + + + asyncio.run(find_all_devices_services()) + + +----------------------------------------- +Pass more parameters to a notify callback +----------------------------------------- + +If you need a way to pass more parameters to the notify callback, please use +``functools.partial`` to pass in more arguments. + +Issue #759 might fix this in the future. + +Python:: + + from functools import partial + + from bleak import BleakClient + + + def my_notification_callback_with_client_input( + client: BleakClient, sender: int, data: bytearray + ): + """Notification callback with client awareness""" + print( + f"Notification from device with address {client.address} and characteristic with handle {client.services.get_characteristic(sender)}. Data: {data}" + ) + + # [...] + + await client.start_notify( + char_specifier, partial(my_notification_callback_with_client_input, client) + ) + ------------------------- Capture Bluetooth Traffic ------------------------- diff --git a/examples/detection_callback.py b/examples/detection_callback.py index c3363769..a1e8326e 100644 --- a/examples/detection_callback.py +++ b/examples/detection_callback.py @@ -10,13 +10,11 @@ import asyncio import logging -import platform import sys from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from bleak.uuids import uuid16_dict, uuid128_dict logger = logging.getLogger(__name__) @@ -26,20 +24,6 @@ def simple_callback(device: BLEDevice, advertisement_data: AdvertisementData): async def main(service_uuids): - mac_ver = platform.mac_ver()[0].split(".") - if mac_ver[0] and int(mac_ver[0]) >= 12 and not service_uuids: - # In macOS 12 Monterey the service_uuids need to be specified. As a - # workaround for this example program, we scan for all known UUIDs to - # increse the chance of at least something showing up. However, in a - # "real" program, only the device-specific advertised UUID should be - # used. Devices that don't advertize at least one service UUID cannot - # currently be detected. - logger.warning( - "Scanning using all known service UUIDs to work around a macOS 12 bug. Some devices may not be detected. Please report this to Apple using the Feedback Assistant app and reference ." - ) - for item in uuid16_dict: - service_uuids.append("{0:04x}".format(item)) - service_uuids.extend(uuid128_dict.keys()) scanner = BleakScanner(service_uuids=service_uuids) scanner.register_detection_callback(simple_callback) diff --git a/requirements_dev.txt b/requirements_dev.txt index dca565dc..b9b62312 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -2,7 +2,7 @@ pip>=18.0 bump2version==1.0.1 wheel>=0.32.2 watchdog>=0.8.3 -black>=20.8b1 +black>=22.1.0 flake8>=3.5.0 tox>=3.1.3 coverage>=6.0b1 diff --git a/setup.cfg b/setup.cfg index 28ba2640..a575a716 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ replace = __version__ = "{new_version}" universal = 1 [flake8] -exclude = docs,.venv,*.pyi,.buildozer +exclude = docs,.venv,*.pyi,.buildozer,build,dist,.eggs ignore = E203,E501,W503 [aliases]