Skip to content

fix(ble): drop the hand-rolled CCCD that made the command characteristic unsubscribable - #92

Merged
ehsan6sha merged 1 commit into
mainfrom
fix/ble-duplicate-cccd
Sep 1, 2026
Merged

ehsan6sha merged 1 commit into
mainfrom
fix/ble-duplicate-cccd

Conversation

@ehsan6sha

Copy link
Copy Markdown
Member

A browser could connect to a Blox, read it and write to it — but never subscribe for replies. Replies are delivered only on the command characteristic, so no browser could receive any answer at all: every command appeared to send, then silently timed out.

The failure

startNotifications() threw NotSupportedError four times out of four. Isolating each GATT step from Chrome 151 on Android against a live Blox:

connect                 ok
getPrimaryService       ok
getCharacteristic       ok
writeValueWithResponse  ok
startNotifications      NotSupportedError: GATT operation failed for unknown reason

The cause, from the client's own view of the descriptors

command   (00000003-…):  00002902-…  +  00002902-…    <- two CCCDs
broadcast (00000002-…):  00000003-…  +  00002902-…    <- one, and it works

BlueZ creates and owns the 0x2902 Client Characteristic Configuration Descriptor for any characteristic flagged notify or indicate. CommandDescriptor declared a second one. Subscribing is a CCCD write, so the client hit the ambiguity — and the broadcast characteristic escaped it only because its own descriptor uses a custom UUID.

Verified on hardware before committing

With the descriptor removed and bluetooth.py restarted on a live Blox, the phone subscribes successfully and the reply arrives. This is not a reasoned fix.

The mobile app was unaffected throughout, which is why this survived so long — Android's CCCD write goes through a different path and evidently tolerates the duplicate.

Notes on the diff

CommandDescriptor is deleted rather than left unreferenced, so reattaching it takes a deliberate act. Its WriteValue drove StartNotify/StopNotify by hand; BlueZ now calls those on the characteristic itself. It was also the only caller of StartIndicate/StopIndicate, so nothing sets indicating any more and the indicate flag is vestigial — worth removing as a follow-up, left alone here because the configuration proven on hardware kept it.

Still outstanding, separately

Subscribing now works, but the log fetch still returns an incomplete reply: the system half alone is over 15 KB, and prepare_response halves its own chunk size with a // 2 margin on top of a loop that already measures each encoded chunk and shrinks it on overflow. That doubles the frame count for nothing, and replies travel as unacknowledged notifications. Dropping the margin roughly halves the frames. Happy to open that separately.

Closes #91.


🤖 Generated with Claude Code

https://claude.ai/code/session_01YJyqpFReP1wbz86nsqsoXq

…tic unsubscribable

A browser could connect to a Blox, read it and write to it, but never subscribe
for replies -- and replies are delivered only on the command characteristic, so
no browser could receive any answer at all. Every command appeared to be sent and
then silently timed out.

startNotifications() failed with NotSupportedError, every time, on four
consecutive attempts. Isolating each GATT step from Chrome 151 on Android against
a live Blox:

    connect                 ok
    getPrimaryService       ok
    getCharacteristic       ok
    writeValueWithResponse  ok
    startNotifications      NotSupportedError

The cause is visible in the descriptors the client sees:

    command   (00000003-...):  00002902-...  +  00002902-...   <- two CCCDs
    broadcast (00000002-...):  00000003-...  +  00002902-...   <- one, and it works

BlueZ creates and owns the 0x2902 Client Characteristic Configuration Descriptor
for any characteristic flagged notify or indicate. CommandDescriptor declared a
second one. Subscribing IS a CCCD write, so the client hit the ambiguity; the
broadcast characteristic escaped it because its own descriptor uses a custom UUID.

Verified on hardware before committing: with the descriptor removed and
bluetooth.py restarted, the phone subscribes and the reply arrives.

The mobile app was unaffected throughout, which is why this survived so long --
Android's CCCD write goes through a different path and evidently tolerates the
duplicate.

CommandDescriptor is deleted rather than left unreferenced so that reattaching it
takes a deliberate act. Its WriteValue drove StartNotify/StopNotify by hand and
BlueZ now calls those on the characteristic itself. It was also the only caller of
StartIndicate/StopIndicate, so nothing sets indicating any more and the
indicate flag is now vestigial -- worth removing as a follow-up, left here
because the configuration proven on hardware kept it.

Closes #91.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJyqpFReP1wbz86nsqsoXq
@ehsan6sha
ehsan6sha merged commit 78a056e into main Sep 1, 2026
4 checks passed
@ehsan6sha
ehsan6sha deleted the fix/ble-duplicate-cccd branch September 1, 2026 03:32
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.

BLE: startNotifications() on the command characteristic always fails from a browser (hand-declared 0x2902 CCCD)

1 participant