Skip to content

Commit

Permalink
WIP: winrt/client: implement pairing callbacks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dlech committed Oct 31, 2022
1 parent d1e1ee7 commit bee490f
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 55 deletions.
123 changes: 85 additions & 38 deletions bleak/backends/winrt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,21 @@
DevicePairingKinds,
DevicePairingResultStatus,
DeviceUnpairingResultStatus,
DevicePairingRequestedEventArgs,
DeviceInformationCustomPairing,
)
from bleak_winrt.windows.foundation import EventRegistrationToken
from bleak_winrt.windows.storage.streams import Buffer

from ... import BleakScanner
from ...agent import BaseBleakAgentCallbacks
from ...exc import PROTOCOL_ERROR_CODES, BleakDeviceNotFoundError, BleakError
from ...exc import (
PROTOCOL_ERROR_CODES,
BleakDeviceNotFoundError,
BleakError,
BleakPairingCancelledError,
BleakPairingFailedError,
)
from ..characteristic import BleakGATTCharacteristic
from ..client import BaseBleakClient, NotifyCallback
from ..device import BLEDevice
Expand Down Expand Up @@ -251,7 +259,7 @@ async def connect(self, **kwargs) -> bool:

def handle_services_changed():
if not self._services_changed_events:
logger.warn("%s: unhandled services changed event", self.address)
logger.warning("%s: unhandled services changed event", self.address)
else:
for event in self._services_changed_events:
event.set()
Expand Down Expand Up @@ -446,52 +454,91 @@ async def pair(
"""
Attempts to pair with the device.
"""
if callbacks:
raise NotImplementedError

# 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
device_information: DeviceInformation = (
await DeviceInformation.create_from_id_async(
self._requester.device_information.id
)
)
if (
device_information.pairing.can_pair
and not device_information.pairing.is_paired
):

# Currently only supporting Just Works solutions...
ceremony = DevicePairingKinds.CONFIRM_ONLY
custom_pairing = device_information.pairing.custom
if device_information.pairing.is_paired:
logger.debug("already paired")
return True

def handler(sender, args):
args.accept()
if not device_information.pairing.can_pair:
raise BleakError("device does not support pairing")

pairing_requested_token = custom_pairing.add_pairing_requested(handler)
try:
if protection_level:
pairing_result = await custom_pairing.pair_async(
ceremony, protection_level
)
else:
pairing_result = await custom_pairing.pair_async(ceremony)
if callbacks:

except Exception as e:
raise BleakError("Failure trying to pair with device!") from e
finally:
custom_pairing.remove_pairing_requested(pairing_requested_token)
loop = asyncio.get_running_loop()

if pairing_result.status not in (
DevicePairingResultStatus.PAIRED,
DevicePairingResultStatus.ALREADY_PAIRED,
def handle_pairing_requested(
sender: DeviceInformationCustomPairing,
args: DevicePairingRequestedEventArgs,
):
raise BleakError(f"Could not pair with device: {pairing_result.status}")
else:
logger.info(
"Paired to device with protection level %d.",
pairing_result.protection_level_used,
print(args.pairing_kind, args.pin)
logger.debug("kind: %r, pin: %s", args.pairing_kind, args.pin)

deferral = args.get_deferral()

async def handle_callback():
print("prompt")
try:
ble_device = BLEDevice(
args.device_information.id,
args.device_information.name,
args.device_information,
)

if args.pairing_kind & DevicePairingKinds.CONFIRM_PIN_MATCH:
if await callbacks.confirm_pin(ble_device):
args.accept()
elif args.pairing_kind & DevicePairingKinds.PROVIDE_PIN:
pin = await callbacks.request_pin(ble_device)

if pin:
args.accept(pin)
elif args.pairing_kind & DevicePairingKinds.DISPLAY_PIN:
await callbacks.display_pin(ble_device, args.pin)
args.accept()
elif args.pairing_kind & DevicePairingKinds.CONFIRM_ONLY:
if await callbacks.confirm(ble_device):
args.accept()
finally:
print("complete")
deferral.complete()

loop.call_soon_threadsafe(loop.create_task, handle_callback())

token = device_information.pairing.custom.add_pairing_requested(
handle_pairing_requested
)
try:
result = await device_information.pairing.custom.pair_async(
DevicePairingKinds.CONFIRM_ONLY
| DevicePairingKinds.DISPLAY_PIN
| DevicePairingKinds.PROVIDE_PIN
| DevicePairingKinds.CONFIRM_PIN_MATCH
)
return True
finally:
device_information.pairing.custom.remove_pairing_requested(token)

else:
return device_information.pairing.is_paired
result = await device_information.pairing.pair_async()

if result.status == DevicePairingResultStatus.PAIRING_CANCELED:
raise BleakPairingCancelledError

if result.status == DevicePairingResultStatus.FAILED:
raise BleakPairingFailedError

if result.status not in [
DevicePairingResultStatus.PAIRED,
DevicePairingResultStatus.ALREADY_PAIRED,
]:
raise BleakError("pairing failed", result.status)

return True

async def unpair(self) -> bool:
"""Attempts to unpair from the device.
Expand Down
52 changes: 35 additions & 17 deletions examples/pairing_agent.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,44 @@
import argparse
import asyncio
from concurrent.futures import Future
import sys
import threading

from bleak import BleakScanner, BleakClient, BaseBleakAgentCallbacks
from bleak.backends.device import BLEDevice
from bleak.exc import BleakPairingCancelledError, BleakPairingFailedError
from bleak.exc import (
BleakDeviceNotFoundError,
BleakPairingCancelledError,
BleakPairingFailedError,
)


class AgentCallbacks(BaseBleakAgentCallbacks):
def __init__(self) -> None:
super().__init__()
self._reader = asyncio.StreamReader()
async def run_as_daemon(func, *args):
future = Future()
future.set_running_or_notify_cancel()

def daemon():
try:
result = func(*args)
print("result", result)
except BaseException as e:
future.set_exception(e)
else:
future.set_result(result)

async def __aenter__(self):
loop = asyncio.get_running_loop()
protocol = asyncio.StreamReaderProtocol(self._reader)
self._input_transport, _ = await loop.connect_read_pipe(
lambda: protocol, sys.stdin
)
return self
threading.Thread(target=daemon, daemon=True).start()
return await asyncio.wrap_future(future)

async def __aexit__(self, *args):
self._input_transport.close()

class AgentCallbacks(BaseBleakAgentCallbacks):
async def _input(self, msg: str) -> str:
"""
Async version of the builtin input function.
"""
print(msg, end=" ", flush=True)
return (await self._reader.readline()).decode().strip()
line: str = await run_as_daemon(sys.stdin.readline)
print("line", line)
return line.strip()

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

print("response", response)

return response


async def main(addr: str, unpair: bool) -> None:
if unpair:
print("unpairing...")
await BleakClient(addr).unpair()
try:
await BleakClient(addr).unpair()
except BleakDeviceNotFoundError:
pass

print("scanning...")

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

async with BleakClient(device) as client, AgentCallbacks() as callbacks:
print("pairing...")
callbacks = AgentCallbacks()
async with BleakClient(device) as client:
try:
await client.pair(callbacks)
print("success")
except BleakPairingCancelledError:
print("paring was canceled")
except BleakPairingFailedError:
Expand Down

0 comments on commit bee490f

Please sign in to comment.