Add queue per device (#48)

* Add queue per device

* Improve API and documentation

* Update lib/src/universal_ble.dart

* Update lib/src/ble_command_queue.dart

---------

Co-authored-by: Foti Dim <fdimanidis@gmail.com>
Co-authored-by: Foti Dim <foti@navideck.com>
This commit is contained in:
Rohit Sangwan
2024-06-07 20:55:46 +05:30
committed by GitHub
parent 5847bbffc5
commit 2fe21962d2
11 changed files with 262 additions and 166 deletions
+4
View File
@@ -1,3 +1,7 @@
## 0.9.12
* Add .perDevice queue
* Improve code level documentation
## 0.9.11
* Add device name prefix filtering
+25 -5
View File
@@ -137,9 +137,9 @@ UniversalBle.connect(deviceId);
// Disconnect from a device
UniversalBle.disconnect(deviceId);
// Get notified for connection state changes
// Get connection state updates
UniversalBle.onConnectionChanged = (String deviceId, BleConnectionState state) {
print('OnConnectionChanged $deviceId, $state');
debugPrint('OnConnectionChanged $deviceId, $state');
}
```
@@ -168,7 +168,7 @@ UniversalBle.setNotifiable(deviceId, serviceId, characteristicId, BleInputProper
// Get characteristic updates in `onValueChanged`
UniversalBle.onValueChanged = (String deviceId, String characteristicId, Uint8List value) {
print('onValueChanged $deviceId, $characteristicId, ${hex.encode(value)}');
debugPrint('onValueChanged $deviceId, $characteristicId, ${hex.encode(value)}');
}
// Unsubscribe from a characteristic
@@ -210,11 +210,31 @@ UniversalBle.enableBluetooth();
## Command Queue
By default, all commands are executed in a queue, with each command waiting for the previous one to finish. This is because some platforms (e.g. Android) may fail to send consecutive commands without a delay between them. Therefore, it is a good idea to leave the queue enabled.
By default, all commands are executed in a global queue (`QueueType.global`), with each command waiting for the previous one to finish.
If you want to parallelize commands between multiple devices, you can set:
```dart
// Create a separate queue for each device.
UniversalBle.queueType = QueueType.perDevice;
```
You can also disable the queue completely and parallelize all commands, even for the same device, by using:
```dart
// Disable queue
UniversalBle.queuesCommands = false;
UniversalBle.queueType = QueueType.none;
```
Keep in mind that some platforms (e.g. Android) may not handle well devices that fail to process consecutive commands without a minimum interval. Therefore, it is not advised to set `queueType` to `none`.
You can get queue updates by setting:
```dart
// Get queue state updates
UniversalBle.onQueueUpdate = (String id, int remainingItems) {
debugPrint("Queue: $id Remaining: $remainingItems");
};
```
## Timeout
+13 -5
View File
@@ -22,7 +22,7 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> {
final _scanResults = <BleScanResult>[];
bool _isScanning = false;
bool _isQueueEnabled = true;
QueueType _queueType = QueueType.global;
AvailabilityState? bleAvailabilityState;
final List<String> _services = [
@@ -45,7 +45,7 @@ class _MyAppState extends State<MyApp> {
}
/// Setup queue and timeout
UniversalBle.queuesCommands = _isQueueEnabled;
UniversalBle.queueType = _queueType;
UniversalBle.timeout = const Duration(seconds: 10);
UniversalBle.onAvailabilityChange = (state) {
@@ -68,6 +68,10 @@ class _MyAppState extends State<MyApp> {
}
setState(() {});
};
// UniversalBle.onQueueUpdate = (String id, int remainingItems) {
// debugPrint("Queue: $id RemainingItems: $remainingItems");
// };
}
Future<void> startScan() async {
@@ -184,11 +188,15 @@ class _MyAppState extends State<MyApp> {
},
),
PlatformButton(
text: _isQueueEnabled ? 'Disable Queue' : 'Enable Queue',
text: 'Queue: ${_queueType.name.toUpperCase()}',
onPressed: () {
setState(() {
_isQueueEnabled = !_isQueueEnabled;
UniversalBle.queuesCommands = _isQueueEnabled;
_queueType = switch (_queueType) {
QueueType.global => QueueType.perDevice,
QueueType.perDevice => QueueType.none,
QueueType.none => QueueType.global,
};
UniversalBle.queueType = _queueType;
});
},
),
+38 -68
View File
@@ -1,80 +1,50 @@
import 'dart:async';
import 'package:universal_ble/src/queue.dart';
import 'package:universal_ble/universal_ble.dart';
/// Original Author: Ryan Knell (https://github.com/rknell/dart_queue)
/// Queue to execute Futures in order.
/// It awaits each future before executing the next one.
/// Execute commands in queue and manage queue per device
class BleCommandQueue {
final Set<int> _activeItems = {};
int _lastProcessId = 0;
bool _isCancelled = false;
final List<_QueuedFuture> _nextCycle = [];
QueueType queueType = QueueType.global;
Duration? timeout = const Duration(seconds: 10);
OnQueueUpdate? onQueueUpdate;
final Queue _globalQueue = Queue();
final Map<String, Queue> _queueMap = {};
Future<T> add<T>(Future<T> Function() closure, {Duration? timeout}) {
if (_isCancelled) throw Exception('Queue Cancelled');
final completer = Completer<T>();
_nextCycle.add(_QueuedFuture<T>(closure, completer, timeout));
_updateRemainingItems();
if (_activeItems.isEmpty) _queueUpNext();
return completer.future;
BleCommandQueue() {
_globalQueue.onRemainingItemsUpdate = (int items) {
onQueueUpdate?.call(QueueType.global.name, items);
};
}
void dispose() {
for (final item in _nextCycle) {
item.completer.completeError(Exception('Queue Cancelled'));
Future<T> executeCommand<T>(
Future<T> Function() command, {
bool withTimeout = true,
String? deviceId,
}) {
Duration? duration = withTimeout ? timeout : null;
switch (queueType) {
case QueueType.none:
return duration != null ? command().timeout(duration) : command();
case QueueType.global:
return _globalQueue.add(command, timeout: duration);
case QueueType.perDevice:
// If deviceId not available, use global queue
if (deviceId != null) {
return _getQueue(deviceId).add(command, timeout: duration);
} else {
return _globalQueue.add(command, timeout: duration);
}
}
_nextCycle.removeWhere((item) => item.completer.isCompleted);
_isCancelled = true;
}
void _queueUpNext() {
if (_nextCycle.isNotEmpty && !_isCancelled && _activeItems.length <= 1) {
final processId = _lastProcessId;
_activeItems.add(processId);
final item = _nextCycle.first;
_lastProcessId++;
_nextCycle.remove(item);
item.onComplete = () async {
_activeItems.remove(processId);
_updateRemainingItems();
_queueUpNext();
Queue _getQueue(String deviceId) {
Queue? queue = _queueMap[deviceId];
if (queue == null) {
queue = Queue();
queue.onRemainingItemsUpdate = (int items) {
onQueueUpdate?.call(deviceId, items);
};
unawaited(item.execute());
}
}
void _updateRemainingItems() {
// int remainingQueueItems = _nextCycle.length + _activeItems.length;
// onRemainingItemsUpdate?.call(_);
}
}
class _QueuedFuture<T> {
final Completer completer;
final Future<T> Function() closure;
Function? onComplete;
final Duration? timeout;
_QueuedFuture(this.closure, this.completer, this.timeout, {this.onComplete});
Future<void> execute() async {
try {
T result;
if (timeout != null) {
result = await closure().timeout(timeout!);
} else {
result = await closure();
}
if (result != null) {
completer.complete(result);
} else {
completer.complete(null);
}
await Future.microtask(() {});
} catch (e, stack) {
completer.completeError(e, stack);
} finally {
if (onComplete != null) onComplete?.call();
_queueMap[deviceId] = queue;
}
return queue;
}
}
+3
View File
@@ -1,3 +1,4 @@
export 'package:universal_ble/src/models/queue_type.dart';
export 'package:universal_ble/src/models/uuid.dart';
export 'package:universal_ble/src/models/scan_filter.dart';
export 'package:universal_ble/src/models/ble_property.dart';
@@ -5,3 +6,5 @@ export 'package:universal_ble/src/models/ble_service.dart';
export 'package:universal_ble/src/models/availability_state.dart';
export 'package:universal_ble/src/models/ble_connection_state.dart';
export 'package:universal_ble/src/models/ble_scan_result.dart';
+5
View File
@@ -0,0 +1,5 @@
enum QueueType {
none,
perDevice,
global,
}
+81
View File
@@ -0,0 +1,81 @@
import 'dart:async';
/// Original Author: Ryan Knell (https://github.com/rknell/dart_queue)
/// Queue to execute Futures in order.
/// It awaits each future before executing the next one.
class Queue {
final Set<int> _activeItems = {};
int _lastProcessId = 0;
bool _isCancelled = false;
final List<_QueuedFuture> _nextCycle = [];
Function(int)? onRemainingItemsUpdate;
Future<T> add<T>(Future<T> Function() closure, {Duration? timeout}) {
if (_isCancelled) throw Exception('Queue Cancelled');
final completer = Completer<T>();
_nextCycle.add(_QueuedFuture<T>(closure, completer, timeout));
_updateRemainingItems();
if (_activeItems.isEmpty) _queueUpNext();
return completer.future;
}
void dispose() {
for (final item in _nextCycle) {
item.completer.completeError(Exception('Queue Cancelled'));
}
_nextCycle.removeWhere((item) => item.completer.isCompleted);
_isCancelled = true;
}
void _queueUpNext() {
if (_nextCycle.isNotEmpty && !_isCancelled && _activeItems.length <= 1) {
final processId = _lastProcessId;
_activeItems.add(processId);
final item = _nextCycle.first;
_lastProcessId++;
_nextCycle.remove(item);
item.onComplete = () async {
_activeItems.remove(processId);
_updateRemainingItems();
_queueUpNext();
};
unawaited(item.execute());
}
}
void _updateRemainingItems() {
int remainingQueueItems = _nextCycle.length + _activeItems.length;
onRemainingItemsUpdate?.call(remainingQueueItems);
}
}
class _QueuedFuture<T> {
final Completer completer;
final Future<T> Function() closure;
Function? onComplete;
final Duration? timeout;
_QueuedFuture(this.closure, this.completer, this.timeout, {this.onComplete});
Future<void> execute() async {
try {
T result;
if (timeout != null) {
result = await closure().timeout(timeout!);
} else {
result = await closure();
}
if (result != null) {
completer.complete(result);
} else {
completer.complete(null);
}
await Future.microtask(() {});
} catch (e, stack) {
completer.completeError(e, stack);
} finally {
if (onComplete != null) onComplete?.call();
}
}
}
+90 -86
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:universal_ble/src/ble_command_queue.dart';
import 'package:universal_ble/src/universal_ble_linux/universal_ble_linux.dart';
import 'package:universal_ble/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart';
import 'package:universal_ble/src/universal_ble_web/universal_ble_web.dart';
@@ -9,89 +10,95 @@ import 'package:universal_ble/universal_ble.dart';
class UniversalBle {
/// Get platform specific implementation
static UniversalBlePlatform _platform = _defaultPlatform();
static final BleCommandQueue _bleCommandQueue = BleCommandQueue();
/// Set custom platform specific implementation (e.g. for testing)
static void setInstance(UniversalBlePlatform instance) =>
_platform = instance;
/// Set global timeout for all commands
static Duration? timeout = const Duration(seconds: 10);
static BleCommandQueue? _queue = BleCommandQueue();
/// Setup global queue for all commands, by default queue is enabled
static set queuesCommands(bool value) {
if (value) {
_queue ??= BleCommandQueue();
UniversalBlePlatform.logInfo('Queue enabled');
} else {
_queue?.dispose();
_queue = null;
UniversalBlePlatform.logInfo('Queue disabled');
}
/// Set global timeout for all commands.
/// Default timeout is 10 seconds
static set timeout(Duration? duration) {
_bleCommandQueue.timeout = duration;
}
/// To get Bluetooth state availability
/// To get updates, set [onAvailabilityChange] listener
/// Set how commands will be executed. By default, all commands are executed in a global queue (`QueueType.global`),
/// with each command waiting for the previous one to finish.
///
/// [QueueType.global] will execute commands of all devices in a single queue
/// [QueueType.perDevice] will execute command of each device in separate queues
/// [QueueType.none] will execute all commands in parallel
static set queueType(QueueType queueType) {
_bleCommandQueue.queueType = queueType;
UniversalBlePlatform.logInfo('Queue ${queueType.name}');
}
/// Get Bluetooth availability state
/// To be notified of updates, set [onAvailabilityChange] listener
static Future<AvailabilityState> getBluetoothAvailabilityState() async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.getBluetoothAvailabilityState(),
timeout: timeout,
);
}
/// To Start scan, get scan results in [onScanResult] listener
/// might throw errors if Bluetooth is not available
/// `webRequestOptions` supported on Web only
/// Start scan.
/// Scan results will arrive in [onScanResult] listener
/// It might throw errors if Bluetooth is not available
/// `webRequestOptions` is supported on Web only
static Future<void> startScan({
ScanFilter? scanFilter,
}) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.startScan(scanFilter: scanFilter),
timeout: null,
withTimeout: false,
);
}
/// To Stop scan, set [onScanResult] listener to `null` if you don't need it anymore
/// might throw errors if Bluetooth is not available
/// Stop scan.
/// Set [onScanResult] listener to `null` if you don't need it anymore
/// It might throw errors if Bluetooth is not available
static Future<void> stopScan() async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.stopScan(),
timeout: null,
withTimeout: false,
);
}
/// To connect to a device, get connection state in [onConnectionChanged] listener
/// preferred to stop scan before connecting
/// might throw errors if device is not connectable
/// `connectionTimeout` supported on Web only
/// Connect to a device.
/// Get notified of connection state changes in [onConnectionChanged] listener
/// It is advised to stop scanning before connecting
/// It might throw errors if device is not connectable
/// `connectionTimeout` is supported on Web only
static Future<void> connect(
String deviceId, {
Duration? connectionTimeout,
}) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.connect(deviceId, connectionTimeout: connectionTimeout),
timeout: timeout,
deviceId: deviceId,
);
}
/// To disconnect from a device, get connection state in [onConnectionChanged] listener
/// Disconnect from a device.
/// Get notified of connection state changes in [onConnectionChanged] listener
static Future<void> disconnect(String deviceId) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.disconnect(deviceId),
timeout: timeout,
deviceId: deviceId,
);
}
/// To discover services of a device
/// Discover services of a device
static Future<List<BleService>> discoverServices(String deviceId) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.discoverServices(deviceId),
timeout: timeout,
deviceId: deviceId,
);
}
/// To set a characteristic notifiable, set `bleInputProperty` to [BleInputProperty.notification] or [BleInputProperty.indication], get updates in [onValueChanged] listener
/// Set a characteristic notifiable.
/// Set `bleInputProperty` to [BleInputProperty.notification] or [BleInputProperty.indication]
/// Updates will arrive in [onValueChanged] listener
/// To stop listening to a characteristic, set `bleInputProperty` to [BleInputProperty.disabled]
static Future<void> setNotifiable(
String deviceId,
@@ -99,31 +106,31 @@ class UniversalBle {
String characteristic,
BleInputProperty bleInputProperty,
) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.setNotifiable(
deviceId,
service,
characteristic,
bleInputProperty,
),
timeout: timeout,
deviceId: deviceId,
);
}
/// To read a characteristic value
/// on iOS and MacOS, this command will also trigger [onValueChanged] listener
/// Read a characteristic value
/// On iOS and MacOS this command will also trigger [onValueChanged] listener
static Future<Uint8List> readValue(
String deviceId,
String service,
String characteristic,
) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.readValue(deviceId, service, characteristic),
timeout: timeout,
deviceId: deviceId,
);
}
/// To write a characteristic value
/// Write a characteristic value
/// To write a characteristic value with response, set `bleOutputProperty` to [BleOutputProperty.withResponse]
static Future<void> writeValue(
String deviceId,
@@ -132,7 +139,7 @@ class UniversalBle {
Uint8List value,
BleOutputProperty bleOutputProperty,
) async {
await _executeCommand(
await _bleCommandQueue.executeCommand(
() => _platform.writeValue(
deviceId,
service,
@@ -140,68 +147,69 @@ class UniversalBle {
value,
bleOutputProperty,
),
timeout: timeout,
deviceId: deviceId,
);
}
/// `requestMtu` not supported on `Linux` and `Web
/// Request MTU value
/// `requestMtu` is not supported on `Linux` and `Web
static Future<int> requestMtu(String deviceId, int expectedMtu) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.requestMtu(deviceId, expectedMtu),
timeout: timeout,
deviceId: deviceId,
);
}
/// Pair commands are not supported on `iOS`, `MacOS` and `Web`
/// Check if a device is paired
/// Pair commands are not supported on `Apple` and `Web`
static Future<bool> isPaired(String deviceId) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.isPaired(deviceId),
timeout: timeout,
deviceId: deviceId,
);
}
/// To trigger pair request
/// might throw errors if device is already paired
/// Trigger pair request
/// It might throw an error if device is already paired
static Future<void> pair(String deviceId) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.pair(deviceId),
timeout: timeout,
deviceId: deviceId,
);
}
/// To trigger unPair request
/// might throw errors if device is not paired
/// Unpair a device
/// It might throw an error if device is not paired
static Future<void> unPair(String deviceId) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.unPair(deviceId),
timeout: timeout,
deviceId: deviceId,
);
}
/// To get connected devices to the system (connected by any app)
/// use [withServices] to filter devices by services
/// on `iOS`, `MacOS` [withServices] is required to get connected devices, else [1800] service will be used as default filter
/// on `Android`, `Linux` and `Windows`, if [withServices] is used, then internally all services will be discovered for each device first (either by connecting or by using cached services)
/// Get connected devices to the system (connected by any app)
/// Use [withServices] to filter devices by services
/// On `Apple`, [withServices] is required to get connected devices, else [1800] service will be used as default filter
/// On `Android`, `Linux` and `Windows`, if [withServices] is used, then internally all services will be discovered for each device first (either by connecting or by using cached services)
/// Not supported on `Web`
static Future<List<BleScanResult>> getConnectedDevices({
List<String>? withServices,
}) async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.getConnectedDevices(withServices),
timeout: timeout,
);
}
/// Enabling Bluetooth, might throw errors if Bluetooth is not available
/// Enable Bluetooth
/// It might throw errors if Bluetooth is not available
/// Not supported on `Web` and `Apple`
static Future<bool> enableBluetooth() async {
return await _executeCommand(
return await _bleCommandQueue.executeCommand(
() => _platform.enableBluetooth(),
timeout: timeout,
);
}
/// To get Bluetooth state availability
/// Get Bluetooth state availability
static set onAvailabilityChange(OnAvailabilityChange? onAvailabilityChange) {
_platform.onAvailabilityChange = onAvailabilityChange;
if (onAvailabilityChange != null) {
@@ -211,19 +219,23 @@ class UniversalBle {
}
}
/// To get scan results
/// Get updates of remaining items of a queue
static set onQueueUpdate(OnQueueUpdate? onQueueUpdate) =>
_bleCommandQueue.onQueueUpdate = onQueueUpdate;
/// Get scan results
static set onScanResult(OnScanResult? onScanResult) =>
_platform.onScanResult = onScanResult;
/// To get connection state changes
/// Get connection state changes
static set onConnectionChanged(OnConnectionChanged? onConnectionChanged) =>
_platform.onConnectionChanged = onConnectionChanged;
/// To get characteristic value updates, set `bleInputProperty` in [setNotifiable] to [BleInputProperty.notification] or [BleInputProperty.indication]
/// Get characteristic value updates, set `bleInputProperty` in [setNotifiable] to [BleInputProperty.notification] or [BleInputProperty.indication]
static set onValueChanged(OnValueChanged? onValueChanged) =>
_platform.onValueChanged = onValueChanged;
/// To get pair state changes,
/// Get pair state changes,
static set onPairingStateChange(OnPairingStateChange pairingStateChange) =>
_platform.onPairingStateChange = pairingStateChange;
@@ -234,12 +246,4 @@ class UniversalBle {
}
return UniversalBlePigeonChannel.instance;
}
static Future<T> _executeCommand<T>(
Future<T> Function() command, {
required Duration? timeout,
}) {
return _queue?.add(command, timeout: timeout) ??
(timeout != null ? command().timeout(timeout) : command());
}
}
@@ -84,3 +84,5 @@ typedef OnAvailabilityChange = void Function(AvailabilityState state);
typedef OnPairingStateChange = void Function(
String deviceId, bool isPaired, String? error);
typedef OnQueueUpdate = void Function(String id, int remainingQueueItems);
-1
View File
@@ -1,6 +1,5 @@
library universal_ble;
export 'package:universal_ble/src/ble_command_queue.dart';
export 'package:universal_ble/src/universal_ble_platform_interface.dart';
export 'package:universal_ble/src/universal_ble.dart';
export 'package:universal_ble/src/models/model_exports.dart';
+1 -1
View File
@@ -1,6 +1,6 @@
name: universal_ble
description: A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE) plugin for Flutter
version: 0.9.11
version: 0.9.12
homepage: https://navideck.com
repository: https://github.com/Navideck/universal_ble
issue_tracker: https://github.com/Navideck/universal_ble/issues