Skip to content

backends/bluezdbus/client: get mtu_size from the MTU property - #2058

Open
nebkat wants to merge 2 commits into
hbldh:developfrom
nebkat:bluez-mtu-from-characteristic
Open

nebkat wants to merge 2 commits into
hbldh:developfrom
nebkat:bluez-mtu-from-characteristic

Conversation

@nebkat

@nebkat nebkat commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

On the BlueZ backend, BleakClient.mtu_size returns the default of 23 with a warning unless the private _acquire_mtu() is called first, which calls AcquireWrite/AcquireNotify on a characteristic only to read the MTU from the reply.

BlueZ has no MTU property on the device, but 5.62 added an "MTU" property to org.bluez.GattCharacteristic1, which max_write_without_response_size already reads. It is the ATT MTU of the connection, so mtu_size now reads it. It is read from BlueZ with Properties.Get once services are resolved, rather than from the properties cache: BlueZ emits no PropertiesChanged for "MTU" on a reconnect to a device whose GATT objects it kept from an earlier connection, and omits the property while the device is disconnected, so a process that connects within that window would otherwise read the default for the whole connection (in mtu_size and in max_write_without_response_size, which this also fixes). The manager keeps the value per device until it disconnects. With BlueZ < 5.62, or without services, mtu_size logs a warning naming the minimum BlueZ version and returns 23.

The _mtu_size override is removed. _acquire_mtu() is kept but deprecated: it does nothing except issue a DeprecationWarning. Code that supports older versions of Bleak can call it only if hasattr(client._backend, "_mtu_size"), as the Linux backend docs now show. The mtu_size.py example is removed, and the changelog is updated.

The BlueZ side is fixed in https://lore.kernel.org/all/20261114-gatt-client-mtu-reconnect-v1-1-a86edc1eb208@gmail.com/, which makes BlueZ emit "MTU" on reconnection; the live read here is still needed for BlueZ releases without it.

Testing: on BlueZ 5.82 (Raspberry Pi OS), a second process connecting within ~30 s of the first one's disconnect reads max_write_without_response_size 20 with bleak 3.1.0, though the link MTU is 517; with this branch (99e42fe) on the same stock BlueZ it reads mtu_size 517 and max_write_without_response_size 514. The test_mtu_size integration test no longer expects a warning on BlueZ. poe lint, poe typecheck and the unit tests pass.

@codecov

codecov Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.06897% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.59%. Comparing base (a213f8c) to head (99e42fe).

Files with missing lines Patch % Lines
bleak/backends/bluezdbus/manager.py 68.18% 6 Missing and 1 partial ⚠️
bleak/backends/bluezdbus/client.py 42.85% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2058      +/-   ##
===========================================
- Coverage    65.74%   65.59%   -0.15%     
===========================================
  Files           53       53              
  Lines         5047     5060      +13     
  Branches       611      615       +4     
