Skip to content

Commit bee490f

Browse files
committed
WIP: winrt/client: implement pairing callbacks
This is a work in progress and breaks stdin pairing_agent.py causing ^C to not work. Also, the pair() method will hang forever if the pairing is rejected or an invalid value is returned from a pairing callback.
1 parent d1e1ee7 commit bee490f

File tree

2 files changed

+120
-55
lines changed

2 files changed

+120
-55
lines changed

bleak/backends/winrt/client.py

Lines changed: 85 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,21 @@
4343
DevicePairingKinds,
4444
DevicePairingResultStatus,
4545
DeviceUnpairingResultStatus,
46+
DevicePairingRequestedEventArgs,
47+
DeviceInformationCustomPairing,
4648
)
4749
from bleak_winrt.windows.foundation import EventRegistrationToken
4850
from bleak_winrt.windows.storage.streams import Buffer
4951

5052
from ... import BleakScanner
5153
from ...agent import BaseBleakAgentCallbacks
52-
from ...exc import PROTOCOL_ERROR_CODES, BleakDeviceNotFoundError, BleakError
54+
from ...exc import (
55+
PROTOCOL_ERROR_CODES,
56+
BleakDeviceNotFoundError,
57+
BleakError,
58+
BleakPairingCancelledError,
59+
BleakPairingFailedError,
60+
)
5361
from ..characteristic import BleakGATTCharacteristic
5462
from ..client import BaseBleakClient, NotifyCallback
5563
from ..device import BLEDevice
@@ -251,7 +259,7 @@ async def connect(self, **kwargs) -> bool:
251259

