Skip to content

Commit

Permalink
bluez: implement pairing agent
Browse files Browse the repository at this point in the history
  • Loading branch information
dlech committed Nov 18, 2022
1 parent 589c975 commit 4e46255
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 24 deletions.
171 changes: 171 additions & 0 deletions bleak/backends/bluezdbus/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""
Agent
-----
This module contains types associated with the BlueZ D-Bus `agent api
<https://github.com/bluez/bluez/blob/master/doc/agent-api.txt>`.
"""

import asyncio
import contextlib
import logging
import os
from typing import Set, no_type_check

from dbus_fast import DBusError, Message
from dbus_fast.aio import MessageBus
from dbus_fast.service import ServiceInterface, method

from bleak.backends.device import BLEDevice

from ...agent import BaseBleakAgentCallbacks
from . import defs
from .manager import get_global_bluez_manager
from .utils import assert_reply

logger = logging.getLogger(__name__)


class Agent(ServiceInterface):
"""
Implementation of the org.bluez.Agent1 D-Bus interface.
"""

def __init__(self, callbacks: BaseBleakAgentCallbacks):
"""
Args:
"""
super().__init__(defs.AGENT_INTERFACE)
self._callbacks = callbacks
self._tasks: Set[asyncio.Task] = set()

async def _create_ble_device(self, 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}
)

@method()
def Release(self):
logger.debug("Release")

# REVISIT: mypy is broke, so we have to add redundant @no_type_check
# https://github.com/python/mypy/issues/6583

@method()
@no_type_check
async def RequestPinCode(self, device: "o") -> "s": # noqa: F821
logger.debug("RequestPinCode %s", device)
raise NotImplementedError

@method()
@no_type_check
async def DisplayPinCode(self, device: "o", pincode: "s"): # noqa: F821
logger.debug("DisplayPinCode %s %s", device, pincode)
raise NotImplementedError

@method()
@no_type_check
async def RequestPasskey(self, device: "o") -> "u": # noqa: F821
logger.debug("RequestPasskey %s", device)

ble_device = await self._create_ble_device(device)

task = asyncio.create_task(self._callbacks.request_pin(ble_device))
self._tasks.add(task)

try:
pin = await task
except asyncio.CancelledError:
raise DBusError("org.bluez.Error.Canceled", "task canceled")
finally:
self._tasks.remove(task)

if not pin:
raise DBusError("org.bluez.Error.Rejected", "user rejected")

return int(pin)

@method()
@no_type_check
async def DisplayPasskey(
self, device: "o", passkey: "u", entered: "q" # noqa: F821
):
passkey = f"{passkey:06}"
logger.debug("DisplayPasskey %s %s %d", device, passkey, entered)
raise NotImplementedError

@method()
@no_type_check
async def RequestConfirmation(self, device: "o", passkey: "u"): # noqa: F821
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
logger.debug("RequestAuthorization %s", device)
raise NotImplementedError

@method()
@no_type_check
async def AuthorizeService(self, device: "o", uuid: "s"): # noqa: F821
logger.debug("AuthorizeService %s", device, uuid)
raise NotImplementedError

@method()
@no_type_check
def Cancel(self): # noqa: F821
logger.debug("Cancel")
for t in self._tasks:
t.cancel()


@contextlib.asynccontextmanager
async def bluez_agent(bus: MessageBus, callbacks: BaseBleakAgentCallbacks):
agent = Agent(callbacks)

# REVISIT: implement passing capability if needed
# "DisplayOnly", "DisplayYesNo", "KeyboardOnly", "NoInputNoOutput", "KeyboardDisplay"
capability = ""

# this should be a unique path to allow multiple python interpreters
# running bleak and multiple agents at the same time
agent_path = f"/org/bleak/agent/{os.getpid()}/{id(agent)}"

bus.export(agent_path, agent)

try:
reply = await bus.call(
Message(
destination=defs.BLUEZ_SERVICE,
path="/org/bluez",
interface=defs.AGENT_MANAGER_INTERFACE,
member="RegisterAgent",
signature="os",
body=[agent_path, capability],
)
)

assert_reply(reply)

try:
yield
finally:
reply = await bus.call(
Message(
destination=defs.BLUEZ_SERVICE,
path="/org/bluez",
interface=defs.AGENT_MANAGER_INTERFACE,
member="UnregisterAgent",
signature="o",
body=[agent_path],
)
)

assert_reply(reply)

finally:
bus.unexport(agent_path, agent)
55 changes: 31 additions & 24 deletions bleak/backends/bluezdbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
BLE Client for BlueZ on Linux
"""
import asyncio
import contextlib
import logging
import os
import sys
Expand All @@ -22,12 +23,19 @@

