From 7606e8fd292023eca2195d05c721911bb09fb735 Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Fri, 9 Aug 2024 19:17:31 +0200 Subject: [PATCH] Implement pairing on Apple (#59) --- .vscode/settings.json | 1 + CHANGELOG.md | 12 +- README.md | 7 +- darwin/Classes/UniversalBlePlugin.swift | 2 +- example/lib/data/capabilities.dart | 2 +- example/lib/data/mock_universal_ble.dart | 12 +- example/lib/home/home.dart | 2 +- .../peripheral_detail_page.dart | 67 ++-- example/macos/Runner/AppDelegate.swift | 2 +- example/pubspec.lock | 2 +- lib/src/models/ble_command.dart | 13 + lib/src/models/model_exports.dart | 2 + lib/src/universal_ble.dart | 298 +++++++++++++----- .../universal_ble_linux.dart | 16 +- .../universal_ble_pigeon_channel.dart | 17 +- lib/src/universal_ble_platform_interface.dart | 45 ++- .../universal_ble_web/universal_ble_web.dart | 8 +- pubspec.yaml | 2 +- 18 files changed, 371 insertions(+), 139 deletions(-) create mode 100644 lib/src/models/ble_command.dart diff --git a/.vscode/settings.json b/.vscode/settings.json index 00a16e3..713d462 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "BTLE", "BTNHIGHLIGHT", "bytevc", + "cccd", "connnection", "Cupertino", "DFACE", diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ea40f..0919cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ -## 0.11.2 +## 0.12.0 +* BREAKING CHANGE: `unPair` is now `unpair` +* Add isPaired support on Apple +* `connect` will now return the connection result * Add `PlatformConfig` property in `StartScan` * Add `WebConfig` property in `PlatformConfig` -* Fix notification for characteristics without cccd +* Fix notifications for characteristics without cccd on Android +* Add `connectionStream` API to get connection updates as stream ## 0.11.1 * Trim spaces in UUIDs @@ -11,9 +15,13 @@ ## 0.11.0 * Unify UUID format across all platforms, 128-bit lowercase * Add BleUuidParser utility methods for UUID parsing + +## 0.10.1 * Improve Android error handling * Fix Android disconnection events sometimes missed * Improve cleanup after disconnection on Apple and Android +* Support pairing on Apple +* Improve code level documentation ## 0.10.0 * BREAKING CHANGE: `ScanResult` is now `BleDevice` diff --git a/README.md b/README.md index 23dc6ee..7f9375b 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE | readValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | writeValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | setNotifiable | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | -| pair/unPair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | +| pair | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | +| unpair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | +| isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ? | +| getBluetoothAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | | onPairingStateChange | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | | enableBluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | | onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | @@ -201,7 +204,7 @@ UniversalBle.onPairingStateChange = (String deviceId, bool isPaired, String? err } // Unpair -UniversalBle.unPair(deviceId); +UniversalBle.unpair(deviceId); // Check current pairing state bool isPaired = UniversalBle.isPaired(deviceId); diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index 21bb158..ceda265 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -281,7 +281,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } func pair(deviceId _: String) throws { - throw FlutterError(code: "NotSupported", message: nil, details: nil) + throw FlutterError(code: "Implemented in Dart", message: nil, details: nil) } func unPair(deviceId _: String) throws { diff --git a/example/lib/data/capabilities.dart b/example/lib/data/capabilities.dart index 039d253..a2606bc 100644 --- a/example/lib/data/capabilities.dart +++ b/example/lib/data/capabilities.dart @@ -9,7 +9,7 @@ class Capabilities { static bool supportsConnectedDevicesApi = !Platform.isWeb; - static bool supportsPairingApi = !Platform.isWeb && !Platform.isCupertino; + static bool supportsPairingApi = !Platform.isWeb; static bool supportsRequestMtuApi = !Platform.isWeb; } diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index 9d58fd5..f5945b5 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -27,19 +27,19 @@ class MockUniversalBle extends UniversalBlePlatform { ScanFilter? scanFilter, PlatformConfig? platformConfig, }) async => - onScanResult?.call(_mockBleDevice); + updateScanResult(_mockBleDevice); @override Future stopScan() async {} @override Future connect(String deviceId, {Duration? connectionTimeout}) async { - onConnectionChange?.call(deviceId, true); + updateConnection(deviceId, true); } @override Future disconnect(String deviceId) async { - onConnectionChange?.call(deviceId, false); + updateConnection(deviceId, false); } @override @@ -99,12 +99,12 @@ class MockUniversalBle extends UniversalBlePlatform { @override Future pair(String deviceId) async { - onPairingStateChange?.call(deviceId, true, null); + updatePairingState(deviceId, true, null); } @override - Future unPair(String deviceId) async { - onPairingStateChange?.call(deviceId, false, null); + Future unpair(String deviceId) async { + updatePairingState(deviceId, false, null); } @override diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart index f7d3913..e0769ad 100644 --- a/example/lib/home/home.dart +++ b/example/lib/home/home.dart @@ -192,7 +192,7 @@ class _MyAppState extends State { }, ), PlatformButton( - text: 'Queue: ${_queueType.name.toUpperCase()}', + text: 'Queue: ${_queueType.name}', onPressed: () { setState(() { _queueType = switch (_queueType) { diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index b37273a..6c97417 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -84,11 +84,11 @@ class _PeripheralDetailPageState extends State { void _handlePairingStateChange( String deviceId, bool isPaired, String? error) { - print('OnPairStateChange $deviceId, $isPaired'); + print('isPaired $deviceId, $isPaired'); if (error != null && error.isNotEmpty) { _addLog("PairStateChangeError", "(Paired: $isPaired): $error "); } else { - _addLog("PairStateChange", isPaired); + _addLog("PairStateChange - isPaired", isPaired); } } @@ -254,7 +254,10 @@ class _PeripheralDetailPageState extends State { enabled: !isConnected, onPressed: () async { try { - await UniversalBle.connect(widget.deviceId); + bool connected = await UniversalBle.connect( + widget.deviceId, + ); + _addLog("ConnectionResult", connected); } catch (e) { _addLog('ConnectError', e); } @@ -405,29 +408,41 @@ class _PeripheralDetailPageState extends State { BleInputProperty.disabled), text: 'Unsubscribe', ), - if (Capabilities.supportsPairingApi) - PlatformButton( - onPressed: () async { - await UniversalBle.pair(widget.deviceId); - }, - text: 'Pair', - ), - if (Capabilities.supportsPairingApi) - PlatformButton( - onPressed: () async { - bool? isPaired = await UniversalBle.isPaired( - widget.deviceId); - _addLog('IsPaired', isPaired); - }, - text: 'IsPaired', - ), - if (Capabilities.supportsPairingApi) - PlatformButton( - onPressed: () async { - await UniversalBle.unPair(widget.deviceId); - }, - text: 'UnPair', - ), + PlatformButton( + onPressed: () async { + await UniversalBle.pair( + widget.deviceId, + // pairingCommand: BleCommand( + // service: + // "", + // characteristic: + // "", + // ), + ); + }, + text: 'Pair', + ), + PlatformButton( + onPressed: () async { + bool? isPaired = await UniversalBle.isPaired( + widget.deviceId, + // pairingCheckCommand: BleCommand( + // service: + // "", + // characteristic: + // "", + // ), + ); + _addLog('IsPaired', isPaired); + }, + text: 'IsPaired', + ), + PlatformButton( + onPressed: () async { + await UniversalBle.unpair(widget.deviceId); + }, + text: 'Unpair', + ), ], ), ), diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift index d53ef64..8e02df2 100644 --- a/example/macos/Runner/AppDelegate.swift +++ b/example/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/example/pubspec.lock b/example/pubspec.lock index 32c1274..7c083fa 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -402,7 +402,7 @@ packages: path: ".." relative: true source: path - version: "0.11.1" + version: "0.12.0" vector_math: dependency: transitive description: diff --git a/lib/src/models/ble_command.dart b/lib/src/models/ble_command.dart new file mode 100644 index 0000000..345d19c --- /dev/null +++ b/lib/src/models/ble_command.dart @@ -0,0 +1,13 @@ +import 'dart:typed_data'; + +class BleCommand { + String service; + String characteristic; + Uint8List? writeValue; + + BleCommand({ + required this.service, + required this.characteristic, + this.writeValue, + }); +} diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart index 1671518..207b7c4 100644 --- a/lib/src/models/model_exports.dart +++ b/lib/src/models/model_exports.dart @@ -7,3 +7,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_device.dart'; +export 'package:universal_ble/src/models/ble_command.dart'; + diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index bd32821..f4e160c 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -9,17 +9,17 @@ import 'package:universal_ble/src/universal_ble_web/universal_ble_web.dart'; import 'package:universal_ble/universal_ble.dart'; class UniversalBle { - /// Get platform specific implementation + /// Get platform specific implementation. static UniversalBlePlatform _platform = _defaultPlatform(); static final BleCommandQueue _bleCommandQueue = BleCommandQueue(); - /// Set custom platform specific implementation (e.g. for testing) + /// Set custom platform specific implementation (e.g. for testing). static void setInstance(UniversalBlePlatform instance) => _platform = instance; /// Set global timeout for all commands. - /// Default timeout is 10 seconds - /// Set to null to disable + /// Default timeout is 10 seconds. + /// Set to null to disable. static set timeout(Duration? duration) { _bleCommandQueue.timeout = duration; } @@ -27,16 +27,16 @@ class UniversalBle { /// 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 + /// [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 + /// Get Bluetooth availability state. + /// To be notified of updates, set [onAvailabilityChange] listener. static Future getBluetoothAvailabilityState() async { return await _bleCommandQueue.queueCommand( () => _platform.getBluetoothAvailabilityState(), @@ -44,9 +44,9 @@ class UniversalBle { } /// Start scan. - /// Scan results will arrive in [onScanResult] listener - /// It might throw errors if Bluetooth is not available - /// `webRequestOptions` is supported on Web only + /// Scan results will arrive in [onScanResult] listener. + /// It might throw errors if Bluetooth is not available. + /// `webRequestOptions` is supported on Web only. static Future startScan({ ScanFilter? scanFilter, PlatformConfig? platformConfig, @@ -61,8 +61,8 @@ class UniversalBle { } /// Stop scan. - /// Set [onScanResult] listener to `null` if you don't need it anymore - /// It might throw errors if Bluetooth is not available + /// Set [onScanResult] listener to `null` if you don't need it anymore. + /// It might throw errors if Bluetooth is not available. static Future stopScan() async { return await _bleCommandQueue.queueCommand( () => _platform.stopScan(), @@ -70,23 +70,52 @@ class UniversalBle { ); } + /// Connection stream of a device + Stream connectionStream(String deviceId) => + _platform.connectionStream(deviceId); + /// Connect to a device. - /// Get notified of connection state changes in [onConnectionChange] 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 connect( + /// 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 connect( String deviceId, { Duration? connectionTimeout, }) async { - return await _bleCommandQueue.queueCommand( - () => _platform.connect(deviceId, connectionTimeout: connectionTimeout), - deviceId: deviceId, - ); + StreamSubscription? connectionSubscription; + try { + Completer completer = Completer(); + + connectionSubscription = + _platform.connectionStream(deviceId).listen((bool event) { + connectionSubscription?.cancel(); + if (!completer.isCompleted) { + completer.complete(event); + } + }); + + _platform + .connect(deviceId, connectionTimeout: connectionTimeout) + .catchError( + (error) { + if (completer.isCompleted == false) { + connectionSubscription?.cancel(); + completer.completeError(error); + } + }, + ); + + if (connectionTimeout != null) { + return await completer.future.timeout(connectionTimeout); + } + return await completer.future; + } finally { + connectionSubscription?.cancel(); + } } /// Disconnect from a device. - /// Get notified of connection state changes in [onConnectionChange] listener + /// Get notified of connection state changes in [onConnectionChange] listener. static Future disconnect(String deviceId) async { return await _bleCommandQueue.queueCommand( () => _platform.disconnect(deviceId), @@ -94,7 +123,7 @@ class UniversalBle { ); } - /// Discover services of a device + /// Discover services of a device. static Future> discoverServices(String deviceId) async { return await _bleCommandQueue.queueCommand( () => _platform.discoverServices(deviceId), @@ -103,9 +132,9 @@ class UniversalBle { } /// Set a characteristic notifiable. - /// Set `bleInputProperty` to [BleInputProperty.notification] or [BleInputProperty.indication] - /// Updates will arrive in [onValueChange] listener - /// To stop listening to a characteristic, set `bleInputProperty` to [BleInputProperty.disabled] + /// Set `bleInputProperty` to [BleInputProperty.notification] or [BleInputProperty.indication]. + /// Updates will arrive in [onValueChange] listener. + /// To stop listening to a characteristic, set `bleInputProperty` to [BleInputProperty.disabled]. static Future setNotifiable( String deviceId, String service, @@ -123,8 +152,8 @@ class UniversalBle { ); } - /// Read a characteristic value - /// On iOS and MacOS this command will also trigger [onValueChange] listener + /// Read a characteristic value. + /// On iOS and MacOS this command will also trigger [onValueChange] listener. static Future readValue( String deviceId, String service, @@ -140,8 +169,8 @@ class UniversalBle { ); } - /// Write a characteristic value - /// To write a characteristic value with response, set `bleOutputProperty` to [BleOutputProperty.withResponse] + /// Write a characteristic value. + /// To write a characteristic value with response, set `bleOutputProperty` to [BleOutputProperty.withResponse]. static Future writeValue( String deviceId, String service, @@ -161,8 +190,8 @@ class UniversalBle { ); } - /// Request MTU value - /// `requestMtu` is not supported on `Linux` and `Web + /// Request MTU value. + /// `requestMtu` is not supported on `Linux` and `Web. static Future requestMtu(String deviceId, int expectedMtu) async { return await _bleCommandQueue.queueCommand( () => _platform.requestMtu(deviceId, expectedMtu), @@ -170,39 +199,61 @@ class UniversalBle { ); } - /// Check if a device is paired - /// Returns null on `Apple` and `Web` - static Future isPaired(String deviceId) async { - if (kIsWeb || Platform.isIOS || Platform.isMacOS) return null; + /// Check if a device is paired. + /// Returns null on `Apple` and `Web` when no `bleCommand` is passed. + /// + /// On Apple and Web, you can optionally pass a pairingCheckCommand if you know an encrypted read or write characteristic. + /// It will trigger pairing if the device is not already paired. + /// It will return true/false if it manages to execute the command. + static Future isPaired( + String deviceId, { + BleCommand? pairingCheckCommand, + }) async { + bool lacksPairingApi = kIsWeb || Platform.isMacOS || Platform.isIOS; + if (lacksPairingApi && pairingCheckCommand == null) { + return null; + } + + return lacksPairingApi + ? await _connectAndExecuteBleCommand(deviceId, pairingCheckCommand) + : await _bleCommandQueue.queueCommand( + () => _platform.isPaired(deviceId), + deviceId: deviceId, + ); + } + + /// Pair a device. + /// It might throw an error if device is already paired. + /// + /// On Apple, it only works on devices with encrypted characteristics. + /// It throws an error if there is no readable characteristic. + /// + /// You can optionally pass a pairingCommand if you know an encrypted read or write characteristic. + static Future pair( + String deviceId, { + BleCommand? pairingCommand, + }) => + kIsWeb || Platform.isMacOS || Platform.isIOS + ? _connectAndExecuteBleCommand(deviceId, pairingCommand) + : _bleCommandQueue.queueCommand( + () => _platform.pair(deviceId), + deviceId: deviceId, + ); + + /// Unpair a device. + /// It might throw an error if device is not paired. + static Future unpair(String deviceId) async { return await _bleCommandQueue.queueCommand( - () => _platform.isPaired(deviceId), + () => _platform.unpair(deviceId), deviceId: deviceId, ); } - /// Trigger pair request - /// It might throw an error if device is already paired - static Future pair(String deviceId) async { - return await _bleCommandQueue.queueCommand( - () => _platform.pair(deviceId), - deviceId: deviceId, - ); - } - - /// Unpair a device - /// It might throw an error if device is not paired - static Future unPair(String deviceId) async { - return await _bleCommandQueue.queueCommand( - () => _platform.unPair(deviceId), - deviceId: deviceId, - ); - } - - /// 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` + /// 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> getSystemDevices({ List? withServices, }) async { @@ -220,9 +271,9 @@ class UniversalBle { ); } - /// Enable Bluetooth - /// It might throw errors if Bluetooth is not available - /// Not supported on `Web` and `Apple` + /// Enable Bluetooth. + /// It might throw errors if Bluetooth is not available. + /// Not supported on `Web` and `Apple`. static Future enableBluetooth() async { return await _bleCommandQueue.queueCommand( () => _platform.enableBluetooth(), @@ -239,7 +290,7 @@ class UniversalBle { static bool receivesAdvertisements(String deviceId) => _platform.receivesAdvertisements(deviceId); - /// Get Bluetooth state availability + /// Get Bluetooth state availability. static set onAvailabilityChange(OnAvailabilityChange? onAvailabilityChange) { _platform.onAvailabilityChange = onAvailabilityChange; if (onAvailabilityChange != null) { @@ -249,23 +300,128 @@ class UniversalBle { } } - /// Get updates of remaining items of a queue + static Future _connectAndExecuteBleCommand( + String deviceId, + BleCommand? bleCommand, + ) async { + try { + if (await getConnectionState(deviceId) != BleConnectionState.connected) { + await connect(deviceId); + } + + List services = await discoverServices(deviceId); + + if (bleCommand == null) { + await _attemptPairingReadingAll(deviceId, services); + return null; + } else { + await _executeBleCommand(deviceId, services, bleCommand); + _platform.updatePairingState(deviceId, true, null); + return true; + } + } catch (e) { + UniversalBlePlatform.logInfo( + "FailedToPerform EncryptedCharOperation: $e", + ); + // Probably failed to pair, Notify callback + _platform.updatePairingState(deviceId, false, e.toString()); + return false; + } + } + + // Fire and forget, and do not rely on result + static _attemptPairingReadingAll( + String deviceId, + List services, + ) async { + bool containsReadCharacteristics = false; + try { + // If BleCommand not given, fallback to reading all characteristics + for (BleService service in services) { + for (BleCharacteristic characteristic in service.characteristics) { + if (characteristic.properties.contains(CharacteristicProperty.read)) { + containsReadCharacteristics = true; + await readValue(deviceId, service.uuid, characteristic.uuid); + } + } + } + } catch (_) {} + if (!containsReadCharacteristics) { + throw "No readable characteristic found"; + } + } + + static Future _executeBleCommand( + String deviceId, + List services, + BleCommand bleCommand, + ) async { + // First find BleCommand's characteristic + BleCharacteristic? characteristic; + for (BleService service in services) { + if (BleUuidParser.compareStrings(service.uuid, bleCommand.service)) { + for (BleCharacteristic char in service.characteristics) { + if (BleUuidParser.compareStrings( + char.uuid, bleCommand.characteristic)) { + characteristic = char; + break; + } + } + } + } + + if (characteristic == null) { + throw "BleCommand char not found"; + } + + // Check if BleCommand Supports Read or Write + BleOutputProperty? bleOutputProperty; + if (characteristic.properties.contains(CharacteristicProperty.write)) { + bleOutputProperty = BleOutputProperty.withResponse; + } else if (characteristic.properties + .contains(CharacteristicProperty.writeWithoutResponse)) { + bleOutputProperty = BleOutputProperty.withoutResponse; + } else if (!characteristic.properties + .contains(CharacteristicProperty.read)) { + throw "BleCommand does not support read or write operations"; + } + + 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, + ); + } + } + + /// Get updates of remaining items of a queue. static set onQueueUpdate(OnQueueUpdate? onQueueUpdate) => _bleCommandQueue.onQueueUpdate = onQueueUpdate; - /// Get scan results + /// Get scan results. static set onScanResult(OnScanResult? bleDevice) => _platform.onScanResult = bleDevice; - /// Get connection state changes + /// Get connection state changes. static set onConnectionChange(OnConnectionChange? onConnectionChange) => _platform.onConnectionChange = onConnectionChange; - /// 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 onValueChange(OnValueChange? onValueChange) => _platform.onValueChange = onValueChange; - /// Get pair state changes, + /// Get pair state changes. static set onPairingStateChange(OnPairingStateChange pairingStateChange) => _platform.onPairingStateChange = pairingStateChange; diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index eb69bb3..5cb453d 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -101,7 +101,7 @@ class UniversalBleLinux extends UniversalBlePlatform { Future connect(String deviceId, {Duration? connectionTimeout}) async { final device = _findDeviceById(deviceId); if (device.connected) { - onConnectionChange?.call(deviceId, true); + updateConnection(deviceId, true); return; } await device.connect(); @@ -111,7 +111,7 @@ class UniversalBleLinux extends UniversalBlePlatform { Future disconnect(String deviceId) async { final device = _findDeviceById(deviceId); if (!device.connected) { - onConnectionChange?.call(deviceId, false); + updateConnection(deviceId, false); return; } await device.disconnect(); @@ -273,12 +273,12 @@ class UniversalBleLinux extends UniversalBlePlatform { Future pair(String deviceId) async { BlueZDevice device = _findDeviceById(deviceId); device.pair().onError((error, _) { - onPairingStateChange?.call(deviceId, false, error.toString()); + updatePairingState(deviceId, false, error.toString()); }); } @override - Future unPair(String deviceId) async { + Future unpair(String deviceId) async { BlueZDevice device = _findDeviceById(deviceId); if (device.paired) { // await device.cancelPairing(); @@ -356,7 +356,7 @@ class UniversalBleLinux extends UniversalBlePlatform { for (final property in properties) { switch (property) { case BluezProperty.powered: - onAvailabilityChange?.call(_availabilityState); + updateAvailability(_availabilityState); break; case BluezProperty.discoverable: case BluezProperty.discovering: @@ -374,7 +374,7 @@ class UniversalBleLinux extends UniversalBlePlatform { _client.deviceAdded.listen(_onDeviceAdd); _client.deviceRemoved.listen(_onDeviceRemoved); - onAvailabilityChange?.call(_availabilityState); + updateAvailability(_availabilityState); isInitialized = true; _initializationCompleter?.complete(); _initializationCompleter = null; @@ -433,13 +433,13 @@ class UniversalBleLinux extends UniversalBlePlatform { updateScanResult(device.toBleDevice()); break; case BluezProperty.connected: - onConnectionChange?.call(device.address, device.connected); + updateConnection(device.address, device.connected); break; case BluezProperty.manufacturerData: updateScanResult(device.toBleDevice()); break; case BluezProperty.paired: - onPairingStateChange?.call(device.address, device.paired, null); + updatePairingState(device.address, device.paired, null); break; // Ignored these properties updates case BluezProperty.bonded: diff --git a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart index 2a04977..e6213fd 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -108,7 +108,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { Future pair(String deviceId) => _channel.pair(deviceId); @override - Future unPair(String deviceId) => _channel.unPair(deviceId); + Future unpair(String deviceId) => _channel.unPair(deviceId); @override Future> getSystemDevices( @@ -126,16 +126,11 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { /// To set listeners void _setupListeners() { UniversalBleCallbackChannel.setUp(_UniversalBleCallbackHandler( - scanResult: (BleDevice bleDevice) => updateScanResult(bleDevice), - availabilityChange: (AvailabilityState state) => - onAvailabilityChange?.call(state), - connectionChanged: (String deviceId, bool connected) => - onConnectionChange?.call(deviceId, connected), - valueChanged: - (String deviceId, String characteristicId, Uint8List value) => - updateCharacteristicValue(deviceId, characteristicId, value), - pairStateChange: (String deviceId, bool isPaired, String? error) => - onPairingStateChange?.call(deviceId, isPaired, error), + scanResult: updateScanResult, + availabilityChange: updateAvailability, + connectionChanged: updateConnection, + valueChanged: updateCharacteristicValue, + pairStateChange: updatePairingState, )); } } diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index 2e85660..2ef89af 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'dart:developer'; import 'dart:typed_data'; import 'package:universal_ble/universal_ble.dart'; abstract class UniversalBlePlatform { ScanFilter? _scanFilter; + StreamController? _connectionStreamController; Future getBluetoothAvailabilityState(); @@ -43,7 +45,7 @@ abstract class UniversalBlePlatform { Future pair(String deviceId); - Future unPair(String deviceId); + Future unpair(String deviceId); Future getConnectionState(String deviceId); @@ -51,6 +53,15 @@ abstract class UniversalBlePlatform { List? withServices, ); + bool receivesAdvertisements(String deviceId) => true; + + Stream connectionStream(String deviceId) { + _setupConnectionStreamIfRequired(); + return _connectionStreamController!.stream + .where((event) => event.deviceId == deviceId) + .map((event) => event.isConnected); + } + void updateScanResult(BleDevice bleDevice) { // Filter by name ScanFilter? scanFilter = _scanFilter; @@ -62,7 +73,11 @@ abstract class UniversalBlePlatform { onScanResult?.call(bleDevice); } - bool receivesAdvertisements(String deviceId) => true; + void updateConnection(String deviceId, bool isConnected) { + onConnectionChange?.call(deviceId, isConnected); + _connectionStreamController + ?.add((deviceId: deviceId, isConnected: isConnected)); + } void updateCharacteristicValue( String deviceId, String characteristicId, Uint8List value) { @@ -70,16 +85,40 @@ abstract class UniversalBlePlatform { deviceId, BleUuidParser.string(characteristicId), value); } - OnAvailabilityChange? onAvailabilityChange; + void updateAvailability(AvailabilityState state) { + onAvailabilityChange?.call(state); + } + + void updatePairingState(String deviceId, bool isPaired, String? error) { + onPairingStateChange?.call(deviceId, isPaired, error); + } + + // Do not use these directly to push updates OnScanResult? onScanResult; OnConnectionChange? onConnectionChange; OnValueChange? onValueChange; + 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(); + + // Auto dispose if no more subscribers + _connectionStreamController?.onCancel = () { + // logInfo('Disposing Connection Stream'); + _connectionStreamController?.close(); + _connectionStreamController = null; + }; + } } // Callback types diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 0c7661a..02bc233 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -45,14 +45,14 @@ class UniversalBleWeb extends UniversalBlePlatform { _connectedDeviceStreamList[deviceId] = device.connected.listen((event) { if (!event) _cleanConnection(deviceId); - onConnectionChange?.call(deviceId, event); + updateConnection(deviceId, event); }); } @override Future disconnect(String deviceId) async { _cleanConnection(deviceId); - onConnectionChange?.call(deviceId, false); + updateConnection(deviceId, false); _getDeviceById(deviceId)?.disconnect(); } @@ -246,7 +246,7 @@ class UniversalBleWeb extends UniversalBlePlatform { } @override - Future unPair(String deviceId) { + Future unpair(String deviceId) { throw UnimplementedError(); } @@ -270,7 +270,7 @@ class UniversalBleWeb extends UniversalBlePlatform { } else if (isAvailable) { newState = AvailabilityState.poweredOn; } - onAvailabilityChange?.call(newState); + updateAvailability(newState); }, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 4df0c27..f2703cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.11.1 +version: 0.12.0 homepage: https://navideck.com repository: https://github.com/Navideck/universal_ble issue_tracker: https://github.com/Navideck/universal_ble/issues