252260
def handle_services_changed():
253261
if not self._services_changed_events:
254-
logger.warn("%s: unhandled services changed event", self.address)
262+
logger.warning("%s: unhandled services changed event", self.address)
255263
else:
256264
for event in self._services_changed_events:
257265
event.set()
@@ -446,52 +454,91 @@ async def pair(
446454
"""
447455
Attempts to pair with the device.
448456
"""
449-
if callbacks:
450-
raise NotImplementedError
451-
452457
# New local device information object created since the object from the requester isn't updated
453-
device_information = await DeviceInformation.create_from_id_async(
454-
self._requester.device_information.id
458+
device_information: DeviceInformation = (
459+
await DeviceInformation.create_from_id_async(
460+
self._requester.device_information.id
461+
)
455462
)
456-
if (
457-
device_information.pairing.can_pair
458-
and not device_information.pairing.is_paired
459-
):
460463

461-
# Currently only supporting Just Works solutions...
462-
ceremony = DevicePairingKinds.CONFIRM_ONLY
463-
custom_pairing = device_information.pairing.custom
464+
if device_information.pairing.is_paired:
465+
logger.debug("already paired")
466+
return True
464467

465-
def handler(sender, args):
466-
args.accept()
468+
if not device_information.pairing.can_pair:
469+
raise BleakError("device does not support pairing")
467470

468-
pairing_requested_token = custom_pairing.add_pairing_requested(handler)
469-
try:
470-
if protection_level:
471-
pairing_result = await custom_pairing.pair_async(
472-
ceremony, protection_level
473-
)
474-
else:
475-
pairing_result = await custom_pairing.pair_async(ceremony)
471+
if callbacks:
476472

477-
except Exception as e:
478-
raise BleakError("Failure trying to pair with device!") from e
479-
finally:
480-
custom_pairing.remove_pairing_requested(pairing_requested_token)
473+
loop = asyncio.get_running_loop()
481474

482-
if pairing_result.status not in (
483-
DevicePairingResultStatus.PAIRED,
484-
DevicePairingResultStatus.ALREADY_PAIRED,
475+
def handle_pairing_requested(
476+
sender: DeviceInformationCustomPairing,
477+
args: DevicePairingRequestedEventArgs,
485478
):
486-
raise BleakError(f"Could not pair with device: {pairing_result.status}")
487-
else:
488-
logger.info(
489-
"Paired to device with protection level %d.",
490-
pairing_result.protection_level_used,
479+
print(args.pairing_kind, args.pin)
480+
logger.debug("kind: %r, pin: %s", args.pairing_kind, args.pin)
481+
482+
deferral = args.get_deferral()
483+
484+
async def handle_callback():
485+
print("prompt")
486+
try:
487+
ble_device = BLEDevice(
488+
args.device_information.id,
489+
args.device_information.name,
490+
args.device_information,
491+
)
492+
493+
if args.pairing_kind & DevicePairingKinds.CONFIRM_PIN_MATCH:
494+
if await callbacks.confirm_pin(ble_device):
495+
args.accept()
496+
elif args.pairing_kind & DevicePairingKinds.PROVIDE_PIN:
497+
pin = await callbacks.request_pin(ble_device)
498+
499+
if pin:
500+
args.accept(pin)
501+
elif args.pairing_kind & DevicePairingKinds.DISPLAY_PIN:
502+
await callbacks.display_pin(ble_device, args.pin)
503+
args.accept()
504+
elif args.pairing_kind & DevicePairingKinds.CONFIRM_ONLY:
505+
if await callbacks.confirm(ble_device):
506+
args.accept()
507+
finally:
508+
print("complete")
509+
deferral.complete()
510+
511+
loop.call_soon_threadsafe(loop.create_task, handle_callback())
512+
513+
token = device_information.pairing.custom.add_pairing_requested(
514+
handle_pairing_requested
515+
)
516+
try:
517+
result = await device_information.pairing.custom.pair_async(
518+
DevicePairingKinds.CONFIRM_ONLY
519+
| DevicePairingKinds.DISPLAY_PIN
520+
| DevicePairingKinds.PROVIDE_PIN
521+
| DevicePairingKinds.CONFIRM_PIN_MATCH
491522
)
492-
return True
523+
finally:
524+
device_information.pairing.custom.remove_pairing_requested(token)
525+
493526
else:
494-
return device_information.pairing.is_paired
527+
result = await device_information.pairing.pair_async()
528+
529+
if result.status == DevicePairingResultStatus.PAIRING_CANCELED:
530+
raise BleakPairingCancelledError
531+
532+
if result.status == DevicePairingResultStatus.FAILED:
533+
raise BleakPairingFailedError
534+
535+
if result.status not in [
536+
DevicePairingResultStatus.PAIRED,
537+
DevicePairingResultStatus.ALREADY_PAIRED,
538+
]:
539+
raise BleakError("pairing failed", result.status)
540+
541+
return True
495542

496543
async def unpair(self) -> bool:
497544
"""Attempts to unpair from the device.

examples/pairing_agent.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,44 @@
11
import argparse
22
import asyncio
3+
from concurrent.futures import Future
34
import sys
5+
import threading
46

57
from bleak import BleakScanner, BleakClient, BaseBleakAgentCallbacks
68
from bleak.backends.device import BLEDevice
7-
from bleak.exc import BleakPairingCancelledError, BleakPairingFailedError
9+
from bleak.exc import (
10+
BleakDeviceNotFoundError,
11+
BleakPairingCancelledError,
12+
BleakPairingFailedError,
13+
)
814

915

10-
class AgentCallbacks(BaseBleakAgentCallbacks):
11-
def __init__(self) -> None:
12-
super().__init__()
13-
self._reader = asyncio.StreamReader()
16+
async def run_as_daemon(func, *args):
17+
future = Future()
18+
future.set_running_or_notify_cancel()
19+
20+
def daemon():
21+
try:
22+
result = func(*args)
23+
print("result", result)
24+
except BaseException as e:
25+
future.set_exception(e)
26+
else:
27+
future.set_result(result)
1428

15-
async def __aenter__(self):
16-
loop = asyncio.get_running_loop()
17-
protocol = asyncio.StreamReaderProtocol(self._reader)
18-
self._input_transport, _ = await loop.connect_read_pipe(
19-
lambda: protocol, sys.stdin
20-
)
21-
return self
29+
threading.Thread(target=daemon, daemon=True).start()
30+
return await asyncio.wrap_future(future)
2231

23-
async def __aexit__(self, *args):
24-
self._input_transport.close()
2532

33+
class AgentCallbacks(BaseBleakAgentCallbacks):
2634
async def _input(self, msg: str) -> str:
2735
"""
2836
Async version of the builtin input function.
2937
"""
3038
print(msg, end=" ", flush=True)
31-
return (await self._reader.readline()).decode().strip()
39+
line: str = await run_as_daemon(sys.stdin.readline)
40+
print("line", line)
41+
return line.strip()
3242

3343
async def confirm(self, device: BLEDevice) -> bool:
3444
print(f"{device.name} wants to pair.")
@@ -52,13 +62,18 @@ async def request_pin(self, device: BLEDevice) -> str:
5262
print(f"{device.name} wants to pair.")
5363
response = await self._input("enter pin:")
5464

65+
print("response", response)
66+
5567
return response
5668

5769

5870
async def main(addr: str, unpair: bool) -> None:
5971
if unpair:
6072
print("unpairing...")
61-
await BleakClient(addr).unpair()
73+
try:
74+
await BleakClient(addr).unpair()
75+
except BleakDeviceNotFoundError:
76+
pass
6277

6378
print("scanning...")
6479

@@ -68,9 +83,12 @@ async def main(addr: str, unpair: bool) -> None:
6883
print("device was not found")
6984
return
7085

71-
async with BleakClient(device) as client, AgentCallbacks() as callbacks:
86+
print("pairing...")
87+
callbacks = AgentCallbacks()
88+
async with BleakClient(device) as client:
7289
try:
7390
await client.pair(callbacks)
91+
print("success")
7492
except BleakPairingCancelledError:
7593
print("paring was canceled")
7694
except BleakPairingFailedError:

0 commit comments

Comments
 (0)