from ... import BleakScanner
from ...agent import BaseBleakAgentCallbacks
from ...exc import BleakDBusError, BleakError, BleakDeviceNotFoundError
from ...exc import (
BleakDBusError,
BleakDeviceNotFoundError,
BleakError,
BleakPairingCancelledError,
BleakPairingFailedError,
)
from ..characteristic import BleakGATTCharacteristic
from ..client import BaseBleakClient, NotifyCallback
from ..device import BLEDevice
from ..service import BleakGATTServiceCollection
from . import defs
from .agent import bluez_agent
from .characteristic import BleakGATTCharacteristicBlueZDBus
from .manager import get_global_bluez_manager
from .scanner import BleakScannerBlueZDBus
Expand Down Expand Up @@ -343,9 +351,6 @@ async def pair(
Pair with the peripheral.
"""

if callbacks:
raise NotImplementedError

# See if it is already paired.
reply = await self._bus.call(
Message(
Expand Down Expand Up @@ -377,29 +382,31 @@ async def pair(

logger.debug("Pairing to BLE device @ %s", self.address)

reply = await self._bus.call(
Message(
destination=defs.BLUEZ_SERVICE,
path=self._device_path,
interface=defs.DEVICE_INTERFACE,
member="Pair",
async with contextlib.nullcontext() if callbacks is None else bluez_agent(
self._bus, callbacks
):
reply = await self._bus.call(
Message(
destination=defs.BLUEZ_SERVICE,
path=self._device_path,
interface=defs.DEVICE_INTERFACE,
member="Pair",
)
)
)
assert_reply(reply)

reply = await self._bus.call(
Message(
destination=defs.BLUEZ_SERVICE,
path=self._device_path,
interface=defs.PROPERTIES_INTERFACE,
member="Get",
signature="ss",
body=[defs.DEVICE_INTERFACE, "Paired"],
)
)
assert_reply(reply)
try:
assert_reply(reply)
except BleakDBusError as e:
if e.dbus_error == "org.bluez.Error.AuthenticationCanceled":
raise BleakPairingCancelledError from e

if e.dbus_error == "org.bluez.Error.AuthenticationFailed":
raise BleakPairingFailedError from e

return reply.body[0].value
raise

# for backwards compatibility
return True

async def unpair(self) -> bool:
"""Unpair with the peripheral.
Expand Down
2 changes: 2 additions & 0 deletions bleak/backends/bluezdbus/defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
ADAPTER_INTERFACE = "org.bluez.Adapter1"
ADVERTISEMENT_MONITOR_INTERFACE = "org.bluez.AdvertisementMonitor1"
ADVERTISEMENT_MONITOR_MANAGER_INTERFACE = "org.bluez.AdvertisementMonitorManager1"
AGENT_INTERFACE = "org.bluez.Agent1"
AGENT_MANAGER_INTERFACE = "org.bluez.AgentManager1"
DEVICE_INTERFACE = "org.bluez.Device1"
BATTERY_INTERFACE = "org.bluez.Battery1"

Expand Down
14 changes: 14 additions & 0 deletions bleak/backends/bluezdbus/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,20 @@ async def get_services(

return services

def get_device_props(self, device_path: str) -> Device1:
"""
Gets the current properties of a device.
Args:
device_path: The D-Bus object path of the device.
Returns:
The current properties.
"""
return cast(
Device1, self._properties[device_path][defs.DEVICE_INTERFACE].copy()
)

def get_device_name(self, device_path: str) -> str:
"""
Gets the value of the "Name" property for a device.
Expand Down

0 comments on commit 4e46255

Please sign in to comment.