Skip to content

fix(ble): size notification chunks for the MTU that was negotiated, not 512 - #93

Closed
ehsan6sha wants to merge 1 commit into
mainfrom
fix/ble-notify-mtu
Closed

fix(ble): size notification chunks for the MTU that was negotiated, not 512#93
ehsan6sha wants to merge 1 commit into
mainfrom
fix/ble-notify-mtu

Conversation

@ehsan6sha

Copy link
Copy Markdown
Member

fix(ble): size notification chunks for the MTU that was negotiated, not 512

The Bluetooth logs panel showed its two headers -- "=== Docker Logs ===" and
"=== System Logs ===" -- with nothing under them. The reply was arriving as
damaged JSON, failing to parse, and leaving an object with no docker and no
system key for the client to render.

Every chunk goes out as a D-Bus PropertiesChanged "Value", and BlueZ silently
truncates that to the NEGOTIATED ATT MTU minus 3. BLEResponseHandler assumed
512 -- the largest MTU anyone negotiates, not the smallest -- and nothing here
can see the real one: service.py implements none of the BlueZ APIs that report
it (no AcquireNotify, no MTU property), so there is no value to read and no
error when a chunk is cut short.

With base_overhead 47 and the old halving, chunks came out at ~278 bytes. A
phone that settles on ATT_MTU 247 -- completely ordinary on Android -- cuts
those at 244. Every chunk lands damaged, and the reply can never be
reassembled.

assumed 512, //2 chunk ~278 B truncated at 244
assumed 512, no //2 chunk ~509 B truncated at 244 (worse)
assumed 185, no //2 chunk ~182 B fits

So the fix is NOT simply dropping the // 2. Doing that alone doubles the
chunk size to ~509 B and breaks the transfer on every link below 512 -- the
halving was, by accident, the only thing keeping chunks anywhere near a real
MTU. Both parts have to move together:

  • assume 185, the conservative floor (what iOS uses, at or below every MTU
    seen in practice), so a chunk fits whatever was actually negotiated;
  • drop the // 2, which is redundant now the budget is honest. Its stated
    job -- worst-case JSON double-escaping -- is already done by the loop that
    measures the ENCODED chunk and shrinks until it fits.

The cost is more chunks per reply. A truncated chunk is a certain failure; a
longer transfer is not.

Verified by simulating the chunker over a realistic 4722-byte log payload
(quotes, backslashes, newlines, unicode, random printable): 42 chunks, max
chunk 182 bytes, every chunk within budget, fits ATT_MTU 247 and 185, and the
reassembled payload is byte-identical to the input.

Not verified on hardware: which MTU this Blox actually negotiates. That is the
one thing that would turn "explains the symptom" into "was the symptom", and
it needs a device.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01YJyqpFReP1wbz86nsqsoXq

…ot 512

The Bluetooth logs panel showed its two headers -- "=== Docker Logs ===" and
"=== System Logs ===" -- with nothing under them. The reply was arriving as
damaged JSON, failing to parse, and leaving an object with no `docker` and no
`system` key for the client to render.

Every chunk goes out as a D-Bus PropertiesChanged "Value", and BlueZ silently
truncates that to the NEGOTIATED ATT MTU minus 3. `BLEResponseHandler` assumed
512 -- the largest MTU anyone negotiates, not the smallest -- and nothing here
can see the real one: service.py implements none of the BlueZ APIs that report
it (no AcquireNotify, no MTU property), so there is no value to read and no
error when a chunk is cut short.

With base_overhead 47 and the old halving, chunks came out at ~278 bytes. A
phone that settles on ATT_MTU 247 -- completely ordinary on Android -- cuts
those at 244. Every chunk lands damaged, and the reply can never be
reassembled.

  assumed 512, //2      chunk ~278 B    truncated at 244
  assumed 512, no //2   chunk ~509 B    truncated at 244  (worse)
  assumed 185, no //2   chunk ~182 B    fits

So the fix is NOT simply dropping the `// 2`. Doing that alone doubles the
chunk size to ~509 B and breaks the transfer on every link below 512 -- the
halving was, by accident, the only thing keeping chunks anywhere near a real
MTU. Both parts have to move together:

  - assume 185, the conservative floor (what iOS uses, at or below every MTU
    seen in practice), so a chunk fits whatever was actually negotiated;
  - drop the `// 2`, which is redundant now the budget is honest. Its stated
    job -- worst-case JSON double-escaping -- is already done by the loop that
    measures the ENCODED chunk and shrinks until it fits.

The cost is more chunks per reply. A truncated chunk is a certain failure; a
longer transfer is not.

Verified by simulating the chunker over a realistic 4722-byte log payload
(quotes, backslashes, newlines, unicode, random printable): 42 chunks, max
chunk 182 bytes, every chunk within budget, fits ATT_MTU 247 and 185, and the
reassembled payload is byte-identical to the input.

Not verified on hardware: which MTU this Blox actually negotiates. That is the
one thing that would turn "explains the symptom" into "was the symptom", and
it needs a device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJyqpFReP1wbz86nsqsoXq
@ehsan6sha
ehsan6sha marked this pull request as draft September 1, 2026 03:46
@ehsan6sha

Copy link
Copy Markdown
Member Author

Holding this as a draft. It does not survive a question from the field.

The Blox's Bluetooth logs DID work from both the Android and the iOS native
apps, and only came back empty in the browser. Same Blox, same bluetooth.py,
same chunk sizes on the wire -- so if the firmware were emitting chunks too
large for the link, every client would have broken, not just the browser. An
MTU that is too optimistic cannot be the explanation for a browser-only
failure.

The asymmetry I leaned on does not exist either. I claimed the mobile app asks
for a bigger MTU with requestMTU(512) and that Web Bluetooth cannot. The second
half is true; the first is not -- requestMTU appears nowhere in apps/box.
That claim is also written into a comment in the web app
(platform/bluetooth/responseAssembler.ts) and needs correcting there too.

Worse, the change may push the wrong way. Sizing to 185 roughly doubles the
number of notifications per reply, and unacknowledged notification loss under a
burst is now the leading explanation for a browser-only failure -- Chrome
delivers them across a process boundary that a native app does not have.

Not deleting the branch: assuming 512 when nothing can read the negotiated MTU
is still wrong, and this is the right shape for that fix once there is evidence
about what is actually negotiated. It should not ship device-wide on a theory
that the field report contradicts.

@ehsan6sha ehsan6sha closed this Sep 2, 2026
@ehsan6sha

Copy link
Copy Markdown
Member Author

not needed because the BLE issue was not related to this fix

@ehsan6sha
ehsan6sha deleted the fix/ble-notify-mtu branch September 2, 2026 15:14
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.

1 participant