Skip to content

Commit

Permalink
Add hack for getting MTU on BlueZ backend.
Browse files Browse the repository at this point in the history
BlueZ doesn't have a good way to get the negotiated MTU, but we can get
it by abusing existing D-Bus methods.

Since the workaround is async and could cause unwanted side effects for
some devices, it opt-in as seen in the example code.
  • Loading branch information
dlech committed May 21, 2021
1 parent 62f45b0 commit 5b090c0
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 6 deletions.
60 changes: 54 additions & 6 deletions bleak/backends/bluezdbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
# used to ensure device gets disconnected if event loop crashes
self._disconnect_monitor_event: Optional[asyncio.Event] = None

# used to override mtu_size property
self._mtu_size: Optional[int] = None

# get BlueZ version
p = subprocess.Popen(["bluetoothctl", "--version"], stdout=subprocess.PIPE)
out, _ = p.communicate()
Expand Down Expand Up @@ -137,8 +140,7 @@ async def connect(self, **kwargs) -> bool:

# Create system bus
self._bus = await MessageBus(
bus_type=BusType.SYSTEM,
negotiate_unix_fd=self._write_without_response_workaround_needed,
bus_type=BusType.SYSTEM, negotiate_unix_fd=True
).connect()

try:
Expand Down Expand Up @@ -570,13 +572,59 @@ def is_connected(self) -> bool:
False if self._bus is None else self._properties.get("Connected", False)
)

async def _acquire_mtu(self) -> None:
"""Acquires the MTU for this device by calling the "AcquireWrite" or
"AcquireNotify" method of the first characteristic that has such a method.
This method only needs to be called once, after connecting to the device
but before accessing the ``mtu_size`` property.
If a device uses encryption on characteristics, it will need to be bonded
first before calling this method.
"""
# This will try to get the "best" characteristic for getting the MTU.
# We would rather not start notifications if we don't have to.
try:
method = "AcquireWrite"
char = next(
c
for c in self.services.characteristics.values()
if "write-without-response" in c.properties
)
except StopIteration:
method = "AcquireNotify"
char = next(
c
for c in self.services.characteristics.values()
if "notify" in c.properties
)

reply = await self._bus.call(
Message(
destination=defs.BLUEZ_SERVICE,
path=char.path,
interface=defs.GATT_CHARACTERISTIC_INTERFACE,
member=method,
signature="a{sv}",
body=[{}],
)
)
assert_reply(reply)

# we aren't actually using the write or notify, we just want the MTU
os.close(reply.unix_fds[0])
self._mtu_size = reply.body[1]

@property
def mtu_size(self) -> int:
"""Get ATT MTU size for active connection"""
warnings.warn(
"MTU size not supported with BlueZ; this function returns the default value of 23. Note that the actual MTU size might be larger"
)
return 23
if self._mtu_size is None:
warnings.warn(
"Using default MTU value. Call _assign_mtu() or set _mtu_size first to avoid this warning."
)
return 23

return self._mtu_size

# GATT services methods

Expand Down
47 changes: 47 additions & 0 deletions examples/mtu_size.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Example showing how to use BleakClient.mtu_size
"""

import asyncio

from bleak import BleakScanner, BleakClient
from bleak.backends.scanner import BLEDevice, AdvertisementData

# replace with real characteristic UUID
CHAR_UUID = "00000000-0000-0000-0000-000000000000"


async def start():
queue = asyncio.Queue()

def callback(device: BLEDevice, adv: AdvertisementData) -> None:
# can use advertising data to filter here
queue.put_nowait(device)

async with BleakScanner(detection_callback=callback):
# get the first matching device
device = await queue.get()

async with BleakClient(device) as client:
# BlueZ doesn't have a proper way to get the MTU, so we have this hack.
# If this doesn't work for you, you can set the client._mtu_size attribute
# to override the value instead.
if client.__class__.__name__ == "BleakClientBlueZDBus":
await client._acquire_mtu()

print("MTU:", client.mtu_size)

# Write without response is limited to MTU - 3 bytes

data = bytes(1000) # replace with real data
chunk_size = client.mtu_size - 3
for chunk in (
data[i : i + chunk_size] for i in range(0, len(data), chunk_size)
):
await client.write_gatt_char(CHAR_UUID, chunk)


# It is important to use asyncio.run() to get proper cleanup on KeyboardInterrupt.
# This was introduced in Python 3.7. If you need it in Python 3.6, you can copy
# it from https://github.com/python/cpython/blob/3.7/Lib/asyncio/runners.py
asyncio.run(start())

0 comments on commit 5b090c0

Please sign in to comment.