Add BluetoothRemoteGATTCharacteristic.maxWriteWithoutResponseSize#672
Add BluetoothRemoteGATTCharacteristic.maxWriteWithoutResponseSize#672hjanuschka wants to merge 8 commits into
Conversation
Expose the negotiated ATT_MTU for the connection carrying a characteristic via a new getMTU() method, returning the smaller of the local Host's transmit ATT_MTU and the peer's receive ATT_MTU as established by the Exchange MTU procedure. Knowing the MTU lets sites size writeValueWithoutResponse() payloads to fit a single PDU and avoid fragmentation.
I think it has to be there because of the way BlueZ implements it. In theory, other OSes could add support for per-characteristic MTU in the future as well.
Since the actual useful information we want is "what is the max size can I use for write without response", I would use a name inspired from CoreBluetooth instead, e.g. Since most OSes don't actually perform any I/O to get the number (they just return whatever number they currently know), making it a promise seems misleading. I also know from experience with maintaining the Bleak Python library that MTU changes can come late (after connecting and resolving services), particularly on Windows. So it could actually make more sense to make this an event rather than an attribute. Or we just need to document that the value can change, so it should be read every time that it is used rather than caching it in some variable in the user application. |
Address review feedback: rename getMTU() to a synchronous maxWriteWithoutResponseSize attribute (inspired by CoreBluetooth) that returns ATT_MTU - 3, the largest payload writeValueWithoutResponse() can send in a single PDU. Since platforms return a cached value without performing I/O, a plain attribute is more honest than a promise. Document that the value can change while connected and should be read each time rather than cached.
Pair the live maxWriteWithoutResponseSize attribute with an event so applications can either read the current value at write time or react to changes pushed by the UA (e.g. a late Exchange MTU renegotiation). The event does not carry the value; listeners read the attribute, keeping a single source of truth. Add the onmaxwritewithoutresponsesizechanged handler to CharacteristicEventHandlers and a "Responding to MTU Changes" algorithm that fires the event.
|
Thanks, pushed new commit.
LMKWYT |
Reference the attribute from its getter-steps block instead of defining a second <dfn>, which removes the duplicate id and the conflicting local attribute definition. Drop the unresolved "service discovery" autolink and move the trailing note out of the list so the closing tag parses as valid Markdown.
reillyeon
left a comment
There was a problem hiding this comment.
This looks like generally the right solution. Will follow up on the proposed Chromium change to make sure this is implementable.
|
Please update the PR title and description to match the latest revision. |
I forgot about this when I suggested an event, but CoreBluetooth doesn't have a delegate callback to be notified when the MTU changes. So it probably doesn't make sense to have an event as part of the API (unless we can be sure that |
Introduce a "characteristic" variable in the "for each Characteristic" clause and reference it in the sub-steps instead of repeating "the Characteristic", per review feedback.
Some platforms (notably CoreBluetooth) do not surface ATT_MTU changes to the UA, so the event cannot fire there. Document it as a best-effort hint to re-read maxWriteWithoutResponseSize rather than an authoritative or guaranteed signal, and tell applications not to rely on it as the only way to observe the value.
Rather than drop it, I've specced the event as best-effort: it MAY fire when the UA learns of an MTU change, but isn't guaranteed on platforms like CoreBluetooth that don't surface those changes. that keeps the event usable on platforms that support it. hope that is ok for you! |
| best-effort basis. Some platforms do not notify the UA when the ATT_MTU | ||
| changes, in which case the event does not fire even though | ||
| {{BluetoothRemoteGATTCharacteristic/maxWriteWithoutResponseSize}} may return a | ||
| different value on a subsequent read. The event is therefore a hint to |
There was a problem hiding this comment.
This isn't how you've implemented it in Chromium. I think if the implementation is capable of noticing changes and returning a different value it could fire an event. Otherwise if the platform doesn't provide a notification then the property also would never change.
There was a problem hiding this comment.
The getter is now defined to return the current ATT_MTU at read time, so a fresh read always reflects the present value; the maxwritewithoutresponsesizechanged event is just a convenience notification fired when the UA observes a change, not the only way the value updates.
That lines up with the Chromium CL: the value is seeded from the live MTU at characteristic enumeration and refreshed by the event on BlueZ/Android/WinRT. its not 100% "fresh" but the caching is what i think implementational detail, as the developer observed value always represents the real value.
macOS is the one platform with no MTU-change callback, but CoreBluetooth settles the ATT_MTU at connection time (before characteristics are discovered) and never renegotiates, so the value there is final from the first read and never goes stale.
There was a problem hiding this comment.
My issue with the current text is that it implies that on platforms where there is no MTU change event the value will be read live rather than being cached and could change even without a maxwritewithoutresponsesizechanged event. This isn't how it is implemented.
There was a problem hiding this comment.
Fair. I'll flip the spec to the coupled model: the value updates together with the maxwritewithoutresponsesizechanged event when the UA observes an MTU change, and on platforms with no change notification the value just stays constant after it's first established. That matches the cached implementation, the value never changes without the event firing.
Drop the cached internal-slot model in favor of an on-demand getter that reflects the ATT_MTU at read time. This way a fresh read returns the current value even on platforms (such as CoreBluetooth) that do not surface ATT_MTU changes via the maxwritewithoutresponsesizechanged event. The event remains a convenience notification fired when the UA observes a change; the attribute is the authoritative source.
Restore a stored value that is established at characteristic discovery and only updated when the UA observes an ATT_MTU change, at which point the maxwritewithoutresponsesizechanged event fires. This matches the cached Chromium implementation: the value never changes without the event firing, and platforms with no MTU-change notification (CoreBluetooth) keep a constant value and never fire the event. Replaces the earlier live-read wording that implied the value could change without an event.
| not surface ATT_MTU changes to the UA at all; on those platforms the value | ||
| stays constant after it is first established and the event never fires. |
There was a problem hiding this comment.
Can we really be sure of this? Bluetooth 5.2 allows the MTU to change later. And since we can't see Apple's source code, we don't know if they handle this or not.
This is why I am having second thoughts about having an event. If it doesn't work on all platforms, then it can't be relied upon. And if it can't be relied upon, then what is the point of having it?
There was a problem hiding this comment.
If the platform will change its answer without firing an event and the browser is caching the value then it doesn't matter if the platform silently changes it because the browser won't notice. I think this is fine, and if the browser does check again for some reason then it could fire an event and update the value.
The key thing is that unless we go back to the asynchronous getMTU() method, we can't guarantee that the attribute will reflect the latest value unless the OS provides an event. At best, when the site accesses the attribute we could trigger a check in the background and fire an event if we discover a new value.
There was a problem hiding this comment.
At best, when the site accesses the attribute we could trigger a check in the background and fire an event if we discover a new value.
that is meh and smart at the same time - to close the platform gap, i love it!
as you both have maybe figured out, i am pretty new to spec's (in general), not sure how to proceed now!
There was a problem hiding this comment.
I would expect the implementation to either not cache the value if a synchronous getter is available on the OS. Or cache the value and always be subscribed to changes if there isn't such a getter.
In other words, I would expect reading the maxWriteWithoutResponseSize attribute in the browser would synchronously call GattSession.MaxPduSize on WinRT and maximumWriteValueLengthForType(CBCharacteristicWriteWithoutResponse) on CoreBluetooth.
For BlueZ, I would expect that we are already subscribed to D-Bus property changes on characteristics so should always have the latest value without having to do an async D-Bus call. Similarly, on Android, I would expect that we would always be subscribed to onMtuChanged so that we always have the latest value available.
There was a problem hiding this comment.
still not sure if i am fully understanding the concerns, the described "never cache and also synch" is a potential blocker for the browser, from my point of view.
all platforms have events, that totally solve the issue, except one, wouldn't it be better to fix that one platform?
(or use @reillyeon approach of synthetic events?)
anyway, i am following whatever you as experts resolve to, its just my 2cents, and i am trying to move the implementation forward.
There was a problem hiding this comment.
I don't see the event as a Windows-only workaround, though. The attribute is the simple path for apps that just want a size at the time they start writing. The event is the complementary path for apps that want to react if the UA later learns a better current size. Windows is one concrete case where that can happen (thats where i am comming from, trying to fix the limitation on windows), but the web-facing model does not have to expose that as a Windows quirk.
From the developer point of view the API stays simple: read the synchronous attribute; optionally listen for maxwritewithoutresponsesizechanged if you want to re-chunk/retry when the usable size improves. For example, your snippet could become:
async function writeFirmwareBlob(characteristic, array) {
let chunkSize = characteristic.maxWriteWithoutResponseSize;
characteristic.addEventListener("maxwritewithoutresponsesizechanged", () => {
chunkSize = characteristic.maxWriteWithoutResponseSize;
});
for (let i = 0; i < array.length; ) {
const chunk = array.slice(i, i + chunkSize);
await characteristic.writeValueWithoutResponse(chunk);
i += chunk.byteLength;
// Some delay or back-pressure check here.
// If the MTU improves while this is running, the next iteration uses the
// new chunk size.
}
}Platforms like CoreBluetooth that do not report later changes can simply never fire it. Platforms that do report changes can fire it natively.
That also fits Chromium's architecture: Blink can expose the synchronous attribute from renderer-held state, while the browser process updates that state asynchronously when it observes a change. So we avoid blocking the renderer while still giving developers one consistent API surface.
There was a problem hiding this comment.
Also, even without listening for the event, apps can avoid holding a stale local copy by reading the attribute at the point of use instead of copying it once:
for (let i = 0; i < array.byteLength; ) {
const chunkSize = characteristic.maxWriteWithoutResponseSize;
const chunk = array.slice(i, i + chunkSize);
await characteristic.writeValueWithoutResponse(chunk);
i += chunk.byteLength;
}Whether Chromium implements that synchronous attribute from renderer-held state or another UA reads it directly from its platform backend is an implementation detail. The web-observable contract is just that the getter returns the UA's current known maximum.
There was a problem hiding this comment.
This second example is what makes me think we don't really need an event. Unless there are other use cases?
There was a problem hiding this comment.
true, but, i initially came from the issue that e.g windows having issues.
still noob here on the spec side, not sure what is required for spec, and what is implementational-freedom of the UA :/
@reillyeon guess we could, on chromium side, block till windows has working mtu (eventhough it might block)? and go from there?
thank you both for the time and help!
Hi folks 👋
I've been working on the Chromium implementation for exposing the
negotiated ATT MTU to web pages (crrev.com/c/7879985)
What this adds
A new synchronous
maxWriteWithoutResponseSizeattribute onBluetoothRemoteGATTCharacteristic:It returns the largest byte sequence, in octets, that a single
writeValueWithoutResponse()can send without being rejected. It isderived from the ATT_MTU negotiated for the connection that carries the
characteristic (the smaller of the local Host's transmit ATT_MTU and the
peer's receive ATT_MTU), minus the 3-octet ATT Write Command header. When
no MTU exchange has happened it's
23 - 3 = 20octets.The attribute is evaluated at read time, so there's no cached value in the
spec object: the next read after an MTU change returns the new value.
I also added a
maxwritewithoutresponsesizechangedevent (bubbles, likecharacteristicvaluechanged) plus theonmaxwritewithoutresponsesizechangedhandler, so apps that batch largetransfers can react to an increase instead of polling. The event
deliberately doesn't carry the value; listeners read the attribute, keeping
a single source of truth.
Why I want this
Today there's no way for a site to know how big a single write can be. The
practical pain is
writeValueWithoutResponse(): if you hand it more thanATT_MTU - 3bytes, the write either fails or gets silently choppeddepending on the platform, and the page has no way to tell ahead of time.
People end up hard-coding 20 (the old default payload) to be safe, which
leaves a lot of throughput on the table on modern devices that happily
negotiate 247+.
Exposing the max write size lets sites chunk their writes to fit a single
PDU and avoid unnecessary round trips. This has been asked for a few times
over the years (40265040 / 40686244 / 40163619 on the Chromium side).
Design notes
ATT_MTU - 3(inspired by CoreBluetooth'smaximumWriteValueLengthForType:) rather than the raw MTU means callersdon't have to know to subtract the ATT Write Command header themselves.
BluetoothRemoteGATTServerbecause BlueZ tracks MTU per characteristic, and other OSes could add
per-characteristic MTU support in the future.
maxwritewithoutresponsesizechangedevent may never fire on macOS.Still deciding whether to keep it as best-effort or drop it in favor of
"read the attribute every time you need it".
Preview | Diff