Add error in connection updates (#89)

* Add error in connection updates

* implement windows

* Update changelog and Docs

* minor fix in connect()

* Add default timeout in connect

* Document new pair and connect API

* Fix Connect and Pair apis to throw proper errors and Improve logging

* remove import

* Rename PairingException and update Exception doc

* Update changelog

* Update comments

---------

Co-authored-by: Foti Dim <fdimanidis@gmail.com>
This commit is contained in:
Rohit Sangwan
2024-09-27 02:38:25 +05:30
committed by GitHub
parent f41aceaa0e
commit b77d0aaeb4
26 changed files with 851 additions and 698 deletions
@@ -0,0 +1,9 @@
class BleConnectionUpdate {
final bool isConnected;
final String? error;
BleConnectionUpdate({
required this.isConnected,
this.error,
});
}
+13
View File
@@ -7,6 +7,11 @@ class BleService {
BleService(String uuid, this.characteristics) {
this.uuid = BleUuidParser.string(uuid);
}
@override
String toString() {
return 'BleService{uuid: $uuid, characteristics: $characteristics}';
}
}
class BleCharacteristic {
@@ -16,6 +21,11 @@ class BleCharacteristic {
BleCharacteristic(String uuid, this.properties) {
this.uuid = BleUuidParser.string(uuid);
}
@override
String toString() {
return 'BleCharacteristic{uuid: $uuid, properties: $properties}';
}
}
enum CharacteristicProperty {
@@ -32,4 +42,7 @@ enum CharacteristicProperty {
factory CharacteristicProperty.parse(int index) =>
CharacteristicProperty.values[index];
@override
String toString() => name;
}
+1
View File
@@ -1,3 +1,4 @@
export 'package:universal_ble/src/models/ble_connection_update.dart';
export 'package:universal_ble/src/models/manufacturer_data.dart';
export 'package:universal_ble/src/models/platform_config.dart';
export 'package:universal_ble/src/models/queue_type.dart';
+114 -75
View File
@@ -5,6 +5,7 @@ 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';
import 'package:universal_ble/src/universal_logger.dart';
import 'package:universal_ble/universal_ble.dart';
class UniversalBle {
@@ -31,7 +32,7 @@ class UniversalBle {
/// [QueueType.none] will execute all commands in parallel.
static set queueType(QueueType queueType) {
_bleCommandQueue.queueType = queueType;
UniversalBlePlatform.logInfo('Queue ${queueType.name}');
UniversalLogger.logInfo('Queue ${queueType.name}');
}
/// Get Bluetooth availability state.
@@ -70,28 +71,37 @@ class UniversalBle {
}
/// Connection stream of a device
Stream<bool> connectionStream(String deviceId) =>
static Stream<BleConnectionUpdate> connectionStream(String deviceId) =>
_platform.connectionStream(deviceId);
/// Connect to a device.
/// 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<bool> connect(
/// It throws error if device connection fails.
/// Default connection timeout is 60 sec.
/// Can throw `ConnectionException` or `PlatformException`.
static Future<void> connect(
String deviceId, {
Duration? connectionTimeout,
}) async {
connectionTimeout ??= const Duration(seconds: 60);
StreamSubscription? connectionSubscription;
try {
Completer<bool> completer = Completer();
connectionSubscription =
_platform.connectionStream(deviceId).listen((bool event) {
connectionSubscription?.cancel();
if (!completer.isCompleted) {
completer.complete(event);
}
});
connectionSubscription = connectionStream(deviceId).listen(
(BleConnectionUpdate event) {
connectionSubscription?.cancel();
if (!completer.isCompleted) {
String? error = event.error;
if (error != null) {
completer.completeError(ConnectionException(error));
} else {
completer.complete(event.isConnected);
}
}
},
);
_platform
.connect(deviceId, connectionTimeout: connectionTimeout)
@@ -99,15 +109,14 @@ class UniversalBle {
(error) {
if (completer.isCompleted == false) {
connectionSubscription?.cancel();
completer.completeError(error);
completer.completeError(ConnectionException(error));
}
},
);
if (connectionTimeout != null) {
return await completer.future.timeout(connectionTimeout);
if (!await completer.future.timeout(connectionTimeout)) {
throw ConnectionException("Failed to connect");
}
return await completer.future;
} finally {
connectionSubscription?.cancel();
}
@@ -211,36 +220,65 @@ class UniversalBle {
static Future<bool?> isPaired(
String deviceId, {
BleCommand? pairingCommand,
Duration? connectionTimeout,
}) async {
if (BleCapabilities.hasSystemPairingApi) {
return _bleCommandQueue.queueCommand(
() => _platform.isPaired(deviceId),
deviceId: deviceId,
);
} else if (pairingCommand != null) {
return _connectAndExecuteBleCommand(deviceId, pairingCommand,
updateCallbackValue: false);
}
return null;
if (pairingCommand == null) {
UniversalLogger.logWarning("PairingCommand required to get result");
return null;
}
try {
await _connectAndExecuteBleCommand(
deviceId,
pairingCommand,
connectionTimeout: connectionTimeout,
updateCallbackValue: false,
);
// Because pairingCommand will be never null, so we wont get Unknown result here
return true;
} catch (e) {
UniversalLogger.logError("ExecuteBleCommandFailed: $e");
return false;
}
}
/// Pair a device.
///
/// It throws error if pairing fails.
///
/// On `Apple` and `Web`, it only works on devices with encrypted characteristics.
/// It returns null if there is no readable characteristic.
/// It is advised to pass a pairingCommand with an encrypted read or write characteristic.
/// When not passing a pairingCommand, you should afterwards use [isPaired] with a pairingCommand
/// to verify the pairing state.
///
/// You can optionally pass a pairingCommand if you know an encrypted read or write characteristic.
/// If you do, it returns true if it can successfully execute the command after pairing.
///
/// On `Web/Windows` and `Web/Linux`, it does not work for devices where `BleCapabilities.triggersConfirmOnlyPairing` is false.
static Future<bool?> pair(
/// On `Web/Windows` and `Web/Linux`, it does not work for devices that use `ConfirmOnly` pairing.
/// Can throw `PairingException`, `ConnectionException` or `PlatformException`.
static Future<void> pair(
String deviceId, {
BleCommand? pairingCommand,
Duration? connectionTimeout,
}) async {
if (BleCapabilities.hasSystemPairingApi) {
return _platform.pair(deviceId);
bool paired = await _platform.pair(deviceId);
if (!paired) throw PairingException();
} else {
if (pairingCommand == null) {
UniversalLogger.logWarning("PairingCommand required to get result");
}
await _connectAndExecuteBleCommand(
deviceId,
pairingCommand,
connectionTimeout: connectionTimeout,
);
}
return _connectAndExecuteBleCommand(deviceId, pairingCommand);
}
/// Unpair a device.
@@ -303,38 +341,32 @@ class UniversalBle {
}
}
static Future<bool?> _connectAndExecuteBleCommand(
static Future<void> _connectAndExecuteBleCommand(
String deviceId,
BleCommand? bleCommand, {
Duration? connectionTimeout,
bool updateCallbackValue = false,
}) async {
try {
if (await getConnectionState(deviceId) != BleConnectionState.connected) {
await connect(deviceId);
}
List<BleService> services = await discoverServices(deviceId);
if (bleCommand == null) {
await _attemptPairingReadingAll(deviceId, services);
return null;
} else {
bool commandResult =
await _executeBleCommand(deviceId, services, bleCommand);
if (updateCallbackValue) {
_platform.updatePairingState(deviceId, commandResult);
}
return commandResult;
}
} catch (e) {
UniversalBlePlatform.logInfo(
"FailedToPerform EncryptedCharOperation: $e",
// Try to connect first
if (await getConnectionState(deviceId) != BleConnectionState.connected) {
UniversalLogger.logInfo("Connecting to $deviceId");
await connect(
deviceId,
connectionTimeout: connectionTimeout,
);
if (updateCallbackValue) {
_platform.updatePairingState(deviceId, false);
}
return false;
}
List<BleService> services = await discoverServices(deviceId);
UniversalLogger.logInfo("Discovered services: ${services.length}");
if (bleCommand == null) {
// Just attempt pairing
await _attemptPairingReadingAll(deviceId, services);
return;
}
await _executeBleCommand(deviceId, services, bleCommand);
if (updateCallbackValue) _platform.updatePairingState(deviceId, true);
}
// Fire and forget, and do not rely on result
@@ -359,12 +391,13 @@ class UniversalBle {
}
}
} catch (_) {}
if (!containsReadCharacteristics) {
throw "No readable characteristic found";
throw PairingException("No readable characteristic found");
}
}
static Future<bool> _executeBleCommand(
static Future<void> _executeBleCommand(
String deviceId,
List<BleService> services,
BleCommand bleCommand,
@@ -384,7 +417,7 @@ class UniversalBle {
}
if (characteristic == null) {
return false;
throw PairingException("BleCommand not found in discovered services");
}
// Check if BleCommand Supports Read or Write
@@ -396,28 +429,34 @@ class UniversalBle {
bleOutputProperty = BleOutputProperty.withoutResponse;
} else if (!characteristic.properties
.contains(CharacteristicProperty.read)) {
return false;
throw PairingException(
"BleCommand does not support read or write operation",
);
}
Uint8List? value = bleCommand.writeValue;
if (value != null && bleOutputProperty != null) {
await writeValue(
deviceId,
bleCommand.service,
bleCommand.characteristic,
value,
bleOutputProperty,
);
} else {
// Fallback to read if supported
await readValue(
deviceId,
bleCommand.service,
bleCommand.characteristic,
timeout: const Duration(seconds: 30),
);
try {
if (value != null && bleOutputProperty != null) {
await writeValue(
deviceId,
bleCommand.service,
bleCommand.characteristic,
value,
bleOutputProperty,
);
} else {
// Fallback to read if supported
await readValue(
deviceId,
bleCommand.service,
bleCommand.characteristic,
timeout: const Duration(seconds: 30),
);
}
} catch (e) {
throw PairingException(e.toString());
}
return true;
}
/// Get updates of remaining items of a queue.
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/services.dart';
class ConnectionException implements Exception {
late String message;
ConnectionException([dynamic error]) {
message = _errorParser(error);
}
@override
String toString() => message;
}
class PairingException implements Exception {
late String message;
PairingException([dynamic error]) {
message = _errorParser(error);
}
@override
String toString() => message;
}
String _errorParser(dynamic error) {
if (error == null) {
return "Failed";
} else if (error is PlatformException) {
return error.message ?? error.details ?? error.code;
} else if (error is String) {
return error;
} else {
return error.toString();
}
}
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:universal_ble/src/models/model_exports.dart';
import 'package:universal_ble/src/universal_ble_filter_util.dart';
import 'package:universal_ble/src/universal_ble_platform_interface.dart';
import 'package:universal_ble/src/universal_logger.dart';
class UniversalBleLinux extends UniversalBlePlatform {
UniversalBleLinux._();
@@ -45,10 +46,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
await _activeAdapter?.setPowered(true);
return _activeAdapter?.powered ?? false;
} catch (e) {
UniversalBlePlatform.logInfo(
'Error enabling bluetooth: $e',
isError: true,
);
UniversalLogger.logError('Error enabling bluetooth: $e');
return false;
}
}
@@ -113,10 +111,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
return true;
});
} catch (e) {
UniversalBlePlatform.logInfo(
"stopScan error: $e",
isError: true,
);
UniversalLogger.logError("stopScan error: $e");
}
}
@@ -159,7 +154,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
await device.propertiesChanged.firstWhere((element) {
if (element.contains(BluezProperty.connected)) {
if (!device.connected) {
UniversalBlePlatform.logInfo(
UniversalLogger.logInfo(
"DiscoverServicesFailed: Device disconnected",
);
return true;
@@ -167,7 +162,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
}
return element.contains(BluezProperty.servicesResolved);
}).timeout(const Duration(seconds: 10), onTimeout: () {
UniversalBlePlatform.logInfo(
UniversalLogger.logInfo(
"DiscoverServicesFailed: Timeout",
);
return [];
@@ -238,8 +233,9 @@ class UniversalBleLinux extends UniversalBlePlatform {
);
break;
default:
UniversalBlePlatform.logInfo(
"UnhandledCharValuePropertyChange: $property");
UniversalLogger.logInfo(
"UnhandledCharValuePropertyChange: $property",
);
}
}
});
@@ -351,8 +347,9 @@ class UniversalBleLinux extends UniversalBlePlatform {
.map((e) => e.uuid.toString())
.any((service) => withServices.contains(service));
} else {
UniversalBlePlatform.logInfo(
'Skipping: ${device.address}: Services not resolved yet.');
UniversalLogger.logInfo(
'Skipping: ${device.address}: Services not resolved yet.',
);
return false;
}
}).toList();
@@ -394,7 +391,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
_activeAdapter ??= _client.adapters.first;
UniversalBlePlatform.logInfo(
UniversalLogger.logInfo(
'BleAdapter: ${_activeAdapter?.name} - ${_activeAdapter?.address}',
);
@@ -411,7 +408,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
break;
case BluezProperty.propertyClass:
default:
UniversalBlePlatform.logInfo(
UniversalLogger.logInfo(
"UnhandledPropertyChanged: $property",
);
}
@@ -426,10 +423,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
_initializationCompleter?.complete();
_initializationCompleter = null;
} catch (e) {
UniversalBlePlatform.logInfo(
'Error initializing: $e',
isError: true,
);
UniversalLogger.logError('Error initializing: $e');
_initializationCompleter?.completeError(e);
await _client.close();
rethrow;
@@ -501,7 +495,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
case BluezProperty.manufacturerData:
break;
default:
UniversalBlePlatform.logInfo(
UniversalLogger.logInfo(
"UnhandledDevicePropertyChanged ${device.name} ${device.address}: $property",
);
break;
File diff suppressed because it is too large Load Diff
@@ -119,10 +119,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
) async {
var devices = await _channel.getSystemDevices(withServices ?? []);
return List<BleDevice>.from(
devices
.map((e) => e?.toBleDevice(isSystemDevice: true))
.where((e) => e != null)
.toList(),
devices.map((e) => e.toBleDevice(isSystemDevice: true)).toList(),
);
}
@@ -175,8 +172,8 @@ class _UniversalBleCallbackHandler extends UniversalBleCallbackChannel {
availabilityChange(AvailabilityState.parse(state));
@override
void onConnectionChanged(String deviceId, bool connected) =>
connectionChanged(deviceId, connected);
void onConnectionChanged(String deviceId, bool connected, String? error) =>
connectionChanged(deviceId, connected, error);
@override
void onScanResult(UniversalBleScanResult result) =>
@@ -200,9 +197,9 @@ extension _UniversalBleScanResultExtension on UniversalBleScanResult {
rssi: rssi,
isPaired: isPaired,
isSystemDevice: isSystemDevice,
services: services?.nonNulls.map(BleUuidParser.string).toList() ?? [],
manufacturerDataList: manufacturerDataList?.nonNulls
.map((e) => ManufacturerData(e.companyIdentifier, e.data))
services: services?.map(BleUuidParser.string).toList() ?? [],
manufacturerDataList: manufacturerDataList
?.map((e) => ManufacturerData(e.companyIdentifier, e.data))
.toList() ??
[],
);
+18 -16
View File
@@ -1,10 +1,11 @@
import 'dart:async';
import 'dart:developer';
import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';
abstract class UniversalBlePlatform {
StreamController? _connectionStreamController;
StreamController<({String deviceId, bool isConnected, String? error})>?
_connectionStreamController;
final Map<String, bool> _pairStateMap = {};
Future<AvailabilityState> getBluetoothAvailabilityState();
@@ -57,21 +58,27 @@ abstract class UniversalBlePlatform {
bool receivesAdvertisements(String deviceId) => true;
Stream<bool> connectionStream(String deviceId) {
Stream<BleConnectionUpdate> connectionStream(String deviceId) {
_setupConnectionStreamIfRequired();
return _connectionStreamController!.stream
.where((event) => event.deviceId == deviceId)
.map((event) => event.isConnected);
.map((event) => BleConnectionUpdate(
isConnected: event.isConnected,
error: event.error,
));
}
void updateScanResult(BleDevice bleDevice) {
onScanResult?.call(bleDevice);
}
void updateConnection(String deviceId, bool isConnected) {
onConnectionChange?.call(deviceId, isConnected);
_connectionStreamController
?.add((deviceId: deviceId, isConnected: isConnected));
void updateConnection(String deviceId, bool isConnected, [String? error]) {
onConnectionChange?.call(deviceId, isConnected, error);
_connectionStreamController?.add((
deviceId: deviceId,
isConnected: isConnected,
error: error,
));
}
void updateCharacteristicValue(
@@ -97,17 +104,11 @@ abstract class UniversalBlePlatform {
OnAvailabilityChange? onAvailabilityChange;
OnPairingStateChange? onPairingStateChange;
static void logInfo(String message, {bool isError = false}) {
if (isError) message = '\x1B[31m$message\x1B[31m';
log(message, name: 'UniversalBle');
}
/// Creates an auto disposable streamController
void _setupConnectionStreamIfRequired() {
if (_connectionStreamController != null) return;
_connectionStreamController =
StreamController<({String deviceId, bool isConnected})>.broadcast();
_connectionStreamController = StreamController.broadcast();
// Auto dispose if no more subscribers
_connectionStreamController?.onCancel = () {
@@ -119,7 +120,8 @@ abstract class UniversalBlePlatform {
}
// Callback types
typedef OnConnectionChange = void Function(String deviceId, bool isConnected);
typedef OnConnectionChange = void Function(
String deviceId, bool isConnected, String? error);
typedef OnValueChange = void Function(
String deviceId, String characteristicId, Uint8List value);
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
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_logger.dart';
class UniversalBleWeb extends UniversalBlePlatform {
static UniversalBleWeb? _instance;
@@ -127,10 +128,7 @@ class UniversalBleWeb extends UniversalBlePlatform {
device.advertisementsUseMemory = true;
await device.watchAdvertisements();
} catch (e) {
UniversalBlePlatform.logInfo(
"WebWatchAdvertisementError: $e",
isError: true,
);
UniversalLogger.logError("WebWatchAdvertisementError: $e");
}
}
@@ -395,9 +393,8 @@ class UniversalBleWeb extends UniversalBlePlatform {
}
if (optionalServices.isEmpty) {
UniversalBlePlatform.logInfo(
UniversalLogger.logError(
"OptionalServices list is empty on web, you have to specify services in the ScanFilter in order to be able to access those after connecting",
isError: true,
);
}
+24
View File
@@ -0,0 +1,24 @@
import 'dart:developer';
class UniversalLogger {
static void logInfo(message) {
log(
message.toString(),
name: 'UniversalBle:INFO',
);
}
static void logError(message) {
log(
'\x1B[31m$message\x1B[31m',
name: 'UniversalBle:ERROR',
);
}
static void logWarning(message) {
log(
'\x1B[33m$message\x1B[33m',
name: 'UniversalBle:WARN',
);
}
}