===========================================
+ Hits          3318     3319       +1     
- Misses        1507     1516       +9     
- Partials       222      225       +3     
Flag Coverage Δ
android-integration-py3.13-api31 26.95% <0.00%> (-0.07%) ⬇️
android-integration-py3.13-api33 26.87% <0.00%> (-0.07%) ⬇️
bluez-integration-py310 34.68% <62.06%> (+0.22%) ⬆️
bluez-integration-py311 34.62% <62.06%> (+0.22%) ⬆️
bluez-integration-py312 34.62% <62.06%> (+0.22%) ⬆️
bluez-integration-py313 34.62% <62.06%> (+0.22%) ⬆️
bluez-integration-py314 33.31% <62.06%> (+0.23%) ⬆️
macos-latest-py310 16.40% <0.00%> (-0.05%) ⬇️
macos-latest-py311 16.40% <0.00%> (-0.05%) ⬇️
macos-latest-py312 16.40% <0.00%> (-0.05%) ⬇️
macos-latest-py313 16.40% <0.00%> (-0.05%) ⬇️
macos-latest-py314 16.17% <0.00%> (-0.05%) ⬇️
ubuntu-latest-py310 27.45% <34.48%> (+0.12%) ⬆️
ubuntu-latest-py311 27.45% <34.48%> (+0.10%) ⬆️
ubuntu-latest-py312 27.45% <34.48%> (+0.12%) ⬆️
ubuntu-latest-py313 27.45% <34.48%> (+0.12%) ⬆️
ubuntu-latest-py314 25.99% <34.48%> (+0.13%) ⬆️
windows-latest-py310 15.86% <0.00%> (-0.05%) ⬇️
windows-latest-py311 15.86% <0.00%> (-0.05%) ⬇️
windows-latest-py312 15.86% <0.00%> (-0.05%) ⬇️
windows-latest-py313 15.86% <0.00%> (-0.05%) ⬇️
windows-latest-py314 15.59% <0.00%> (-0.05%) ⬇️
winvhci-integration-py310 23.77% <0.00%> (-0.07%) ⬇️
winvhci-integration-py311 23.77% <0.00%> (-0.07%) ⬇️
winvhci-integration-py312 23.77% <0.00%> (-0.11%) ⬇️
winvhci-integration-py313 23.77% <0.00%> (-0.07%) ⬇️
winvhci-integration-py314 ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@dlech

dlech commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator

Interestingly enough, this is what WebBluetooth has decided to do on Linux as well. whatwg/bluetooth#672

