Skip to content

Android: serialize GATT operations per device natively - #302

Closed
fotiDim wants to merge 3 commits into
mainfrom
feat/android-native-per-device-queue
Closed

fotiDim wants to merge 3 commits into
mainfrom
feat/android-native-per-device-queue

Conversation

@fotiDim

@fotiDim fotiDim commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #297

Problem

Android's BluetoothGatt enforces a strict single-outstanding-operation rule per device (the AOSP mDeviceBusy lock). When the Dart queue is bypassed — QueueType.none, or the unqueued readRssi from #298 — concurrent writes/rssi/mtu/discovery calls collide: readRemoteRssi()/writeCharacteristic() return false immediately and Dart futures fail with status 133 (GATT_ERROR).

The cross-platform Dart queue can't protect Android: it's the very abstraction Apple requires to be bypassed for write throughput (#271), and Android is the only platform whose native stack has no queue of its own.

Solution

Add PerDeviceGattQueue, a per-device FIFO that serializes GATT operations natively in Kotlin:

  • Operations for the same device run strictly one at a time; the next operation is dispatched only when the in-flight one's matching GATT callback fires (onCharacteristicWrite, onCharacteristicRead, onDescriptorWrite, onDescriptorRead, onReadRemoteRssi, onMtuChanged, onServicesDiscovered), or it completes synchronously.
  • Each operation is tagged with a "kind" so a stray/out-of-order callback can never release an unrelated operation.
  • Different devices are independent (Android permits concurrent ops across connections), so there's no global serialization penalty.
  • Queue is cleared on disconnect (cleanUpConnection) and engine detach.

Routed through the queue: writeValue, readValue, writeDescriptorValue, readDescriptorValue, setNotifiable (CCCD), readRssi, requestMtu, discoverServices (including the background discovery from getSystemDevices), and requestConnectionPriority on Android O+ (where onConnectionUpdated — which clears mDeviceBusy — is actually delivered).

Why native, not a Dart-side guard

With a native per-device queue, Android is protected regardless of QueueType, so the API stays zero-configuration and QueueType.none remains safe on Android — no Dart-side enforcement needed. A Dart-side "don't allow none on Android" would just reintroduce slow Dart serialization for everyone.

Testing

  • New PerDeviceGattQueueTest covering FIFO ordering, kind-matching, cross-device independence, cancel-on-disconnect, sync completion, and exception safety (8 tests).
  • Existing Android unit tests (WriteCompletionTest, DisconnectCloseTest, UniversalBleHelperTest) all pass.
  • flutter analyze clean.

No API changes; this is purely native serialization behind the existing method channel.

Android's BluetoothGatt enforces a strict single-outstanding-operation rule
per device (the AOSP mDeviceBusy lock). Bypassing the Dart queue (QueueType.none,
or the unqueued readRssi from #298) made concurrent writes/rssi/mtu/discovery
collide, returning false or status 133 (GATT_ERROR).

Add PerDeviceGattQueue, a per-device FIFO that runs GATT operations one at a
time and only dispatches the next operation once the in-flight one's matching
GATT callback fires (or it completes synchronously). Different devices stay
independent. Route read/write/descriptor-read/descriptor-write/setNotifiable/
readRssi/requestMtu/discoverServices through the queue; requestConnectionPriority
is serialized on O+ where onConnectionUpdated is delivered.

Refs #297
@fotiDim
fotiDim force-pushed the feat/android-native-per-device-queue branch from 94fb620 to 8a12a93 Compare September 7, 2026 21:08
- Bump version to 3.0.0 (breaking: default queueType changed from global)
- BleCommandQueue now defaults to QueueType.none (parallel) - serialization
  is handled natively per platform (Android PerDeviceGattQueue, Apple
  CoreBluetooth write pipelining)
- Update docstrings, CHANGELOG, and README Command Queue sections
- Update tests that assumed the global default; add default-is-none test

Refs #297
@navidecklabs
navidecklabs requested a lite review from Copilot September 8, 2026 04:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@fotiDim

fotiDim commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

From my testing, there is no need to queue requests on Apple and Windows 11. Linux also behaved fine for the most part.
This PR changes the default queue to none, and implement a native Android queue.
I will test more on Linux, Windows 10 and web and do anything fine-tuning in follow-up PRs.

@fotiDim fotiDim closed this Sep 8, 2026
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.

Move BLE serialization to native layer to fix write throughput degradation

2 participants