bluebird 0.4.0
bluebird: ^0.4.0 copied to clipboard
A Bluetooth Low Energy (BLE) plugin for Flutter — scan, connect, and read/write GATT on Android, iOS, macOS, and Web.
A Bluetooth Low Energy (BLE) plugin for Flutter.
- Zero dependencies
- Easy-to-use interface
- Comprehensively tested
- Permissive license (BSD-3)
Tip
Try the demo in your browser or download the Android demo APK from the latest release.
Important
Migrating from FlutterBluePlus? Check out the migration guide.
Quick Start #
Turn on #
// On iOS this is controlled by the user
if (Platform.isAndroid) await Bluebird.turnOn();
// Wait for adapter to be ready
await Bluebird.adapterReady();
Scan for devices #
await for (final ScanResult r in Bluebird.scan(withServices: [Uuid("180D")], withNames: ["Bluno"])) {
print('${r.device.remoteId}: "${r.advertisementData.advName}"');
}
Tip
No need to explicitly startScan/stopScan - once you break from the loop (or cancel the underlying subscription) the scan is automatically stopped!
Connect to a device #
// Obtain a device from a ScanResult
final BluetoothDevice device = scanResult.device;
// Or from raw address: = BluetoothDevice.fromId("77:c6:24:e3:bb:ec");
await device.connect();
// Disconnect when you're done.
await device.disconnect();
Discover services, characteristics & descriptors #
await device.discoverServices();
final service = device.services.firstWhere((s) => s.uuid == Uuids.service.deviceInformation);
final characteristic = service.characteristics.firstWhere((c) => c.uuid == Uuids.characteristic.manufacturerName);
final descriptor = characteristic.descriptors.firstWhere((c) => c.uuid == Uuids.descriptors.characteristicUserDescription);
Warning
Calling discoverServices (and disconnecting) invalidates any previously discovered attributes, as the
underlying Bluetooth handles are only valid within a connection and may change on re-discovery. Always
re-fetch from device.services after (re-)connecting. Using a stale attribute throws — check
attribute.isValid.
Subscribe to characteristic notifications #
// Enables notifications (or indications) on the peripheral (check c.canNotify if unsure)
final subscription = characteristic.notifications.listen(
(value) {
// Called for each notification/indication
},
// The stream *errors* if the peripheral fails to enable notifications
onError: (e) => print("failed to enable notify: $e"),
);
// Disables notifications on the peripheral
await subscription.cancel();
Tip
No need to explicitly manage the underlying subscription state. If there are any active Flutter subscriptions the platform enables CCCD, and once all subscriptions are cancelled it disables CCCD.
Read & write characteristics #
// Read (check c.canRead if unsure)
List<int> value = await c.read();
// Write (check c.canWrite if unsure)
await c.write([0x12, 0x34]);
Read & write descriptors #
// Read
List<int> value = await d.read();
// Write
await d.write([0x12, 0x34])
L2CAP channels #
Open a connection-oriented L2CAP channel — a bidirectional byte stream to the peer, independent of GATT (Android & iOS/macOS; not supported on Web):
final channel = await device.openL2capChannel(0x80); // the peer's PSM
channel.input.listen((bytes) { /* data from the peer */ });
await channel.write(Uint8List.fromList([0x12, 0x34]));
await channel.close();
Example #
Bluebird ships with an example app that is useful for debugging issues.
cd ./example
flutter run
You can also try it without building anything:
- Web: live at nebkat.github.io/flutter-bluebird (Chrome/Edge — Web Bluetooth is Chromium-only).
- Android: download the demo APK attached to the latest release.
Project Setup #
Android #
minSdkVersion
In android/app/build.gradle:
android {
defaultConfig {
minSdkVersion: 24 // or higher
Permissions (without fine location)
In android/app/src/main/AndroidManifest.xml:
<!-- Tell Google Play Store that your app uses Bluetooth LE
Set android:required="true" if bluetooth is necessary -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- legacy for Android 11 or lower -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>
<!-- legacy for Android 9 or lower -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
Permissions (with fine location)
If you want to use Bluetooth to determine location, or support iBeacons.
In android/app/src/main/AndroidManifest.xml:
<!-- Tell Google Play Store that your app uses Bluetooth LE
Set android:required="true" if bluetooth is necessary -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- legacy for Android 11 or lower -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!-- legacy for Android 9 or lower -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
And set androidUsesFineLocation when scanning:
// Start scanning
Bluebird.scan(androidUsesFineLocation: true).listen((r) { /* ... */ });
Proguard
In project/android/app/proguard-rules.pro:
-keep class com.lib.bluebird.* { *; }
To avoid seeing the following errors in your release builds:
PlatformException(startScan, Field androidScanMode_ for m0.e0 not found. Known fields are
[private int m0.e0.q, private b3.b0$i m0.e0.r, private boolean m0.e0.s, private static final m0.e0 m0.e0.t,
private static volatile b3.a1 m0.e0.u], java.lang.RuntimeException: Field androidScanMode_ for m0.e0 not found
iOS #
Permissions
In ios/Runner/Info.plist:
<dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs Bluetooth to function</string>
For location permissions on iOS see more at: https://developer.apple.com/documentation/corelocation/requesting_authorization_for_location_services
macOS #
Make sure you have granted access to the Bluetooth hardware:
Xcode -> Runners -> Targets -> Runner-> Signing & Capabilities -> App Sandbox -> Hardware -> Enable Bluetooth
Advanced Usage #
Error Handling #
Every error returned by the native platform is checked and thrown as an exception where appropriate. See Reference for a list of throwable functions.
Streams: Most state streams returned by Bluebird (e.g. adapterState, device.connectionState, device.mtu) never emit errors and never close, so there's no need to handle onError or onDone. The exceptions are the operation streams that can fail: Bluebird.scan() (errors if a scan cannot start) and characteristic.notifications / characteristic.values (error if enabling notify fails) — handle onError on those.
Logging #
All Bluebird logs flow through a single package:logging Logger, exposed as Bluebird.logger. It is detached and silent by default. For the common case — just print to the console — there's a one-liner:
Bluebird.configureLoggerPrinting(); // prints INFO and above; pass level: to change it
For anything else, routing and filtering are entirely yours — add package:logging to your pubspec.yaml, then listen to onRecord and set the level:
Bluebird.logger.onRecord.listen((r) => myLogger.log(r)); // e.g. a file, Crashlytics, …
Bluebird.logger.level = Level.INFO; // FINE / FINEST / … to see more
Low-level platform-channel call tracing — every call in and out — is logged at Level.FINEST, so lower the logger's level to see it:
Bluebird.logger.level = Level.FINEST;
Separately, Bluebird.setPlatformLogLevel(LogLevel.verbose) turns up the native logging (Android logcat / Apple os_log); those lines surface through the platform's own tooling, not through Bluebird.logger.
Large characteristic writes #
allowLongWrite: To write large characteristics (up to 512 bytes) regardless of mtu, use allowLongWrite:
/// allowLongWrite should be used with caution.
/// 1. it can only be used *with* response to avoid data loss
/// 2. the peripheral device must support the 'long write' ble protocol.
/// 3. Interrupted transfers can leave the characteristic in a partially written state
/// 4. If the mtu is small, it is very very slow.
await c.write(data, allowLongWrite: true);
splitWrite: To write lots of data (unlimited), you can define the splitWrite function.
import 'dart:math';
// split write should be used with caution.
// 1. due to splitting, `characteristic.read()` will return partial data.
// 2. it can only be used *with* response to avoid data loss
// 3. The characteristic must be designed to support split data
extension splitWrite on BluetoothCharacteristic {
Future<void> splitWrite(List<int> value, {Duration timeout = const Duration(seconds: 15)}) async {
int chunk = device.maxAttrLen.value; // mtu - 3 bytes BLE overhead, capped at 512
for (int i = 0; i < value.length; i += chunk) {
List<int> subvalue = value.sublist(i, min(i + chunk, value.length));
await write(subvalue, withoutResponse:false, timeout: timeout);
}
}
}
Values (including reads) #
characteristic.values is like notifications but also includes the result of
every read(). It is convenient for characteristics that support both READ and
NOTIFY — e.g. a "light switch toggle". Like notifications, listening enables
notify; valuesPassive is the non-subscribing variant.
final subscription = characteristic.values.listen((value) {
// emitted anytime read() is called *or* a notification arrives
});
await characteristic.read(); // also flows through `values`
await subscription.cancel();
Keeping notify enabled independently #
If you want to keep notify enabled regardless of who is listening — or share it
between several consumers — hold a CharacteristicSubscription from subscribe()
and observe the passive streams (which do not toggle notify themselves):
// enable notify and keep it on until you unsubscribe
final handle = await characteristic.subscribe();
// observe values without affecting the notify state
final sub = characteristic.notificationsPassive.listen((value) { /* ... */ });
// later — release your handle (disables notify once nothing else holds it)
await sub.cancel();
await handle.unsubscribe();
Typed characteristic & descriptor views #
characteristic.map(decode, encode:) returns a MappedBluetoothCharacteristic<T> —
a typed view whose read/write/notifications speak T instead of List<int>,
so you decode/encode in one place instead of at every call site.
// decode bytes -> T on the way out; encode T -> bytes on the way in
final level = characteristic.map(
(bytes) => bytes.isEmpty ? 0 : bytes.first,
encode: (n) => [n],
);
int battery = await level.read(); // Future<int>
level.notifications.listen(print); // Stream<int>
await level.write(42); // encodes, then writes
encodeis optional — omit it for a read-only mapping; callingwrite()without one throws aStateError.- Everything else delegates to the underlying characteristic (
uuid,properties,canRead/canWrite/canNotify,descriptors,subscribe(), the passive streams, …), and the raw characteristic is always reachable via.raw. - Mappings chain —
.map(...).map(...)composes the decoders (and encoders, when both are present).
It is not itself a BluetoothCharacteristic (its read() returns Future<T>), so
keep the mapped view where you want typed access and use .raw for the byte-level API.
Descriptors work the same way via descriptor.map(...) → MappedBluetoothDescriptor<T>:
final userDescription = descriptor.map(utf8.decode, encode: utf8.encode);
String text = await userDescription.read();
Accumulating scan results #
Bluebird.scan() emits one advertisement at a time. To collect them into a growing,
de-duplicated device list (keyed by address), use .accumulate():
Bluebird.scan().accumulate().listen((List<ScanResult> results) {
// the full list so far, updated on every advertisement
});
coalesce(defaulttrue): a device splits its data across the advertising and scan-response packets, so any single packet may omit fields an earlier one carried (commonly the name).accumulatemerges each new advertisement onto the stored one, carrying those fields forward so they don't flicker. Passcoalesce: falseto keep each latest packet verbatim. (You can also merge two results yourself withfirst.mergedWith(newer).)removeIfGone: drop a device from the list once it hasn't re-advertised within the given duration — see Scanned device never goes away.
Save Device #
To save a device between app restarts, just store the remoteId to SharedPreferences or a file.
Now you can connect without needing to scan again, like so:
final String remoteId = await File('/remoteId.txt').readAsString();
var device = BluetoothDevice.fromId(remoteId);
await device.connect();
MTU #
On Android, we request an MTU of 517 by default during connection (see: connect function arguments).
On iOS & macOS, the MTU is negotiated automatically, typically 135 to 255.
final subscription = device.mtu.listen((int mtu) {
// iOS: initial value is always 23, but iOS will quickly negotiate a higher value
print("mtu $mtu");
});
// Cleanup: cancel subscription when disconnected
device.cancelWhenDisconnected(subscription);
// You can also manually change the mtu yourself.
if (Platform.isAndroid) await device.requestMtu(517);
Services Changed Characteristic #
Bluebird automatically listens to the Services Changed Characteristic (0x2A05)
In Bluebird, we call it onServicesReset because you must re-discover services.
// - uses the GAP Services Changed characteristic (0x2A05)
// - you must call discoverServices() again
device.onServicesReset.listen(() async {
print("Services Reset");
// a reset invalidates every previously discovered attribute; re-discover and
// re-fetch anything you use, don't reuse old references.
await device.discoverServices();
});
Get Connected Devices #
Get devices currently connected to your app.
for (BluetoothDevice d in Bluebird.connectedDevices) {
print(d);
}
Get System Devices #
Get devices connected to the system by any app.
Note: before you can communicate, you must connect your app to these devices
// `withServices` required on iOS, ignored on android
List<Uuid> withServices = [Uuid("180F")];
List<BluetoothDevice> devs = await Bluebird.systemDevices(withServices);
for (final d in devs) {
await d.connect(); // Must connect *our* app to the device
await d.discoverServices();
}
Create Bond (Android Only) #
Note: calling this is usually not necessary!! The platform will do it automatically.
However, you can force the popup to show sooner.
final bsSubscription = device.bondState.listen((value) {
print("$value prev:{$device.prevBondState}");
});
// cleanup: cancel subscription when disconnected
device.cancelWhenDisconnected(bsSubscription);
// Force the bonding popup to show now (Android Only)
await device.createBond();
// remove bond
await device.removeBond();
Events API #
Bluebird.events is a single broadcast stream of every event, from every device.
Each event is a subtype of the sealed BluebirdEvent class, so you can filter it
by type. Event types include:
OnConnectionStateChangedEventOnMtuChangedEventOnServicesResetEventOnCharacteristicNotifiedEvent/OnCharacteristicReadEventOnAdapterStateChangedEventOnScanAdvertisementEvent/OnScanFailedEventOnNameChangedEvent(iOS only)OnBondStateChangedEvent(Android only)
// listen to *any device* connection state changes
Bluebird.events
.where((e) => e is OnConnectionStateChangedEvent)
.cast<OnConnectionStateChangedEvent>()
.listen((event) {
print('${event.device} ${event.connectionState}');
});
// or use the typed helper, which filters and casts for you
Bluebird.extractEventStream<OnConnectionStateChangedEvent>().listen((event) {
print('${event.device} ${event.connectionState}');
});
Because every event is sealed, a switch over Bluebird.events is checked for
exhaustiveness by the compiler.
Mocking #
To mock Bluebird for development, refer to the Mocking Guide.
Reference #
🌀 = Stream ⚡ = synchronous
Bluebird API #
| Android | iOS | Throws | Description | |
|---|---|---|---|---|
| setPlatformLogLevel | ✅ | ✅ | Configure native/platform log verbosity | |
| setOptions | ✅ | ✅ | Set configurable bluetooth options | |
| isSupported | ✅ | ✅ | Checks whether the device supports Bluetooth | |
| turnOn | ✅ | 🔥 | Turns on the bluetooth adapter | |
| adapterState 🌀 | ✅ | ✅ | Async value + stream of on/off states (await .value for current) |
|
| adapterReady | ✅ | ✅ | 🔥 | Completes once the adapter is on; throws if unavailable/unauthorized |
| scan 🌀 | ✅ | ✅ | 🔥 | Stream of scan advertisements; stops when cancelled |
| isScanning 🌀 | ✅ | ✅ | Value + stream of the current scanning state (.value) |
|
| connectedDevices ⚡ | ✅ | ✅ | List of devices connected to your app | |
| systemDevices | ✅ | ✅ | 🔥 | List of devices connected to the system, even by other apps |
| getPhySupport | ✅ | 🔥 | Get supported bluetooth phy codings |
Bluebird Events API #
Bluebird.events is a single broadcast stream of BluebirdEvents from all
devices. Filter it by type — either with .where((e) => e is T) or the typed
Bluebird.extractEventStream<T>() helper. Event types:
| Event | Android | iOS | Description |
|---|---|---|---|
OnConnectionStateChangedEvent |
✅ | ✅ | Connection state changed |
OnMtuChangedEvent |
✅ | ✅ | MTU changed |
OnServicesResetEvent |
✅ | ✅ | Services changed & must be rediscovered |
OnCharacteristicNotifiedEvent |
✅ | ✅ | A notify/indicate value arrived |
OnCharacteristicReadEvent |
✅ | ✅ | A read() completed |
OnAdapterStateChangedEvent |
✅ | ✅ | Bluetooth adapter turned on/off |
OnScanAdvertisementEvent |
✅ | ✅ | A scan advertisement was received |
OnScanFailedEvent |
✅ | ✅ | A scan failed to start |
OnBondStateChangedEvent |
✅ | Android bond state changed | |
OnNameChangedEvent |
✅ | iOS device name changed |
BluetoothDevice API #
| Android | iOS | Throws | Description | |
|---|---|---|---|---|
| platformName ⚡ | ✅ | ✅ | The platform preferred name of the device | |
| advName ⚡ | ✅ | ✅ | The advertised name of the device found during scanning | |
| connect | ✅ | ✅ | 🔥 | Establishes a connection to the device |
| disconnect | ✅ | ✅ | 🔥 | Cancels an active or pending connection to the device |
| isConnected ⚡ | ✅ | ✅ | Is this device currently connected to your app? | |
| isDisonnected ⚡ | ✅ | ✅ | Is this device currently disconnected from your app? | |
| connectionState 🌀 | ✅ | ✅ | Value + stream of connection changes (.value for current) |
|
| discoverServices | ✅ | ✅ | 🔥 | Discover services |
| services ⚡ | ✅ | ✅ | The current list of discovered services | |
| onServicesReset 🌀 | ✅ | ✅ | The services changed & must be rediscovered | |
| mtu 🌀 | ✅ | ✅ | Value + stream of the mtu (.value for current) |
|
| maxAttrLen ⚡ | ✅ | ✅ | Value + stream of the max writable attribute length | |
| readRssi | ✅ | ✅ | 🔥 | Read RSSI from a connected device |
| requestMtu | ✅ | 🔥 | Request to change the MTU for the device | |
| requestConnectionPriority | ✅ | 🔥 | Request to update a high priority, low latency connection | |
| bondState 🌀 | ✅ | Stream of device bond state. Can be useful on Android | ||
| createBond | ✅ | 🔥 | Force a system pairing dialogue to show, if needed | |
| removeBond | ✅ | 🔥 | Remove Bluetooth Bond of device | |
| setPreferredPhy | ✅ | 🔥 | Set preferred RX and TX phy for connection and phy options | |
| clearGattCache | ✅ | 🔥 | Clear android cache of service discovery results |
BluetoothCharacteristic API #
| Android | iOS | Throws | Description | |
|---|---|---|---|---|
| uuid ⚡ | ✅ | ✅ | The uuid of the characteristic | |
| isValid ⚡ | ✅ | ✅ | False once invalidated by a (re-)discovery or disconnect; ops throw | |
| properties ⚡ | ✅ | ✅ | The characteristic's properties (read/write/notify/...) | |
| canRead ⚡ | ✅ | ✅ | Whether read is supported |
|
| canWrite ⚡ | ✅ | ✅ | Whether write is supported (with or without response) |
|
| canNotify ⚡ | ✅ | ✅ | Whether notifications are supported (notify or indicate) | |
| read | ✅ | ✅ | 🔥 | Retrieves the value of the characteristic |
| write | ✅ | ✅ | 🔥 | Writes the value of the characteristic |
| notifications 🌀 | ✅ | ✅ | 🔥 | Notify/indicate values; enables notify while listened |
| notificationsPassive🌀 | ✅ | ✅ | Notify/indicate values without enabling notify | |
| values 🌀 | ✅ | ✅ | 🔥 | notifications + read() results; enables notify |
| valuesPassive 🌀 | ✅ | ✅ | values without enabling notify |
|
| subscribe | ✅ | ✅ | 🔥 | Enable notify & keep it on until unsubscribe() |
| cccd ⚡ | ✅ | ✅ | The CCCD descriptor, if the characteristic has one |
BluetoothDescriptor API #
| Android | iOS | Throws | Description | |
|---|---|---|---|---|
| uuid ⚡ | ✅ | ✅ | The uuid of the descriptor | |
| isValid ⚡ | ✅ | ✅ | False once invalidated by re-discovery or disconnect | |
| read | ✅ | ✅ | 🔥 | Retrieves the value of the descriptor |
| write | ✅ | ✅ | 🔥 | Writes the value of the descriptor |
Common Problems #
Many common problems are easily solved.
Adapter:
- bluetooth must be turned on
- adapterState is not 'on' but my Bluetooth is on
- adapterState is called multiple times
Scanning:
Connecting:
- Connection fails
- connectionState is called multiple times
- remoteId is different on Android vs iOS
- iOS: "[Error] The connection has timed out unexpectedly."
Reading & Writing:
Subscriptions:
- notifications are never received
- notification data is split up
- notifications arrive with duplicate data
Android Errors:
Flutter Errors:
"bluetooth must be turned on" #
You need to wait for the bluetooth adapter to fully turn on.
await Bluebird.adapterReady();
This returns as soon as the adapter is on (immediately if it already is) and throws
if it is unavailable/unauthorized. You can also observe it yourself via
Bluebird.adapterState.listen(...). See Usage.
adapterState is not 'on' but my Bluetooth is on #
For iOS:
adapterState always starts as unknown. Wait for the service to report a real state:
await Bluebird.adapterState.firstWhere((state) => state != BluetoothAdapterState.unknown);
If adapterState is unavailable, you must add access to Bluetooth Hardware in the app's Xcode settings. See Getting Started.
For Android:
Check that your device supports Bluetooth & has permissions.
adapterState is called multiple times #
You are forgetting to cancel the original Bluebird.adapterState.listen resulting in multiple listeners.
// tip: using ??= makes it easy to only make new listener when currently null
final subscription ??= Bluebird.adapterState.listen((value) {
// ...
});
// also, make sure you cancel the subscription when done!
subscription.cancel()
Scanning does not find my device #
1. you're using an emulator
Use a physical device.
2. try using another ble scanner app
- iOS: nRF Connect
- Android: BLE Scanner
Install a BLE scanner app on your phone. Can it find your device?
3. your device uses bluetooth classic, not BLE.
Headphones, speakers, keyboards, mice, gamepads, & printers all use Bluetooth Classic.
These devices may be found in System Settings, but they cannot be connected to by Bluebird. Bluebird only supports Bluetooth Low Energy.
4. your device stopped advertising.
- you might need to reboot your device
- you might need to put your device in "discovery mode"
- your phone may have already connected automatically
- another app may have already connected to your device
- another phone may have already connected to your device
Try looking through system devices:
// search system devices. i.e. any device connected to by *any* app
List<BluetoothDevice> system = await Bluebird.systemDevices([]);
for (var d in system) {
print('${d.platformName} already connected to! ${d.remoteId}');
if (d.platformName == "myBleDevice") {
await d.connect(); // must connect our app
}
}
5. your scan filters are wrong.
- try removing all scan filters
- for
withServicesto work, your device must actively advertise the serviceUUIDs it supports
6. Android: you're scanning too often
On Android you can only start a scan 5 times per 30 second period. This is a platform restriction.
Scanned device never goes away #
This is expected.
Use .accumulate(removeIfGone: ...) if you want a device to drop out of the list once it stops advertising.
iBeacons Not Showing #
iOS:
iOS does not support iBeacons using CoreBluetooth. You must find a plugin meant for CoreLocation.
Android:
- you need to enable location permissions, see Getting Started
- you must pass
androidUsesFineLocation:truetoBluebird.scan().
Connection fails #
1. Your ble device have low battery
Bluetooth can become erratic when your peripheral device is low on battery.
2. Your ble device may have refused the connection or have a bug
Connection is a two-way process. Your ble device may be misconfigured.
3. You may be on the edge of the Bluetooth range.
The signal is too weak, or there are a lot of devices causing radio interference.
4. Some phones have an issue connecting while scanning.
The Huawei P8 Lite is one of the reported phones to have this issue. Try stopping your scanner before connecting.
5. Try restarting your phone
Bluetooth is a complicated system service, and can enter a bad state.
connectionState is called multiple times #
You are forgetting to cancel the original device.connectionState.listen resulting in multiple listeners.
// tip: using ??= makes it easy to only make new listener when currently null
final subscription ??= device.connectionState.listen((value) {
// ...
});
// also, make sure you cancel the subscription when done!
subscription.cancel()
The remoteId is different on Android versus iOS & macOS #
This is expected. There is no way to avoid it.
For privacy, iOS & macOS use a randomly generated uuid. This uuid will periodically change.
e.g. 6920a902-ba0e-4a13-a35f-6bc91161c517
Android uses the mac address of the bluetooth device. It never changes.
e.g. 05:A4:22:31:F7:ED
iOS: "[Error] The connection has timed out unexpectedly." #
You can google this error. It is a common iOS ble error code.
It means your device stopped working. Bluebird cannot fix it.
List of Bluetooth GATT Errors #
These GATT error codes are part of the BLE Specification.
These are responses from your ble device because you are sending an invalid request.
Bluebird cannot fix these errors. You are doing something wrong & your device is responding with an error.
GATT errors as they appear on iOS:
apple-code: 1 | The handle is invalid.
apple-code: 2 | Reading is not permitted.
apple-code: 3 | Writing is not permitted.
apple-code: 4 | The command is invalid.
apple-code: 6 | The request is not supported.
apple-code: 7 | The offset is invalid.
apple-code: 8 | Authorization is insufficient.
apple-code: 9 | The prepare queue is full.
apple-code: 10 | The attribute could not be found.
apple-code: 11 | The attribute is not long.
apple-code: 12 | The encryption key size is insufficient.
apple-code: 13 | The value's length is invalid.
apple-code: 14 | Unlikely error.
apple-code: 15 | Encryption is insufficient.
apple-code: 16 | The group type is unsupported.
apple-code: 17 | Resources are insufficient.
apple-code: 18 | Unknown ATT error.
GATT errors as they appear on Android:
android-code: 1 | GATT_INVALID_HANDLE
android-code: 2 | GATT_READ_NOT_PERMITTED
android-code: 3 | GATT_WRITE_NOT_PERMITTED
android-code: 4 | GATT_INVALID_PDU
android-code: 5 | GATT_INSUFFICIENT_AUTHENTICATION
android-code: 6 | GATT_REQUEST_NOT_SUPPORTED
android-code: 7 | GATT_INVALID_OFFSET
android-code: 8 | GATT_INSUFFICIENT_AUTHORIZATION
android-code: 9 | GATT_PREPARE_QUEUE_FULL
android-code: 10 | GATT_ATTR_NOT_FOUND
android-code: 11 | GATT_ATTR_NOT_LONG
android-code: 12 | GATT_INSUFFICIENT_KEY_SIZE
android-code: 13 | GATT_INVALID_ATTRIBUTE_LENGTH
android-code: 14 | GATT_UNLIKELY
android-code: 15 | GATT_INSUFFICIENT_ENCRYPTION
android-code: 16 | GATT_UNSUPPORTED_GROUP
android-code: 17 | GATT_INSUFFICIENT_RESOURCES
Descriptions:
1 | Invalid Handle | The attribute handle given was not valid on this server.
2 | Read Not Permitted | The attribute cannot be read.
3 | Write Not Permitted | The attribute cannot be written.
4 | Invalid PDU | The attribute PDU was invalid.
5 | Insufficient Authentication | The attribute requires authentication before it can be read or written.
6 | Request Not Supported | Attribute server does not support the request received from the client.
7 | Invalid Offset | Offset specified was past the end of the attribute.
8 | Insufficient Authorization | The attribute requires an authorization before it can be read or written.
9 | Prepare Queue Full | Too many prepare writes have been queued.
10 | Attribute Not Found | No attribute found within the given attribute handle range.
11 | Attribute Not Long | The attribute cannot be read or written using the Read Blob or Write Blob requests.
12 | Insufficient Key Size | The Encryption Key Size used for encrypting this link is insufficient.
13 | Invalid Attribute Value Length | The attribute value length is invalid for the operation.
14 | Unlikely Error | The request has encountered an unlikely error and cannot be completed.
15 | Insufficient Encryption | The attribute requires encryption before it can be read or written.
16 | Unsupported Group Type | The attribute type is not a supported grouping as defined by a higher layer.
17 | Insufficient Resources | Insufficient Resources to complete the request.
characteristic write fails #
First, check the List of Bluetooth GATT Errors for your error.
1. your bluetooth device turned off, or is out of range
If your device turns off or crashes during a write, it will cause a failure.
2. Your Bluetooth device has bugs
Maybe your device crashed, or is not sending a response due to software bugs.
3. there is radio interference
Bluetooth is wireless and will not always work.
Characteristic read fails #
First, check the List of Bluetooth GATT Errors for your error.
1. your bluetooth device turned off, or is out of range
If your device turns off or crashes during a read, it will cause a failure.
2. Your Bluetooth device has bugs
Maybe your device crashed, or is not sending a response due to software bugs.
3. there is radio interference
Bluetooth is wireless and will not always work.
notifications are never received #
1. you are not listening to the right stream
chr.notifications emits only notify/indicate values. chr.values also includes
the result of chr.read(). Neither emits your own chr.write() calls.
2. your device has nothing to send
With notify/indicate, your device chooses when to send data.
Try interacting with your device to get it to send new data.
3. your device has bugs
Try rebooting your ble device.
Some ble devices have buggy software and stop sending data
notification data is split up #
Verify that the mtu is large enough to hold your message.
device.mtu.value
If it still happens, it is a problem with your peripheral device.
notifications arrive with duplicate data #
You are probably listening to chr.notifications more than once. Because listening
enables notify on the peripheral, each active listener receives every value — hold a
single subscription (or use subscribe() + notificationsPassive).
final subscription = chr.notifications.listen((value) {
// ...
});
// cancel when you are done (this also disables notify on the peripheral)
device.cancelWhenDisconnected(subscription);
ANDROID_SPECIFIC_ERROR #
There is no 100% solution.
Bluebird already has mitigations for this error, but Android will still fail with this code randomly.
The recommended solution is to catch the error, and retry.
android pairing popup appears twice #
This is a bug in android itself.
You can call createBond() yourself just after connecting and this will resolve the issue.
MissingPluginException(No implementation found for method XXXX ...) #
If you just added bluebird to your pubspec.yaml, a hot reload / hot restart is not enough.
You need to fully stop your app and run again so that the native plugins are loaded.
Also try flutter clean.