Comment thread bleak/backends/bluezdbus/client.py Resolved
@nebkat
nebkat force-pushed the bluez-mtu-from-characteristic branch from a635fd4 to f9fb1ed Compare November 11, 2026 15:06
"""
return False if self._bus is None else self._is_connected

async def _acquire_mtu(self) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some research and it looks like there are a decent number of folks calling this. So we should leave the _acquire_mtu() method but just have it raise DeprecationWarning.

And there is a clean workaround to avoid the warning while also supporting older versions of Bleak if they need to.

if hasattr(backend, "_mtu_size"):
    await backend._acquire_mtu()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept _acquire_mtu(); it now only issues a DeprecationWarning. _mtu_size stays removed so the hasattr check works, and I added that snippet to the Linux backend docs.

Comment thread examples/mtu_size.py Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would delete the entire file. The whole point was to show how to do the workaround.

Or update it to show how to support multiple versions of Bleak with the suggested:

if hasattr(backend, "_mtu_size"):
    await backend._acquire_mtu()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted the example; the hasattr snippet is in the Linux backend docs instead.

Comment thread bleak/backends/bluezdbus/client.py Outdated
@@ -107,7 +106,6 @@ def __init__(
self._notification_fds: dict[str, int] = {}

# used to override mtu_size property

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to delete the comment too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

mtu_size returned the default of 23 with a warning unless the private
_acquire_mtu() was called first, which calls AcquireWrite or
AcquireNotify on a characteristic only to read the MTU from the reply.

BlueZ 5.62 added an "MTU" property to org.bluez.GattCharacteristic1,
which max_write_without_response_size already reads. All
characteristics of a connection share the same ATT MTU, so mtu_size now
reads the "MTU" property of a characteristic in the service collection.
Without the property (BlueZ < 5.62) or without services, mtu_size logs
a warning and returns 23.

The _mtu_size override is removed. _acquire_mtu() is kept for existing
callers but does nothing except issue a DeprecationWarning. Code that
supports older versions of Bleak can call it only when _mtu_size exists.
The mtu_size.py example, which only showed the _acquire_mtu()
workaround, is removed.

The integration test no longer expects a warning on BlueZ.
@nebkat
nebkat force-pushed the bluez-mtu-from-characteristic branch from f9fb1ed to e2ec9c6 Compare November 12, 2026 01:05
BlueZ emits no PropertiesChanged for a characteristic's "MTU" on a reconnect to a device whose GATT objects it kept from an earlier connection, and leaves the property out while the device is disconnected. A properties cache filled in the meantime is stale or lacks it for the whole connection, so mtu_size and max_write_without_response_size read the default of 23.

The manager now reads "MTU" with Properties.Get once services are resolved and keeps it per device until the device disconnects; mtu_size and every characteristic's max_write_without_response_size read that value, falling back to the cached property, then to 23.
@nebkat

nebkat commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Pushed 99e42fe, which reads "MTU" from BlueZ with Properties.Get once services are resolved instead of from the properties cache, and keeps it per device until disconnect for mtu_size and every characteristic's max_write_without_response_size.

Reading it from the cache, as the first commit did, turned out to be wrong on reconnects: BlueZ registers the MTU exchange callback that emits PropertiesChanged("MTU") only on the connection a characteristic was created on, so a reconnect to a device whose GATT objects it kept (about 30 s after a disconnect) never emits it, and the property is omitted while disconnected. A process that connects in that window has no "MTU" cached for the whole connection, so mtu_size would have read 23 and max_write_without_response_size already reads 20 today (reproduced on BlueZ 5.82; details in the description). The BlueZ fix is https://patch.msgid.link/20261114-gatt-client-mtu-reconnect-v1-1-a86edc1eb208@gmail.com, but released BlueZ needs the live read.

@dlech

dlech commented Nov 14, 2026

Copy link
Copy Markdown
Collaborator

The patch link is broken. Are you saying that BlueZ is not sending the properties changed signal when the MTU property changes? Otherwise we should be able to get the property from the BlueZManager rather than calling Properties.Get.

@nebkat

nebkat commented Nov 14, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, fixed now: https://patch.msgid.link/20261114-gatt-client-mtu-reconnect-v1-1-a86edc1eb208@gmail.com

Yes, on a reconnect BlueZ doesn't send it. characteristic_create() registers the MTU exchange callback (the one that emits PropertiesChanged("MTU")) on the bt_att of the connection the characteristic was created on. When the device disconnects, BlueZ keeps the GATT objects for about 30 s, and on a reconnect in that window btd_gatt_client_connected() only swaps in the new client, so nothing is registered on the new bt_att and the exchange never emits "MTU". BlueZ also leaves the property out while the device is disconnected (characteristic_mtu_exists()).

So the manager's cache can't be relied on in that window: a process that starts while the device is disconnected gets the objects without "MTU" and never receives it, and one that was already running keeps the previous connection's value. On BlueZ 5.82 that's max_write_without_response_size reading 20 while busctl get-property … MTU reads 517 on the live object.

With the BlueZ patch the signal is sent and the manager's value would be correct. But until that is accepted this reads it once with Properties.Get after services are resolved. If you'd rather keep the manager as the source, I could do the Properties.Get only when the cached property is missing. That covers the new-process case but not a stale value from the previous connection.


# used to override mtu_size property
# the ATT MTU of the connection, read from BlueZ once services are resolved
self._mtu_size: Optional[int] = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this back in now breaks our suggested workaround of testing if this attribute exists.

await self._get_services(
dangerous_use_bleak_cache=dangerous_use_bleak_cache
)
self._mtu_size = await manager.refresh_mtu(self._device_path)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since MTU can change after this, there isn't much point in saving the value here.

I would also add a comment explaining the BlueZ bug this is working around.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With link to the bug.

Comment on lines +654 to +655
if self._mtu_size is not None:
return self._mtu_size

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the MTU can change at any time, we need to be reading the current MTU here. For example, reading the one from the first characteristic.

Comment thread CHANGELOG.rst

Fixed
-----
* Fixed ``BleakClient.mtu_size`` and ``BleakGATTCharacteristic.max_write_without_response_size`` reading the default MTU in BlueZ backend when reconnecting to a device that BlueZ kept from an earlier connection. The "MTU" property is now read from BlueZ after connecting rather than from the properties cache.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is way too long for a changelog line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants