Universal BLE
repository·main·Indexed 41 days ago
https://github.com/navideck/universal_bleA cross-platform Flutter plugin providing Bluetooth Low Energy (BLE) capabilities for Android, iOS, macOS, Windows, Linux, and Web. It supports both Client Mode (via UniversalBle) for scanning and interacting with devices, and Peripheral Mode (via UniversalBlePeripheral) for advertising services and responding to connections.
What's inside Universal BLE
- Universal BLE is a cross-platform Bluetooth Low Energy (BLE) plugin for Flutter. It supports a wide range of platforms including Android, iOS, macOS, Windows, Linux, and Web. The plugin is designed to handle both Client Mode (acting as a central device) and Peripheral Mode (acting as a peripheral device).
Compare Client Mode and Peripheral Mode support
mainUniversal BLE provides two distinct API surfaces depending on your use case:
Client Mode (
UniversalBle)Used when your app needs to scan for, connect to, and interact with external BLE devices.
- Core capabilities: Scanning, connecting, discovering services, reading/writing data, and subscriptions.
- Platform availability: Most features are widely supported across all platforms, though some specific features like
autoConnectorenable/disable Bluetoothmay be restricted on certain platforms (e.g., iOS/macOS/Web).
Peripheral Mode (
UniversalBlePeripheral)Used when your app needs to act as a BLE peripheral that advertises services and responds to connections from other devices.
- Core capabilities: Advertising, managing services/characteristics, and updating characteristic values.
- Platform availability: Highly supported on Android, iOS, macOS, and Windows. Linux support is currently in progress (indicated by
🚧). Web is not supported for Peripheral Mode.
Configure and manage Command Queues
mainTo prevent overlapping operations that might crash the BLE stack (especially on Android),
UniversalBleuses a command queue system.Queue Types
QueueType.auto(Default): Android uses per-device queues; other platforms run commands in parallel.QueueType.global: All commands across all devices run in a single serial queue.QueueType.perDevice: Each device has its own serial queue.QueueType.none: Disables queuing entirely (not recommended for Android).
Custom Queues
You can group specific commands into a custom queue by providing a
queueIdstring. Commands with the samequeueIdwill be serialized relative to each other, even if the globalqueueTypeis set to parallel.Queue Management
- Monitor Progress: Set
UniversalBle.onQueueUpdateto receive updates on remaining items in a queue. - Clear Queues: Use
UniversalBle.clearQueue(id)whereidcan be adeviceId, a customqueueId, orBleCommand.globalQueueId. CallingclearQueue()with no arguments clears all queues.
// Set global strategy UniversalBle.queueType = QueueType.perDevice; // Use a custom queue for specific operations UniversalBle.write(deviceId, service, char, value, queueId: 'sync_group'); // Monitor queue progress UniversalBle.onQueueUpdate = (String id, int remainingItems) { print("Queue $id has $remainingItems items left"); }; // Clear a specific queue UniversalBle.clearQueue('sync_group');Check Bluetooth availability
mainEnsure Bluetooth is powered on before initiating a scan. You can check the current state with
getBluetoothAvailabilityState()or listen to changes viaavailabilityStreamor theonAvailabilityChangehandler.AvailabilityState state = await UniversalBle.getBluetoothAvailabilityState(); // Start scan only if Bluetooth is powered on if (state == AvailabilityState.poweredOn) { UniversalBle.startScan(); } // Listen to bluetooth availability changes using stream UniversalBle.availabilityStream.listen((state) { if (state == AvailabilityState.poweredOn) { UniversalBle.startScan(); } }); // Or set a handler UniversalBle.onAvailabilityChange = (state) {};Run HIL tests on Web (Chrome)
mainThe HIL suite includes an interactive Web Bluetooth runner located in
lib/main.dart.Requirements:
- A secure context (HTTPS or localhost).
- A real user gesture.
- Browser device permission.
Steps:
- Run the application:
flutter run -d chrome. - In the browser, click Select device and run.
- Select
UniversalBLE-HILfrom the Chrome device chooser.
flutter run -d chromeRun HIL baseline and fault injection tests
mainOnce the environment is set up and the firmware is advertising, you can run the Windows-based test suites.
Running Baseline Tests
To run the standard BLE behavior tests:
flutter test integration_test/baseline_hil_test.dart -d windowsRunning Fault Injection Tests
To run tests that simulate hostile peripheral behavior (timing races, disconnects, etc.):
flutter test integration_test/fault_injection_hil_test.dart -d windowsAdvanced Execution Options
- Skip Scanning: If you know the Windows device ID, use
--dart-define=HIL_DEVICE_ID=...to skip the initial scan. - Extended Reconnects: For longer reconnect testing, use
--dart-define=HIL_RECONNECT_CYCLES=N. - Rerun Single Case: Use
--plain-nameto rerun a specific failing case individually.
# Run baseline tests flutter test integration_test/baseline_hil_test.dart -d windows # Run fault injection tests flutter test integration_test/fault_injection_hil_test.dart -d windows # Run with a specific device ID flutter test integration_test/baseline_hil_test.dart -d windows --dart-define=HIL_DEVICE_ID=AA:BB:CC:DD:EE:FF # Run with extended reconnect cycles flutter test integration_test/baseline_hil_test.dart -d windows --dart-define=HIL_RECONNECT_CYCLES=100- Skip Scanning: If you know the Windows device ID, use
Pair BLE devices
mainPairing allows access to encrypted characteristics. Behavior varies by platform:
Android, Windows, and Linux
Trigger pairing directly using
bleDevice.pair().Apple and Web
Pairing is typically triggered automatically by the OS when attempting to read/write an encrypted characteristic. To manually trigger it,
bleDevice.pair()only works if the device has an encrypted read characteristic.If you need to target a specific encrypted characteristic, provide a
pairingCommand:await bleDevice.pair(pairingCommand: BleCommand(service: "SERVICE", characteristic: "ENCRYPTED_CHARACTERISTIC"));Managing Pairing State
- Check Status: Use
bleDevice.isPaired(). On Apple/Web, you must pass the samepairingCommandused to trigger pairing to get a non-null result. - Monitor Changes: Listen to
bleDevice.pairingStateStreamfor real-time updates. - Unpair: Use
bleDevice.unpair()to remove the pairing.
Tip: To find which characteristic is encrypted, attempt to read/write all discovered characteristics one by one while unpaired; the one that triggers the OS pairing prompt is your target.
// Trigger pairing (Android/Win/Linux) await bleDevice.pair(); // Trigger pairing with specific command (Apple/Web) await bleDevice.pair(pairingCommand: BleCommand(service: "SERVICE", characteristic: "ENCRYPTED_CHARACTERISTIC")); // Check status bool? isPaired = bleDevice.isPaired(); // Listen for state changes bleDevice.pairingStateStream.listen((bool paired) { // Handle change });- Check Status: Use
Configure Android Manifest Permissions
mainTo use Universal BLE on Android, you must declare the necessary permissions in your
AndroidManifest.xml.Standard BLE Permissions: Use these for general Bluetooth connectivity.
Location-based Scanning: If your app uses iBeacons or requires
BLUETOOTH_SCANto determine location, you must use the more permissive location declarations instead of theneverForLocationflag.Peripheral Mode: If your app acts as a BLE peripheral (advertising), you must also include the
BLUETOOTH_ADVERTISEpermission.<!-- Standard BLE Permissions --> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <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_COARSE_LOCATION" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <!-- If using iBeacons or location-based scanning --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <!-- For Peripheral Mode (Advertising) --> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />Pair a BLE device
mainPairing behavior varies by platform:
Android, Windows, and Linux
Trigger pairing directly using:
await UniversalBle.pair(deviceId);Apple and Web
Pairing is typically triggered automatically by the OS when attempting to access an encrypted characteristic.
To manually trigger pairing on these platforms, you must provide a
pairingCommandtargeting an encrypted characteristic:UniversalBle.pair(deviceId, pairingCommand: BleCommand(service: "SERVICE", characteristic: "ENCRYPTED_CHARACTERISTIC"));Unpairing
To remove pairing information, use:
UniversalBle.unpair(deviceId);// Android, Windows, Linux await UniversalBle.pair(deviceId); // Apple and Web (requires specific encrypted characteristic) UniversalBle.pair(deviceId, pairingCommand: BleCommand(service: "SERVICE", characteristic: "ENCRYPTED_CHARACTERISTIC")); // Unpair UniversalBle.unpair(deviceId);Set up Peripheral Mode with UniversalBlePeripheral
mainTo use your app as a BLE peripheral (server), use the
UniversalBlePeripheralclass. Before starting, you must check if the platform supports peripheral mode and if the peripheral state is ready.import 'package:universal_ble/universal_ble.dart'; final caps = await UniversalBlePeripheral.getCapabilities(); if (!caps.supportsPeripheralMode) return; final readiness = await UniversalBlePeripheral.getAvailabilityState(); if (readiness != PeripheralReadinessState.ready) return;Subscribe to BLE characteristic notifications and indications
mainTo receive real-time updates from a BLE characteristic, you must first obtain the
BleCharacteristicinstance viableDevice.getCharacteristic.Listening to Values
Use the
onValueReceivedstream to listen for incomingUint8Listdata whenever the characteristic's value changes.Enabling Subscriptions
- Notifications: Use
characteristic.notifications.subscribe(). This throws an exception if the characteristic does not support notifications. - Indications: Use
characteristic.indications.subscribe(). This throws an exception if the characteristic does not support indications. - Unsubscribe: Use
characteristic.unsubscribe()to stop receiving updates.
Checking Subscription Status
You can check if a characteristic is currently subscribed using several methods:
- On the characteristic:
characteristic.isSubscribedorcharacteristic.notifications.isSubscribed. - Globally:
UniversalBle.isSubscribed(deviceId, characteristicId). - Per device:
UniversalBle.getSubscribedCharacteristics(deviceId)returns a list of all subscribed UUIDs.
// Listen to value changes characteristic.onValueReceived.listen((value) { debugPrint('Received value: ${value.toString()}'); }); // Subscribe to notifications await characteristic.notifications.subscribe(); // Subscribe to indications await characteristic.indications.subscribe(); // Unsubscribe await characteristic.unsubscribe(); // Check status bool isSubscribed = characteristic.isSubscribed;- Notifications: Use
Start and Stop Advertising in Peripheral Mode
mainAdvertising allows peripherals to be discovered. Behavior varies by platform:
- Android: Passing
localNamemay temporarily change the system Bluetooth device name. The plugin restores the original name automatically. - Windows: Use
services: []andmanufacturerDatafor connectionless advertising. CustomlocalNameis not supported. Payload is limited to 27 bytes. - iOS/macOS: Supports
localNameand service UUIDs, but does not support manufacturer data.
Use
advertisingStateStreamto monitor for asynchronous failures.import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:universal_ble/universal_ble.dart'; final isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows; final caps = await UniversalBlePeripheral.getCapabilities(); await UniversalBlePeripheral.startAdvertising( services: [batteryService], localName: isWindows ? null : 'UniversalBlePeripheral', manufacturerData: isWindows || !caps.supportsManufacturerDataInAdvertisement ? null : ManufacturerData( 0x012d, Uint8List.fromList([0x03, 0x00, 0x64, 0x00]), ), platformConfig: PeripheralPlatformConfig( android: PeripheralAndroidOptions( addManufacturerDataInScanResponse: false, ), ), ); // To stop: await UniversalBlePeripheral.stopAdvertising();- Android: Passing