Implement queue (#11)

* Implement queue

* Enable queue by default, remove queue from web and update readme

* Rename useQueue to queuesRequests

* Rename queuesRequests and improve readme

* Rename _executeMethod and improve readme

* Rename to _isQueueEnabled

---------

Co-authored-by: Foti Dim <fdimanidis@gmail.com>
This commit is contained in:
Rohit Sangwan
2024-02-15 12:27:11 +05:30
committed by GitHub
parent d187c8de83
commit 35d4737f3a
12 changed files with 387 additions and 223 deletions
+2 -2
View File
@@ -11,13 +11,13 @@
"type": "dart"
},
{
"name": "example_auto_connect",
"name": "example_mock",
"cwd": "example",
"request": "launch",
"type": "dart",
"args": [
"--dart-define",
"AUTO_CONNECT=true",
"MOCK=true",
]
}
]
+4
View File
@@ -1,3 +1,7 @@
## 0.9.2
* Add command queue
* Improve error handling on Android and Apple
## 0.9.1
* Improve logging
+25 -1
View File
@@ -14,6 +14,8 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
- [Reading & Writing data](#reading--writing-data)
- [Pairing](#pairing)
- [Bluetooth Availability](#bluetooth-availability)
- [Command Queue](#command-queue)
- [Timeout](#timeout)
### API Support Matrix
@@ -147,6 +149,28 @@ UniversalBle.onAvailabilityChange = (state) {
UniversalBle.enableBluetooth();
```
## Command Queue
By default, all commands will be executed in a queue. Each command will wait for the previous one to finish.
Some platforms (e.g. Android) will fail to send consecutive commands without any delay between them so it is a good idea to leave to queue enabled.
```dart
// Disable queue
UniversalBle.queuesCommands = false;
```
## Timeout
By default, all commands have a timeout of 10 seconds.
```dart
// Change timeout
UniversalBle.timeout = const Duration(seconds: 10);
// Disable timeout
UniversalBle.timeout = null;
```
## Platform-Specific Setup
### Android
@@ -191,7 +215,7 @@ UniversalBle.startScan(
```dart
// Create a class that extends UniversalBlePlatform
class UniversalBleMock extends UniversalBlePlatform {
// Implement all methods
// Implement all commands
}
UniversalBle.setInstance(UniversalBleMock());
+108
View File
@@ -0,0 +1,108 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';
/// Mock implementation of [UniversalBlePlatform] for testing
class MockUniversalBle extends UniversalBlePlatform {
final _mockBleScanResult = BleScanResult(
name: 'MockDevice',
deviceId: 'MockDeviceId',
rssi: 50,
manufacturerData: Uint8List(0),
);
Uint8List _serviceValue = utf8.encode('Result');
final BleService _mockService = BleService('180', [
BleCharacteristic('180A', [
CharacteristicProperty.read,
CharacteristicProperty.write,
CharacteristicProperty.notify,
]),
]);
@override
Future<void> startScan({WebRequestOptionsBuilder? webRequestOptions}) async {
onScanResult?.call(_mockBleScanResult);
}
@override
Future<void> stopScan() async {}
@override
Future<void> connect(String deviceId, {Duration? connectionTimeout}) async {
onConnectionChanged?.call(deviceId, BleConnectionState.connected);
}
@override
Future<void> disconnect(String deviceId) async {
onConnectionChanged?.call(deviceId, BleConnectionState.disconnected);
}
@override
Future<List<BleService>> discoverServices(String deviceId) async {
return [_mockService];
}
@override
Future<bool> enableBluetooth() async {
await Future.delayed(const Duration(milliseconds: 500));
return true;
}
@override
Future<AvailabilityState> getBluetoothAvailabilityState() async {
return AvailabilityState.poweredOn;
}
@override
Future<List<BleScanResult>> getConnectedDevices(
List<String>? withServices) async {
return [];
}
@override
Future<Uint8List> readValue(
String deviceId, String service, String characteristic) async {
await Future.delayed(const Duration(milliseconds: 500));
return _serviceValue;
}
@override
Future<void> writeValue(
String deviceId,
String service,
String characteristic,
Uint8List value,
BleOutputProperty bleOutputProperty) async {
await Future.delayed(const Duration(milliseconds: 500));
_serviceValue = value;
}
@override
Future<int> requestMtu(String deviceId, int expectedMtu) async {
await Future.delayed(const Duration(seconds: 1));
return 512;
}
@override
Future<void> setNotifiable(String deviceId, String service,
String characteristic, BleInputProperty bleInputProperty) async {}
@override
Future<bool> isPaired(String deviceId) async {
await Future.delayed(const Duration(milliseconds: 500));
return true;
}
@override
Future<void> pair(String deviceId) async {
onPairingStateChange?.call(deviceId, true, null);
}
@override
Future<void> unPair(String deviceId) async {
onPairingStateChange?.call(deviceId, false, null);
}
}
+21
View File
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:universal_ble/universal_ble.dart';
import 'package:universal_ble_example/data/capabilities.dart';
import 'package:universal_ble_example/data/mock_universal_ble.dart';
import 'package:universal_ble_example/home/widgets/scanned_devices_placeholder_widget.dart';
import 'package:universal_ble_example/home/widgets/scanned_item_widget.dart';
import 'package:universal_ble_example/data/permission_handler.dart';
@@ -21,6 +22,8 @@ class MyApp extends StatefulWidget {
class _MyAppState extends State<MyApp> {
final _scanResults = <BleScanResult>[];
bool _isScanning = false;
bool _isQueueEnabled = true;
AvailabilityState? bleAvailabilityState;
late WebRequestOptionsBuilder _requestOptions;
final List<String> _services = [
@@ -36,6 +39,15 @@ class _MyAppState extends State<MyApp> {
void initState() {
super.initState();
/// Set mock instance for testing
if (const bool.fromEnvironment('MOCK')) {
UniversalBle.setInstance(MockUniversalBle());
}
/// Setup queue and timeout
UniversalBle.queuesCommands = _isQueueEnabled;
UniversalBle.timeout = const Duration(seconds: 10);
/// Add common services for web
if (kIsWeb) {
_services.addAll(WebRequestOptionsBuilder.defaultServices);
@@ -162,6 +174,15 @@ class _MyAppState extends State<MyApp> {
});
},
),
PlatformButton(
text: _isQueueEnabled ? 'Disable Queue' : 'Enable Queue',
onPressed: () {
setState(() {
_isQueueEnabled = !_isQueueEnabled;
UniversalBle.queuesCommands = _isQueueEnabled;
});
},
),
if (_scanResults.isNotEmpty)
PlatformButton(
text: 'Clear List',
+80
View File
@@ -0,0 +1,80 @@
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 BleCommandQueue {
final Set<int> _activeItems = {};
int _lastProcessId = 0;
bool _isCancelled = false;
final List<_QueuedFuture> _nextCycle = [];
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(_);
}
}
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();
}
}
}
+104 -44
View File
@@ -1,4 +1,4 @@
import 'dart:io';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:universal_ble/src/universal_ble_linux/universal_ble_linux.dart';
@@ -14,10 +14,30 @@ class UniversalBle {
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');
}
}
/// To get Bluetooth state availability
/// To get updates, set [onAvailabilityChange] listener
static Future<AvailabilityState> getBluetoothAvailabilityState() async {
return await _platform.getBluetoothAvailabilityState();
return await _executeCommand(
() => _platform.getBluetoothAvailabilityState(),
timeout: timeout,
);
}
/// To Start scan, get scan results in [onScanResult] listener
@@ -26,13 +46,19 @@ class UniversalBle {
static Future<void> startScan({
WebRequestOptionsBuilder? webRequestOptions,
}) async {
await _platform.startScan(webRequestOptions: webRequestOptions);
return await _executeCommand(
() => _platform.startScan(webRequestOptions: webRequestOptions),
timeout: null,
);
}
/// To Stop scan, set [onScanResult] listener to `null` if you don't need it anymore
/// might throw errors if Bluetooth is not available
static Future<void> stopScan() async {
await _platform.stopScan();
return await _executeCommand(
() => _platform.stopScan(),
timeout: null,
);
}
/// To connect to a device, get connection state in [onConnectionChanged] listener
@@ -43,17 +69,26 @@ class UniversalBle {
String deviceId, {
Duration? connectionTimeout,
}) async {
await _platform.connect(deviceId, connectionTimeout: connectionTimeout);
return await _executeCommand(
() => _platform.connect(deviceId, connectionTimeout: connectionTimeout),
timeout: timeout,
);
}
/// To disconnect from a device, get connection state in [onConnectionChanged] listener
static Future<void> disconnect(String deviceId) async {
await _platform.disconnect(deviceId);
return await _executeCommand(
() => _platform.disconnect(deviceId),
timeout: timeout,
);
}
/// To discover services of a device
static Future<List<BleService>> discoverServices(String deviceId) async {
return await _platform.discoverServices(deviceId);
return await _executeCommand(
() => _platform.discoverServices(deviceId),
timeout: timeout,
);
}
/// To set a characteristic notifiable, set `bleInputProperty` to [BleInputProperty.notification] or [BleInputProperty.indication], get updates in [onValueChanged] listener
@@ -64,22 +99,28 @@ class UniversalBle {
String characteristic,
BleInputProperty bleInputProperty,
) async {
await _platform.setNotifiable(
deviceId,
service,
characteristic,
bleInputProperty,
return await _executeCommand(
() => _platform.setNotifiable(
deviceId,
service,
characteristic,
bleInputProperty,
),
timeout: timeout,
);
}
/// To read a characteristic value
/// on iOS and MacOS, this method will also trigger [onValueChanged] listener
/// on iOS and MacOS, this command will also trigger [onValueChanged] listener
static Future<Uint8List> readValue(
String deviceId,
String service,
String characteristic,
) async {
return await _platform.readValue(deviceId, service, characteristic);
return await _executeCommand(
() => _platform.readValue(deviceId, service, characteristic),
timeout: timeout,
);
}
/// To write a characteristic value
@@ -91,33 +132,50 @@ class UniversalBle {
Uint8List value,
BleOutputProperty bleOutputProperty,
) async {
await _platform.writeValue(
deviceId,
service,
characteristic,
value,
bleOutputProperty,
await _executeCommand(
() => _platform.writeValue(
deviceId,
service,
characteristic,
value,
bleOutputProperty,
),
timeout: timeout,
);
}
/// `requestMtu` not supported on `Linux` and `Web
static Future<int> requestMtu(String deviceId, int expectedMtu) async {
return await _platform.requestMtu(deviceId, expectedMtu);
return await _executeCommand(
() => _platform.requestMtu(deviceId, expectedMtu),
timeout: timeout,
);
}
/// Pair methods are not supported on `iOS`, `MacOS` and `Web`
/// Pair commands are not supported on `iOS`, `MacOS` and `Web`
static Future<bool> isPaired(String deviceId) async {
return await _platform.isPaired(deviceId);
return await _executeCommand(
() => _platform.isPaired(deviceId),
timeout: timeout,
);
}
/// To trigger pair request, might throw errors if device is already paired
/// To trigger pair request
/// might throw errors if device is already paired
static Future<void> pair(String deviceId) async {
await _platform.pair(deviceId);
return await _executeCommand(
() => _platform.pair(deviceId),
timeout: timeout,
);
}
/// To trigger unPair request, might throw errors if device is not paired
/// To trigger unPair request
/// might throw errors if device is not paired
static Future<void> unPair(String deviceId) async {
await _platform.unPair(deviceId);
return await _executeCommand(
() => _platform.unPair(deviceId),
timeout: timeout,
);
}
/// To get connected devices to the system ( connected by any app )
@@ -128,13 +186,19 @@ class UniversalBle {
static Future<List<BleScanResult>> getConnectedDevices({
List<String>? withServices,
}) async {
return await _platform.getConnectedDevices(withServices);
return await _executeCommand(
() => _platform.getConnectedDevices(withServices),
timeout: timeout,
);
}
/// Enabling Bluetooth, might throw errors if Bluetooth is not available
/// Not supported on `Web` and `Apple`
static Future<bool> enableBluetooth() async {
return await _platform.enableBluetooth();
return await _executeCommand(
() => _platform.enableBluetooth(),
timeout: timeout,
);
}
/// To get Bluetooth state availability
@@ -165,21 +229,17 @@ class UniversalBle {
static UniversalBlePlatform _defaultPlatform() {
if (kIsWeb) return UniversalBleWeb.instance;
if (Platform.isLinux) return UniversalBleLinux.instance;
if (defaultTargetPlatform == TargetPlatform.linux) {
return UniversalBleLinux.instance;
}
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());
}
}
// Callback types
typedef OnConnectionChanged = void Function(
String deviceId, BleConnectionState state);
typedef OnValueChanged = void Function(
String deviceId, String characteristicId, Uint8List value);
typedef OnScanResult = void Function(BleScanResult scanResult);
typedef OnAvailabilityChange = void Function(AvailabilityState state);
typedef OnPairingStateChange = void Function(
String deviceId, bool isPaired, String? error);
@@ -56,3 +56,17 @@ abstract class UniversalBlePlatform {
log(message, name: 'UniversalBle');
}
}
// Callback types
typedef OnConnectionChanged = void Function(
String deviceId, BleConnectionState state);
typedef OnValueChanged = void Function(
String deviceId, String characteristicId, Uint8List value);
typedef OnScanResult = void Function(BleScanResult scanResult);
typedef OnAvailabilityChange = void Function(AvailabilityState state);
typedef OnPairingStateChange = void Function(
String deviceId, bool isPaired, String? error);
-139
View File
@@ -1,139 +0,0 @@
import 'dart:async';
/// Original Author: Ryan Knell (https://github.com/rknell/dart_queue/commits/master/lib/src/dart_queue_base.dart)
/// Queue to execute Futures in order.
/// It awaits each future before executing the next one.
class Queue {
final List<_QueuedFuture> _nextCycle = [];
final Duration? delay;
final Duration? timeout;
int parallel;
int _lastProcessId = 0;
bool _isCancelled = false;
bool get isCancelled => _isCancelled;
StreamController<int>? _remainingItemsController;
Stream<int>? get remainingItems {
_remainingItemsController ??= StreamController<int>();
return _remainingItemsController?.stream.asBroadcastStream();
}
final List<Completer<void>> _completeListeners = [];
Future get onComplete {
final completer = Completer();
_completeListeners.add(completer);
return completer.future;
}
Set<int> activeItems = {};
void cancel() {
for (final item in _nextCycle) {
item.completer.completeError(QueueCancelledException());
}
_nextCycle.removeWhere((item) => item.completer.isCompleted);
_isCancelled = true;
}
void dispose() {
_remainingItemsController?.close();
cancel();
}
Queue({this.delay, this.parallel = 1, this.timeout});
Future<T> add<T>(Future<T> Function() closure) {
if (isCancelled) throw QueueCancelledException();
final completer = Completer<T>();
_nextCycle.add(_QueuedFuture<T>(closure, completer, timeout));
_updateRemainingItems();
unawaited(_process());
return completer.future;
}
Future<void> _process() async {
if (activeItems.length < parallel) {
_queueUpNext();
}
}
void _updateRemainingItems() {
final remainingItemsController = _remainingItemsController;
if (remainingItemsController != null &&
remainingItemsController.isClosed == false) {
remainingItemsController.sink.add(_nextCycle.length + activeItems.length);
}
}
void _queueUpNext() {
if (_nextCycle.isNotEmpty &&
!isCancelled &&
activeItems.length <= parallel) {
final processId = _lastProcessId;
activeItems.add(processId);
final item = _nextCycle.first;
_lastProcessId++;
_nextCycle.remove(item);
item.onComplete = () async {
activeItems.remove(processId);
var completionDelay = delay;
if (completionDelay != null) {
await Future.delayed(completionDelay);
}
_updateRemainingItems();
_queueUpNext();
};
unawaited(item.execute());
} else if (activeItems.isEmpty && _nextCycle.isEmpty) {
for (final completer in _completeListeners) {
if (completer.isCompleted != true) {
completer.complete();
}
}
_completeListeners.clear();
}
}
}
class QueueCancelledException implements Exception {}
class _QueuedFuture<T> {
final Completer completer;
final Future<T> Function() closure;
final Duration? timeout;
Function? onComplete;
_QueuedFuture(this.closure, this.completer, this.timeout, {this.onComplete});
bool _timedOut = false;
Future<void> execute() async {
try {
T result;
Timer? timeoutTimer;
var executionTimeout = timeout;
if (executionTimeout != null) {
timeoutTimer = Timer(executionTimeout, () {
_timedOut = true;
if (onComplete != null) {
onComplete?.call();
}
});
}
result = await closure();
if (result != null) {
completer.complete(result);
} else {
completer.complete(null);
}
timeoutTimer?.cancel();
await Future.microtask(() {});
} catch (e) {
completer.completeError(e);
} finally {
if (onComplete != null && !_timedOut) onComplete?.call();
}
}
}
@@ -5,7 +5,6 @@ import 'dart:typed_data';
import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart';
import 'package:universal_ble/src/models/model_exports.dart';
import 'package:universal_ble/src/universal_ble_platform_interface.dart';
import 'package:universal_ble/src/universal_ble_web/queue.dart';
class UniversalBleWeb extends UniversalBlePlatform {
static UniversalBleWeb? _instance;
@@ -15,7 +14,6 @@ class UniversalBleWeb extends UniversalBlePlatform {
_setupListeners();
}
late final _queue = Queue();
final Map<String, BluetoothDevice> _bluetoothDeviceList = {};
final Map<String, StreamSubscription> _deviceAdvertisementStreamList = {};
final Map<String, StreamSubscription> _connectedDeviceStreamList = {};
@@ -222,27 +220,24 @@ class UniversalBleWeb extends UniversalBlePlatform {
Uint8List value,
BleOutputProperty bleOutputProperty,
) async {
await _queue.add(() async {
final bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
characteristicId: characteristic,
final bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
characteristicId: characteristic,
);
if (bleCharacteristic == null) {
throw Exception(
'Characteristic $characteristic for service $service not found',
);
}
if (bleCharacteristic == null) {
throw Exception(
'Characteristic $characteristic for service $service not found',
);
}
if (bleOutputProperty == BleOutputProperty.withResponse) {
await bleCharacteristic
.writeValueWithResponse(Uint8List.fromList(value));
} else {
await bleCharacteristic
.writeValueWithoutResponse(Uint8List.fromList(value));
}
});
if (bleOutputProperty == BleOutputProperty.withResponse) {
await bleCharacteristic.writeValueWithResponse(Uint8List.fromList(value));
} else {
await bleCharacteristic
.writeValueWithoutResponse(Uint8List.fromList(value));
}
}
@override
@@ -251,19 +246,17 @@ class UniversalBleWeb extends UniversalBlePlatform {
String service,
String characteristic,
) async {
return _queue.add<Uint8List>(() async {
var bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
characteristicId: characteristic,
);
if (bleCharacteristic == null) {
throw Exception(
'Characteristic $characteristic for service $service not found');
}
var data = await bleCharacteristic.readValue();
return data.buffer.asUint8List();
});
var bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
characteristicId: characteristic,
);
if (bleCharacteristic == null) {
throw Exception(
'Characteristic $characteristic for service $service not found');
}
var data = await bleCharacteristic.readValue();
return data.buffer.asUint8List();
}
/// `Unimplemented`
+1 -2
View File
@@ -1,7 +1,6 @@
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.1
version: 0.9.2
homepage: https://navideck.com
repository: https://github.com/Navideck/universal_ble
issue_tracker: https://github.com/Navideck/universal_ble/issues