Skip to content

gatt: Add ATT long-write (Prepare/Execute Write) support#961

Open
miq312 wants to merge 4 commits into
google:mainfrom
miq312:feat/att-long-write
Open

gatt: Add ATT long-write (Prepare/Execute Write) support#961
miq312 wants to merge 4 commits into
google:mainfrom
miq312:feat/att-long-write

Conversation

@miq312

@miq312 miq312 commented Jul 15, 2026

Copy link
Copy Markdown

Add support for the Write Long Characteristic Values procedure, i.e. ATT Prepare Write plus Execute Write. This is the GATT procedure in BLE spec Vol 3 Part G 4.9.4, built on the ATT Queued Writes procedure in Vol 3 Part F 3.4.6. It is the write counterpart to the long-read support that already exists, where read_value falls back to Read Blob Requests for values larger than the MTU.

Previously the GATT server had no handlers for these PDUs, so it replied REQUEST_NOT_SUPPORTED to any Prepare Write Request, meaning a value larger than ATT_MTU minus 3 could not be written. The PDU classes already existed in att.py; only the procedure handling was missing.

Server: queues prepared writes per bearer and reassembles them on Execute with flags 0x01, committing the full value through the normal Attribute.write_value path. Flags 0x00 cancels the queue. A gap in the prepared offsets is rejected with INVALID_OFFSET, mirroring on_att_read_blob_request. The queue is cleaned up on disconnection.

Client: write_value with with_response now switches automatically to Prepare and Execute when the value exceeds ATT_MTU minus 3, chunking into parts of ATT_MTU minus 5 bytes, mirroring how read_value handles long reads. Short writes are unchanged.

Tests: client-side auto long-write round-trip, manual Prepare and Execute round-trip, cancel leaves the value unwritten, and non-contiguous offsets are rejected.

Comment thread bumble/gatt_client.py Outdated
# the part value is limited to ATT_MTU - 5 bytes.
max_part_size = self.mtu - 5
offset = 0
while offset < len(value):

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.

Suggested change
while offset < len(value):
for offset in range(0, len(value), max_part_size):

Comment thread bumble/gatt_server.py
for handle, value in values.items():
attribute = self.get_attribute(handle)
assert attribute is not None
await attribute.write_value(bearer, bytes(value))

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 debatable here, and also why I didn't implement long write. On most platforms, including Android, iOS, and BlueZ, write request callbacks have arguments for offset, and every prepared write request is handled by handler applications. Whether to execute long reliable write simultaneously is actually decided by handlers. Thus, the design here will be different from most platforms.

Also, Core spec doesn't regulate the detail about prepared write. For example, sending (offset=0, length=3), (offset=4,length=1) (byte 3 is missing) is valid. Of course, apps can reject them, but here the behavior just fixed by the server class.

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.

However, handling like these platforms is also challenging for us, because we didn't introduce the offset parameter at first. The only way to achieve that is to introduce V3 handler, but it worths a hesitation.

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.

Supporting the most flexible case, where the low level offset-based reads and writes are exposed to the handlers would indeed require some API extension ("v3 handler"), that's probably not needed for most use cases, where what you want it maximum simplicity, where the application only deals with complete payloads (including the ability to encode/decode automatically into specific data types, which is very convenient). So implementing the simple case first, where re-assembly is done in the library, seems like a good option, even if we have to enforce some fixed policy (like "no gaps allowed", for example).

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.

3 participants