From ae406f5a74a8055a9ec0be9ac66690e80f30baa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Poto=C4=8Dnik?= Date: Tue, 15 Nov 2022 17:07:56 +0100 Subject: [PATCH 1/4] bluezdbus/agent: Fix missing rssi parameter when instantiating BLEDevice Add #noqa to suppress "N802 PEP8 Function name should be lowercase" warnings. `@method(name=)` parameter could be used to have lowercase methods still connected to correct pascal-case D-Bus methods, however using exact names in Python makes it clear that these methods are bonded to the D-Bus interface. --- bleak/backends/bluezdbus/agent.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/bleak/backends/bluezdbus/agent.py b/bleak/backends/bluezdbus/agent.py index 66022f2e..e4440377 100644 --- a/bleak/backends/bluezdbus/agent.py +++ b/bleak/backends/bluezdbus/agent.py @@ -39,15 +39,19 @@ def __init__(self, callbacks: BaseBleakAgentCallbacks): self._callbacks = callbacks self._tasks: Set[asyncio.Task] = set() - async def _create_ble_device(self, device_path: str) -> BLEDevice: + @staticmethod + async def _create_ble_device(device_path: str) -> BLEDevice: manager = await get_global_bluez_manager() props = manager.get_device_props(device_path) return BLEDevice( - props["Address"], props["Alias"], {"path": device_path, "props": props} + props["Address"], + props["Alias"], + {"path": device_path, "props": props}, + props.get("RSSI", -127), ) @method() - def Release(self): + def Release(self): # noqa: N802 logger.debug("Release") # REVISIT: mypy is broke, so we have to add redundant @no_type_check @@ -55,19 +59,19 @@ def Release(self): @method() @no_type_check - async def RequestPinCode(self, device: "o") -> "s": # noqa: F821 + async def RequestPinCode(self, device: "o") -> "s": # noqa: F821 N802 logger.debug("RequestPinCode %s", device) raise NotImplementedError @method() @no_type_check - async def DisplayPinCode(self, device: "o", pincode: "s"): # noqa: F821 + async def DisplayPinCode(self, device: "o", pincode: "s"): # noqa: F821 N802 logger.debug("DisplayPinCode %s %s", device, pincode) raise NotImplementedError @method() @no_type_check - async def RequestPasskey(self, device: "o") -> "u": # noqa: F821 + async def RequestPasskey(self, device: "o") -> "u": # noqa: F821 N802 logger.debug("RequestPasskey %s", device) ble_device = await self._create_ble_device(device) @@ -89,7 +93,7 @@ async def RequestPasskey(self, device: "o") -> "u": # noqa: F821 @method() @no_type_check - async def DisplayPasskey( + async def DisplayPasskey( # noqa: N802 self, device: "o", passkey: "u", entered: "q" # noqa: F821 ): passkey = f"{passkey:06}" @@ -98,26 +102,26 @@ async def DisplayPasskey( @method() @no_type_check - async def RequestConfirmation(self, device: "o", passkey: "u"): # noqa: F821 + async def RequestConfirmation(self, device: "o", passkey: "u"): # noqa: F821 N802 passkey = f"{passkey:06}" logger.debug("RequestConfirmation %s %s", device, passkey) raise NotImplementedError @method() @no_type_check - async def RequestAuthorization(self, device: "o"): # noqa: F821 + async def RequestAuthorization(self, device: "o"): # noqa: F821 N802 logger.debug("RequestAuthorization %s", device) raise NotImplementedError @method() @no_type_check - async def AuthorizeService(self, device: "o", uuid: "s"): # noqa: F821 + async def AuthorizeService(self, device: "o", uuid: "s"): # noqa: F821 N802 logger.debug("AuthorizeService %s", device, uuid) raise NotImplementedError @method() @no_type_check - def Cancel(self): # noqa: F821 + def Cancel(self): # noqa: F821 N802 logger.debug("Cancel") for t in self._tasks: t.cancel() @@ -129,6 +133,7 @@ async def bluez_agent(bus: MessageBus, callbacks: BaseBleakAgentCallbacks): # REVISIT: implement passing capability if needed # "DisplayOnly", "DisplayYesNo", "KeyboardOnly", "NoInputNoOutput", "KeyboardDisplay" + # Note: If an empty string is used, BlueZ will fall back to "KeyboardDisplay". capability = "" # this should be a unique path to allow multiple python interpreters From 7a732b74613d66f4a617bacab29bf73858a72777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Poto=C4=8Dnik?= Date: Mon, 21 Nov 2022 15:16:10 +0100 Subject: [PATCH 2/4] examples/pairing_agent: Fix crash if --unpair and device is not paired --- examples/pairing_agent.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/examples/pairing_agent.py b/examples/pairing_agent.py index b16d269e..19a0a676 100644 --- a/examples/pairing_agent.py +++ b/examples/pairing_agent.py @@ -4,7 +4,11 @@ from bleak import BleakScanner, BleakClient, BaseBleakAgentCallbacks from bleak.backends.device import BLEDevice -from bleak.exc import BleakPairingCancelledError, BleakPairingFailedError +from bleak.exc import ( + BleakPairingCancelledError, + BleakPairingFailedError, + BleakDeviceNotFoundError, +) class AgentCallbacks(BaseBleakAgentCallbacks): @@ -58,7 +62,11 @@ async def request_pin(self, device: BLEDevice) -> str: async def main(addr: str, unpair: bool) -> None: if unpair: print("unpairing...") - await BleakClient(addr).unpair() + try: + await BleakClient(addr).unpair() + print("unpaired") + except BleakDeviceNotFoundError: + print("device was not paired") print("scanning...") @@ -68,9 +76,12 @@ async def main(addr: str, unpair: bool) -> None: print("device was not found") return + print("pairing...") + async with BleakClient(device) as client, AgentCallbacks() as callbacks: try: await client.pair(callbacks) + print("pairing successful") except BleakPairingCancelledError: print("paring was canceled") except BleakPairingFailedError: From 0c0c16f73ec604baf1f8ad66683530eb9455c1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Poto=C4=8Dnik?= Date: Tue, 15 Nov 2022 17:07:56 +0100 Subject: [PATCH 3/4] BleakClient: Add pairing_callbacks parameter to constructor --- bleak/__init__.py | 16 +++++++++++++--- bleak/backends/bluezdbus/client.py | 7 +++++++ bleak/backends/client.py | 4 ++++ bleak/backends/corebluetooth/client.py | 8 ++++++++ bleak/backends/p4android/client.py | 5 +++++ bleak/backends/winrt/client.py | 7 +++++++ 6 files changed, 44 insertions(+), 3 deletions(-) diff --git a/bleak/__init__.py b/bleak/__init__.py index f37f1058..12e9d258 100644 --- a/bleak/__init__.py +++ b/bleak/__init__.py @@ -349,6 +349,13 @@ class BleakClient: 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. + pairing_callbacks: + Optional callbacks used in the pairing process (e.g. displaying, + confirming, requesting pin). If provided here instead of to the + :meth:`pair` method as ``callbacks`` parameter, device will be + implicitly paired during connection establishment. This is useful + for devices sending Slave Security Request immediately after + connection, requiring pairing before GATT service discovery. timeout: Timeout in seconds passed to the implicit ``discover`` call when ``address_or_ble_device`` is not a :class:`BLEDevice`. Defaults to 10.0. @@ -385,6 +392,7 @@ def __init__( self, address_or_ble_device: Union[BLEDevice, str], disconnected_callback: Optional[Callable[[BleakClient], None]] = None, + pairing_callbacks: Optional[BaseBleakAgentCallbacks] = None, *, timeout: float = 10.0, winrt: WinRTClientArgs = {}, @@ -398,6 +406,7 @@ def __init__( self._backend = PlatformBleakClient( address_or_ble_device, disconnected_callback=disconnected_callback, + pairing_callbacks=pairing_callbacks, timeout=timeout, winrt=winrt, **kwargs, @@ -507,9 +516,10 @@ async def pair( Args: callbacks: - Optional callbacks for confirming or requesting pin. This is - only supported on Linux and Windows. If omitted, the OS will - handle the pairing request. + Optional callbacks used in the pairing process (e.g. displaying, + confirming, requesting pin). + This is only supported on Linux and Windows. + If omitted, the OS will handle the pairing request. Returns: Always returns ``True`` for backwards compatibility. diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index f20f1174..8144b578 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -96,6 +96,13 @@ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): # used to override mtu_size property self._mtu_size: Optional[int] = None + if kwargs.get("pairing_callbacks"): + warnings.warn( + "Pairing on connect not yet implemented for BlueZ", + RuntimeWarning, + stacklevel=2, + ) + def close(self): self._bus.disconnect() diff --git a/bleak/backends/client.py b/bleak/backends/client.py index d06b887d..59b2b3bb 100644 --- a/bleak/backends/client.py +++ b/bleak/backends/client.py @@ -35,6 +35,10 @@ class BaseBleakClient(abc.ABC): 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. + pairing_callbacks (BaseBleakAgentCallbacks): + Optional callbacks otherwise provided as ``callbacks`` parameter to the + :meth:`pair` method. If provided here, device will be implicitly paired + during connection establishment. """ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py index 30c9f207..acf88a80 100644 --- a/bleak/backends/corebluetooth/client.py +++ b/bleak/backends/corebluetooth/client.py @@ -6,6 +6,7 @@ import asyncio import logging import uuid +import warnings from typing import Optional, Union from CoreBluetooth import ( @@ -52,6 +53,13 @@ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): self._delegate: Optional[PeripheralDelegate] = None self._central_manager_delegate: Optional[CentralManagerDelegate] = None + if kwargs.get("pairing_callbacks"): + warnings.warn( + "Pairing is not available in Core Bluetooth.", + RuntimeWarning, + stacklevel=2, + ) + if isinstance(address_or_ble_device, BLEDevice): ( self._peripheral, diff --git a/bleak/backends/p4android/client.py b/bleak/backends/p4android/client.py index a4186f45..8ee3ba7c 100644 --- a/bleak/backends/p4android/client.py +++ b/bleak/backends/p4android/client.py @@ -45,6 +45,11 @@ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): self.__gatt = None self.__mtu = 23 + if kwargs.get("pairing_callbacks"): + warnings.warn( + "pairing_callbacks are ignored on Android", RuntimeWarning, stacklevel=2 + ) + def __del__(self): if self.__gatt is not None: self.__gatt.close() diff --git a/bleak/backends/winrt/client.py b/bleak/backends/winrt/client.py index a323695e..45e9f654 100644 --- a/bleak/backends/winrt/client.py +++ b/bleak/backends/winrt/client.py @@ -204,6 +204,13 @@ def __init__( self._session_status_changed_token: Optional[EventRegistrationToken] = None self._max_pdu_size_changed_token: Optional[EventRegistrationToken] = None + if kwargs.get("pairing_callbacks"): + warnings.warn( + "pairing_callbacks not yet implemented for Windows", + RuntimeWarning, + stacklevel=2, + ) + def __str__(self): return f"{type(self).__name__} ({self.address})" From dd50aa66730f80e95ca43acd69b2e7a9f2e67043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bojan=20Poto=C4=8Dnik?= Date: Fri, 25 Nov 2022 10:37:32 +0100 Subject: [PATCH 4/4] bluezdbus/client: Support pairing during connection establishment This is achieved by passing ``pairing_callbacks`` to the ``BleakClient`` constructor instead of manually calling ``pair()`` method. --- bleak/backends/bluezdbus/client.py | 15 ++++++++----- examples/pairing_agent.py | 35 ++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py index 8144b578..e93992e6 100644 --- a/bleak/backends/bluezdbus/client.py +++ b/bleak/backends/bluezdbus/client.py @@ -96,12 +96,9 @@ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): # used to override mtu_size property self._mtu_size: Optional[int] = None - if kwargs.get("pairing_callbacks"): - warnings.warn( - "Pairing on connect not yet implemented for BlueZ", - RuntimeWarning, - stacklevel=2, - ) + self._pairing_callbacks: Optional[BaseBleakAgentCallbacks] = kwargs.get( + "pairing_callbacks" + ) def close(self): self._bus.disconnect() @@ -193,6 +190,11 @@ def on_value_changed(char_path: str, value: bytes) -> None: # # For additional details see https://github.com/bluez/bluez/issues/89 # + if self._pairing_callbacks: + # org.bluez.Device1.Pair() will connect to the remote device, initiate + # pairing and then retrieve all SDP records (or GATT primary services). + await self.pair(self._pairing_callbacks) + if not manager.is_connected(self._device_path): logger.debug("Connecting to BlueZ path %s", self._device_path) async with async_timeout(timeout): @@ -400,6 +402,7 @@ async def pair( member="Pair", ) ) + # TODO: Call "CancelPairing" if this task is cancelled try: assert_reply(reply) diff --git a/examples/pairing_agent.py b/examples/pairing_agent.py index 19a0a676..12bd0dbd 100644 --- a/examples/pairing_agent.py +++ b/examples/pairing_agent.py @@ -59,7 +59,7 @@ async def request_pin(self, device: BLEDevice) -> str: return response -async def main(addr: str, unpair: bool) -> None: +async def main(addr: str, unpair: bool, auto: bool) -> None: if unpair: print("unpairing...") try: @@ -76,16 +76,26 @@ async def main(addr: str, unpair: bool) -> None: print("device was not found") return - print("pairing...") + if auto: + print("connecting and pairing...") - async with BleakClient(device) as client, AgentCallbacks() as callbacks: - try: - await client.pair(callbacks) - print("pairing successful") - except BleakPairingCancelledError: - print("paring was canceled") - except BleakPairingFailedError: - print("pairing failed (bad pin?)") + async with AgentCallbacks() as callbacks, BleakClient( + device, pairing_callbacks=callbacks + ) as client: + print(f"connection and pairing to {client.address} successful") + + else: + print("connecting...") + + async with BleakClient(device) as client, AgentCallbacks() as callbacks: + try: + print("pairing...") + await client.pair(callbacks) + print("pairing successful") + except BleakPairingCancelledError: + print("paring was canceled") + except BleakPairingFailedError: + print("pairing failed (bad pin?)") if __name__ == "__main__": @@ -94,6 +104,9 @@ async def main(addr: str, unpair: bool) -> None: parser.add_argument( "--unpair", action="store_true", help="unpair first before pairing" ) + parser.add_argument( + "--auto", action="store_true", help="automatically pair during connect" + ) args = parser.parse_args() - asyncio.run(main(args.address, args.unpair)) + asyncio.run(main(args.address, args.unpair, args.auto))