diff --git a/CHANGELOG.md b/CHANGELOG.md index b47e7bc..3846fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ ## 0.9.12 -* Add .perDevice queue -* Improve code level documentation +* BREAKING CHANGE: Rename ScanResult to BleDevice +* Add `connectionState` property to BleDevice +* Add `isSystemDevice` property to BleDevice +* Add `.perDevice` queue * Support "ProvidePin" pairing on Windows 10/11 * Get RRSI updates on Apple platforms +* Improve code level documentation +* Improve enum parsing performance ## 0.9.11 * Add device name prefix filtering diff --git a/README.md b/README.md index 58d2848..b8bbd00 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE | :------------------- | :-----: | :-: | :---: | :-----: | :----------: | :-: | | startScan/stopScan | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | connect/disconnect | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | -| getConnectedDevices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | +| getSystemDevices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | | discoverServices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | readValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | writeValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | @@ -53,8 +53,8 @@ import 'package:universal_ble/universal_ble.dart'; ```dart // Set a scan result handler -UniversalBle.onScanResult = (scanResult) { - // e.g. Use scan result to connect +UniversalBle.onScanResult = (bleDevice) { + // e.g. Use BleDevice ID to connect } // Perform a scan @@ -73,6 +73,7 @@ UniversalBle.stopScan(); ``` Before initiating a scan, ensure that Bluetooth is available: + ```dart AvailabilityState state = await UniversalBle.getBluetoothAvailabilityState() // Start scan only if Bluetooth is powered on @@ -93,13 +94,16 @@ See the [Bluetooth Availability](#bluetooth-availability) section for more. #### Connected Devices Already connected devices, either through previous sessions or connected through system settings, won't show up as scan results. -You can list those devices using `getConnectedDevices()`. You still need to explicitly connect before using them. +You can list those devices using `getSystemDevices()`. You still need to explicitly connect before using them. ```dart +// Get connected devices // You can set `withServices` to narrow down the results -await UniversalBle.getConnectedDevices(withServices: []); +List devices = await UniversalBle.getSystemDevices(withServices: []); ``` +For each connected device the `isConnected` property will be `true`. + #### Scan Filter You can optionally set filters when scanning. @@ -119,6 +123,7 @@ Use the `withManufacturerData` parameter to filter devices by manufacturer data. ```dart List withManufacturerData; ``` + ##### With namePrefix Use the `withNamePrefix` parameter to filter devices by names (case sensitive). When you pass a list of names, the scan results will only include devices that have this name or start with the provided parameter. @@ -130,8 +135,8 @@ List withNamePrefix; ### Connecting ```dart -// Connect to a device using the `deviceId` of the scanResult received from `UniversalBle.onScanResult` -String deviceId = scanResult.deviceId; +// Connect to a device using the `deviceId` of the BleDevice received from `UniversalBle.onScanResult` +String deviceId = bleDevice.deviceId; UniversalBle.connect(deviceId); // Disconnect from a device @@ -215,7 +220,7 @@ By default, all commands are executed in a global queue (`QueueType.global`), wi If you want to parallelize commands between multiple devices, you can set: ```dart -// Create a separate queue for each device. +// Create a separate queue for each device. UniversalBle.queueType = QueueType.perDevice; ``` diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt index b44969c..0098d3e 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt @@ -257,7 +257,8 @@ interface UniversalBlePlatformChannel { fun isPaired(deviceId: String, callback: (Result) -> Unit) fun pair(deviceId: String) fun unPair(deviceId: String) - fun getConnectedDevices(withServices: List, callback: (Result>) -> Unit) + fun getSystemDevices(withServices: List, callback: (Result>) -> Unit) + fun isConnected(deviceId: String): Boolean companion object { /** The codec used by UniversalBlePlatformChannel. */ @@ -538,12 +539,12 @@ interface UniversalBlePlatformChannel { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val withServicesArg = args[0] as List - api.getConnectedDevices(withServicesArg) { result: Result> -> + api.getSystemDevices(withServicesArg) { result: Result> -> val error = result.exceptionOrNull() if (error != null) { reply.reply(wrapError(error)) @@ -557,6 +558,23 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isConnected$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + val wrapped: List = try { + listOf(api.isConnected(deviceIdArg)) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt index db5fc48..eda5175 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -25,7 +25,7 @@ import java.util.UUID private const val TAG = "UniversalBlePlugin" val knownGatts = mutableListOf() -val ccdCharacteristic = "00002902-0000-1000-8000-00805f9b34fb" +const val ccdCharacteristic = "00002902-0000-1000-8000-00805f9b34fb" enum class BleConnectionState(val value: Long) { Connected(0), diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index aba8cb2..9ec4d30 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -180,6 +180,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), cleanConnection(deviceId.toBluetoothGatt()) } + override fun isConnected(deviceId: String): Boolean { + return devicesStateMap[deviceId] == BluetoothGatt.STATE_CONNECTED + } + override fun discoverServices( deviceId: String, callback: (Result>) -> Unit, @@ -498,7 +502,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - override fun getConnectedDevices( + override fun getSystemDevices( withServices: List, callback: (Result>) -> Unit, ) { diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index c88c702..22a565e 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -258,7 +258,8 @@ protocol UniversalBlePlatformChannel { func isPaired(deviceId: String, completion: @escaping (Result) -> Void) func pair(deviceId: String) throws func unPair(deviceId: String) throws - func getConnectedDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) + func getSystemDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) + func isConnected(deviceId: String) throws -> Bool } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -498,12 +499,12 @@ class UniversalBlePlatformChannelSetup { } else { unPairChannel.setMessageHandler(nil) } - let getConnectedDevicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getSystemDevicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - getConnectedDevicesChannel.setMessageHandler { message, reply in + getSystemDevicesChannel.setMessageHandler { message, reply in let args = message as! [Any?] let withServicesArg = args[0] as! [String] - api.getConnectedDevices(withServices: withServicesArg) { result in + api.getSystemDevices(withServices: withServicesArg) { result in switch result { case .success(let res): reply(wrapResult(res)) @@ -513,7 +514,22 @@ class UniversalBlePlatformChannelSetup { } } } else { - getConnectedDevicesChannel.setMessageHandler(nil) + getSystemDevicesChannel.setMessageHandler(nil) + } + let isConnectedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isConnected\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isConnectedChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + do { + let result = try api.isConnected(deviceId: deviceIdArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + isConnectedChannel.setMessageHandler(nil) } } } diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index 81344fb..60bde90 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -95,6 +95,13 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral cleanUpConnection(deviceId: deviceId) } + func isConnected(deviceId: String) -> Bool { + guard let peripheral = discoveredPeripherals[deviceId] else { + return false + } + return peripheral.state == CBPeripheralState.connected + } + func cleanUpConnection(deviceId: String) { characteristicReadFutures.removeAll { future in if future.deviceId == deviceId { @@ -254,7 +261,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral throw FlutterError(code: "NotSupported", message: nil, details: nil) } - func getConnectedDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) { + func getSystemDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) { var filterCBUUID = withServices.map { CBUUID(string: $0) } // We can't keep this filter empty, so adding a default filter if filterCBUUID.isEmpty { filterCBUUID.append(CBUUID(string: "1800")) } diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index 29cfac3..2d8adda 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -5,7 +5,7 @@ import 'package:universal_ble/universal_ble.dart'; /// Mock implementation of [UniversalBlePlatform] for testing class MockUniversalBle extends UniversalBlePlatform { - final _mockBleScanResult = BleScanResult( + final _mockBleDevice = BleDevice( name: 'MockDevice', deviceId: 'MockDeviceId', rssi: 50, @@ -23,11 +23,8 @@ class MockUniversalBle extends UniversalBlePlatform { ]); @override - Future startScan({ - ScanFilter? scanFilter, - }) async { - onScanResult?.call(_mockBleScanResult); - } + Future startScan({ScanFilter? scanFilter}) async => + onScanResult?.call(_mockBleDevice); @override Future stopScan() async {} @@ -59,8 +56,7 @@ class MockUniversalBle extends UniversalBlePlatform { } @override - Future> getConnectedDevices( - List? withServices) async { + Future> getSystemDevices(List? withServices) async { return []; } @@ -107,4 +103,9 @@ class MockUniversalBle extends UniversalBlePlatform { Future unPair(String deviceId) async { onPairingStateChange?.call(deviceId, false, null); } + + @override + Future isConnected(String deviceId) { + throw UnimplementedError(); + } } diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart index c2440dc..269d6cf 100644 --- a/example/lib/home/home.dart +++ b/example/lib/home/home.dart @@ -20,7 +20,7 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - final _scanResults = []; + final _bleDevices = []; bool _isScanning = false; QueueType _queueType = QueueType.global; @@ -55,16 +55,16 @@ class _MyAppState extends State { }; UniversalBle.onScanResult = (result) { - // debugPrint("ScanResult: ${result.name} ${result.services}"); + // debugPrint("BleDevice: ${result.name} ${result.services}"); // debugPrint("${result.name} ${result.manufacturerData}"); - int index = _scanResults.indexWhere((e) => e.deviceId == result.deviceId); + int index = _bleDevices.indexWhere((e) => e.deviceId == result.deviceId); if (index == -1) { - _scanResults.add(result); + _bleDevices.add(result); } else { - if (result.name == null && _scanResults[index].name != null) { - result.name = _scanResults[index].name; + if (result.name == null && _bleDevices[index].name != null) { + result.name = _bleDevices[index].name; } - _scanResults[index] = result; + _bleDevices[index] = result; } setState(() {}); }; @@ -119,7 +119,7 @@ class _MyAppState extends State { text: 'Start Scan', onPressed: () async { setState(() { - _scanResults.clear(); + _bleDevices.clear(); _isScanning = true; }); try { @@ -171,7 +171,8 @@ class _MyAppState extends State { PlatformButton( text: 'Connected Devices', onPressed: () async { - var devices = await UniversalBle.getConnectedDevices( + List devices = + await UniversalBle.getSystemDevices( withServices: _services, ); if (devices.isEmpty) { @@ -182,8 +183,8 @@ class _MyAppState extends State { ); } setState(() { - _scanResults.clear(); - _scanResults.addAll(devices); + _bleDevices.clear(); + _bleDevices.addAll(devices); }); }, ), @@ -200,12 +201,12 @@ class _MyAppState extends State { }); }, ), - if (_scanResults.isNotEmpty) + if (_bleDevices.isNotEmpty) PlatformButton( text: 'Clear List', onPressed: () { setState(() { - _scanResults.clear(); + _bleDevices.clear(); }); }, ), @@ -225,25 +226,25 @@ class _MyAppState extends State { ), const Divider(color: Colors.blue), Expanded( - child: _isScanning && _scanResults.isEmpty + child: _isScanning && _bleDevices.isEmpty ? const Center(child: CircularProgressIndicator.adaptive()) - : !_isScanning && _scanResults.isEmpty + : !_isScanning && _bleDevices.isEmpty ? const ScannedDevicesPlaceholderWidget() : ListView.separated( - itemCount: _scanResults.length, + itemCount: _bleDevices.length, separatorBuilder: (context, index) => const Divider(), itemBuilder: (context, index) { - BleScanResult scanResult = - _scanResults[_scanResults.length - index - 1]; + BleDevice device = + _bleDevices[_bleDevices.length - index - 1]; return ScannedItemWidget( - scanResult: scanResult, + bleDevice: device, onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => PeripheralDetailPage( - scanResult.deviceId, - scanResult.name ?? "Unknown Peripheral", + device.deviceId, + device.name ?? "Unknown Peripheral", ), )); UniversalBle.stopScan(); diff --git a/example/lib/home/widgets/scanned_item_widget.dart b/example/lib/home/widgets/scanned_item_widget.dart index 0a3a319..31c0302 100644 --- a/example/lib/home/widgets/scanned_item_widget.dart +++ b/example/lib/home/widgets/scanned_item_widget.dart @@ -4,14 +4,14 @@ import 'package:universal_ble/universal_ble.dart'; import 'package:universal_ble_example/data/capabilities.dart'; class ScannedItemWidget extends StatelessWidget { - final BleScanResult scanResult; + final BleDevice bleDevice; final VoidCallback? onTap; - const ScannedItemWidget({super.key, required this.scanResult, this.onTap}); + const ScannedItemWidget({super.key, required this.bleDevice, this.onTap}); @override Widget build(BuildContext context) { - String? name = scanResult.name; - Uint8List? rawManufacturerData = scanResult.manufacturerData; + String? name = bleDevice.name; + Uint8List? rawManufacturerData = bleDevice.manufacturerData; ManufacturerData? manufacturerData; if (rawManufacturerData != null && rawManufacturerData.isNotEmpty) { manufacturerData = ManufacturerData.fromData(rawManufacturerData); @@ -22,12 +22,12 @@ class ScannedItemWidget extends StatelessWidget { child: Card( child: ListTile( title: Text( - '$name (${scanResult.rssi})', + '$name (${bleDevice.rssi})', ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(scanResult.deviceId), + Text(bleDevice.deviceId), Visibility( visible: manufacturerData != null, child: Text( @@ -36,18 +36,15 @@ class ScannedItemWidget extends StatelessWidget { : 'ManufacturerCompanyId: ${manufacturerData?.companyIdRadix16}', ), ), - Visibility( - visible: scanResult.isPaired != null, - child: scanResult.isPaired == true - ? const Text( - "Paired", - style: TextStyle(color: Colors.green), - ) - : const Text( - "Not Paired", - style: TextStyle(color: Colors.red), - ), - ), + bleDevice.isPaired == true + ? const Text( + "Paired", + style: TextStyle(color: Colors.green), + ) + : const Text( + "Not Paired", + style: TextStyle(color: Colors.red), + ), ], ), trailing: const Icon(Icons.arrow_forward_ios), diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index 7708ef7..3f5bd5b 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -344,6 +344,17 @@ class _PeripheralDetailPageState extends State { enabled: isConnected, text: 'Discover Services', ), + PlatformButton( + onPressed: () async { + _addLog( + 'IsConnected', + await UniversalBle.isConnected( + widget.deviceId, + ), + ); + }, + text: 'IsConnected', + ), if (Capabilities.supportsRequestMtuApi) PlatformButton( enabled: isConnected, @@ -405,7 +416,7 @@ class _PeripheralDetailPageState extends State { if (Capabilities.supportsPairingApi) PlatformButton( onPressed: () async { - bool isPaired = await UniversalBle.isPaired( + bool? isPaired = await UniversalBle.isPaired( widget.deviceId); _addLog('IsPaired', isPaired); }, diff --git a/lib/src/models/availability_state.dart b/lib/src/models/availability_state.dart index 321fbbf..0e0f8e6 100644 --- a/lib/src/models/availability_state.dart +++ b/lib/src/models/availability_state.dart @@ -1,14 +1,12 @@ enum AvailabilityState { - unknown(0), - resetting(1), - unsupported(2), - unauthorized(3), - poweredOff(4), - poweredOn(5); + unknown, + resetting, + unsupported, + unauthorized, + poweredOff, + poweredOn; - final int value; - const AvailabilityState(this.value); + const AvailabilityState(); - factory AvailabilityState.parse(int value) => - AvailabilityState.values.firstWhere((element) => element.value == value); + factory AvailabilityState.parse(int index) => AvailabilityState.values[index]; } diff --git a/lib/src/models/ble_connection_state.dart b/lib/src/models/ble_connection_state.dart index bfb52f4..80d504e 100644 --- a/lib/src/models/ble_connection_state.dart +++ b/lib/src/models/ble_connection_state.dart @@ -1,10 +1,9 @@ enum BleConnectionState { - connected(0), - disconnected(1); + connected, + disconnected; - final int value; - const BleConnectionState(this.value); + const BleConnectionState(); - factory BleConnectionState.parse(int value) => - BleConnectionState.values.firstWhere((element) => element.value == value); + factory BleConnectionState.parse(int index) => + BleConnectionState.values[index]; } diff --git a/lib/src/models/ble_scan_result.dart b/lib/src/models/ble_device.dart similarity index 74% rename from lib/src/models/ble_scan_result.dart rename to lib/src/models/ble_device.dart index c7abc7c..5a01224 100644 --- a/lib/src/models/ble_scan_result.dart +++ b/lib/src/models/ble_device.dart @@ -1,22 +1,28 @@ import 'dart:typed_data'; -class BleScanResult { +import 'package:universal_ble/universal_ble.dart'; + +class BleDevice { String deviceId; String? name; + int? rssi; bool? isPaired; + List services; + bool? isSystemDevice; Uint8List? manufacturerDataHead; Uint8List? manufacturerData; - int? rssi; - List services; - BleScanResult({ - required this.name, + Future get connectionState async => await UniversalBle.isConnected(deviceId) ? BleConnectionState.connected : BleConnectionState.disconnected; + + BleDevice({ required this.deviceId, + required this.name, this.rssi, this.isPaired, + this.services = const [], + this.isSystemDevice, Uint8List? manufacturerData, Uint8List? manufacturerDataHead, - this.services = const [], }) { this.manufacturerDataHead = manufacturerDataHead ?? Uint8List.fromList([]); this.manufacturerData = manufacturerData ?? manufacturerDataHead; @@ -24,7 +30,7 @@ class BleScanResult { } /// Represents the manufacturer data of a BLE device. -/// Use [BleScanResult.manufacturerData] with [ManufacturerData.fromData] to create an instance of this class. +/// Use [BleDevice.manufacturerData] with [ManufacturerData.fromData] to create an instance of this class. class ManufacturerData { final int? companyId; final Uint8List? data; diff --git a/lib/src/models/ble_property.dart b/lib/src/models/ble_property.dart index 8d71192..df94343 100644 --- a/lib/src/models/ble_property.dart +++ b/lib/src/models/ble_property.dart @@ -1,22 +1,18 @@ enum BleInputProperty { - disabled(0), - notification(1), - indication(2); + disabled, + notification, + indication; - final int value; - const BleInputProperty(this.value); + const BleInputProperty(); - factory BleInputProperty.parse(int value) => - BleInputProperty.values.firstWhere((element) => element.value == value); + factory BleInputProperty.parse(int index) => BleInputProperty.values[index]; } enum BleOutputProperty { - withResponse(0), - withoutResponse(1); + withResponse, + withoutResponse; - final int value; - const BleOutputProperty(this.value); + const BleOutputProperty(); - factory BleOutputProperty.parse(int value) => - BleOutputProperty.values.firstWhere((element) => element.value == value); + factory BleOutputProperty.parse(int index) => BleOutputProperty.values[index]; } diff --git a/lib/src/models/ble_service.dart b/lib/src/models/ble_service.dart index a0a2fe8..33f1139 100644 --- a/lib/src/models/ble_service.dart +++ b/lib/src/models/ble_service.dart @@ -11,19 +11,17 @@ class BleCharacteristic { } enum CharacteristicProperty { - broadcast(0), - read(1), - writeWithoutResponse(2), - write(3), - notify(4), - indicate(5), - authenticatedSignedWrites(6), - extendedProperties(7); + broadcast, + read, + writeWithoutResponse, + write, + notify, + indicate, + authenticatedSignedWrites, + extendedProperties; - final int value; - const CharacteristicProperty(this.value); + const CharacteristicProperty(); - factory CharacteristicProperty.parse(int value) => - CharacteristicProperty.values - .firstWhere((element) => element.value == value); + factory CharacteristicProperty.parse(int index) => + CharacteristicProperty.values[index]; } diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart index ea74218..823bd2e 100644 --- a/lib/src/models/model_exports.dart +++ b/lib/src/models/model_exports.dart @@ -5,6 +5,6 @@ export 'package:universal_ble/src/models/ble_property.dart'; 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'; +export 'package:universal_ble/src/models/ble_device.dart'; diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index c3d63d0..590cfda 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:universal_ble/src/ble_command_queue.dart'; @@ -161,8 +162,9 @@ class UniversalBle { } /// Check if a device is paired - /// Pair commands are not supported on `Apple` and `Web` - static Future isPaired(String deviceId) async { + /// Returns null on `Apple` and `Web` + static Future isPaired(String deviceId) async { + if (kIsWeb || Platform.isIOS || Platform.isMacOS) return null; return await _bleCommandQueue.executeCommand( () => _platform.isPaired(deviceId), deviceId: deviceId, @@ -192,11 +194,18 @@ class UniversalBle { /// 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> getConnectedDevices({ + static Future> getSystemDevices({ List? withServices, }) async { return await _bleCommandQueue.executeCommand( - () => _platform.getConnectedDevices(withServices), + () => _platform.getSystemDevices(withServices), + ); + } + + /// Returns true if device is connected to the app + static Future isConnected(String deviceId) async { + return await _bleCommandQueue.executeCommand( + () => _platform.isConnected(deviceId), ); } @@ -224,8 +233,8 @@ class UniversalBle { _bleCommandQueue.onQueueUpdate = onQueueUpdate; /// Get scan results - static set onScanResult(OnScanResult? onScanResult) => - _platform.onScanResult = onScanResult; + static set onScanResult(OnScanResult? bleDevice) => + _platform.onScanResult = bleDevice; /// Get connection state changes static set onConnectionChanged(OnConnectionChanged? onConnectionChanged) => diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 911c0fc..447d609 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -84,6 +84,15 @@ class UniversalBleLinux extends UniversalBlePlatform { } } + @override + Future isConnected(String deviceId) async { + BlueZDevice? device = _devices[deviceId] ?? + _client.devices.cast().firstWhere( + (device) => device?.address == deviceId, + orElse: () => null); + return device?.connected ?? false; + } + @override Future connect(String deviceId, {Duration? connectionTimeout}) async { final device = _findDeviceById(deviceId); @@ -279,7 +288,7 @@ class UniversalBleLinux extends UniversalBlePlatform { } @override - Future> getConnectedDevices( + Future> getSystemDevices( List? withServices, ) async { List devices = @@ -297,7 +306,9 @@ class UniversalBleLinux extends UniversalBlePlatform { } }).toList(); } - return devices.map((device) => device.toBleScanResult()).toList(); + return devices + .map((device) => device.toBleDevice(isSystemDevice: true)) + .toList(); } AvailabilityState get _availabilityState { @@ -400,7 +411,7 @@ class UniversalBleLinux extends UniversalBlePlatform { } // Update scan results only if rssi is available - if (device.rssi != 0) updateScanResult(device.toBleScanResult()); + if (device.rssi != 0) updateScanResult(device.toBleDevice()); // Setup Cache _devices[device.address] = device; @@ -415,7 +426,7 @@ class UniversalBleLinux extends UniversalBlePlatform { for (final property in properties) { switch (property) { case BluezProperty.rssi: - updateScanResult(device.toBleScanResult()); + updateScanResult(device.toBleDevice()); break; case BluezProperty.connected: onConnectionChanged?.call( @@ -426,7 +437,7 @@ class UniversalBleLinux extends UniversalBlePlatform { ); break; case BluezProperty.manufacturerData: - updateScanResult(device.toBleScanResult()); + updateScanResult(device.toBleDevice()); break; case BluezProperty.paired: onPairingStateChange?.call(device.address, device.paired, null); @@ -591,14 +602,17 @@ extension BlueZDeviceExtension on BlueZDevice { } } - BleScanResult toBleScanResult() { - return BleScanResult( + BleDevice toBleDevice({ + bool? isSystemDevice, + }) { + return BleDevice( name: alias, deviceId: address, isPaired: paired, manufacturerData: manufacturerDataHead, manufacturerDataHead: manufacturerDataHead, rssi: rssi, + isSystemDevice: isSystemDevice, services: uuids.map((e) => e.toString()).toList(), ); } diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index 942bb97..355fac6 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -581,8 +581,8 @@ class UniversalBlePlatformChannel { } } - Future> getConnectedDevices(List withServices) async { - final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices$__pigeon_messageChannelSuffix'; + Future> getSystemDevices(List withServices) async { + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices$__pigeon_messageChannelSuffix'; final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, @@ -607,6 +607,33 @@ class UniversalBlePlatformChannel { return (__pigeon_replyList[0] as List?)!.cast(); } } + + Future isConnected(String deviceId) async { + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isConnected$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([deviceId]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { + throw PlatformException( + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], + ); + } else if (__pigeon_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (__pigeon_replyList[0] as bool?)!; + } + } } class _UniversalBleCallbackChannelCodec extends StandardMessageCodec { 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 d8b8f75..c1e5e23 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -40,6 +40,9 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { @override Future stopScan() => _channel.stopScan(); + @override + Future isConnected(String deviceId) => _channel.isConnected(deviceId); + @override Future connect(String deviceId, {Duration? connectionTimeout}) => _channel.connect(deviceId); @@ -64,7 +67,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { deviceId, service, characteristic, - bleInputProperty.value, + bleInputProperty.index, ); } @@ -86,7 +89,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { service, characteristic, value, - bleOutputProperty.value, + bleOutputProperty.index, ); } @@ -104,32 +107,32 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { Future unPair(String deviceId) => _channel.unPair(deviceId); @override - Future> getConnectedDevices( + Future> getSystemDevices( List? withServices, ) async { - var devices = await _channel.getConnectedDevices(withServices ?? []); - return List.from(devices - .map((e) => e?.toBleScanResult()) - .where((e) => e != null) - .toList()); + var devices = await _channel.getSystemDevices(withServices ?? []); + return List.from( + devices + .map((e) => e?.toBleDevice(isSystemDevice: true)) + .where((e) => e != null) + .toList(), + ); } /// To set listeners void _setupListeners() { - UniversalBleCallbackChannel.setUp( - _UniversalBleCallbackHandler( - scanResult: (BleScanResult scanResult) => updateScanResult(scanResult), - availabilityChange: (AvailabilityState state) => - onAvailabilityChange?.call(state), - connectionChanged: (String deviceId, BleConnectionState state) => - onConnectionChanged?.call(deviceId, state), - valueChanged: - (String deviceId, String characteristicId, Uint8List value) => - onValueChanged?.call(deviceId, characteristicId, value), - pairStateChange: (String deviceId, bool isPaired, String? error) => - onPairingStateChange?.call(deviceId, isPaired, error), - ), - ); + UniversalBleCallbackChannel.setUp(_UniversalBleCallbackHandler( + scanResult: (BleDevice bleDevice) => updateScanResult(bleDevice), + availabilityChange: (AvailabilityState state) => + onAvailabilityChange?.call(state), + connectionChanged: (String deviceId, BleConnectionState state) => + onConnectionChanged?.call(deviceId, state), + valueChanged: + (String deviceId, String characteristicId, Uint8List value) => + onValueChanged?.call(deviceId, characteristicId, value), + pairStateChange: (String deviceId, bool isPaired, String? error) => + onPairingStateChange?.call(deviceId, isPaired, error), + )); } } @@ -175,7 +178,7 @@ class _UniversalBleCallbackHandler extends UniversalBleCallbackChannel { @override void onScanResult(UniversalBleScanResult result) => - scanResult(result.toBleScanResult()); + scanResult(result.toBleDevice()); @override void onValueChanged( @@ -188,16 +191,19 @@ class _UniversalBleCallbackHandler extends UniversalBleCallbackChannel { } extension _UniversalBleScanResultExtension on UniversalBleScanResult { - BleScanResult toBleScanResult() { + BleDevice toBleDevice({ + bool? isSystemDevice, + }) { var mnfDataHead = manufacturerDataHead ?? Uint8List.fromList([]); var mnfData = manufacturerData ?? mnfDataHead; - return BleScanResult( + return BleDevice( name: name, deviceId: deviceId, - isPaired: isPaired, manufacturerData: mnfData, manufacturerDataHead: mnfDataHead, rssi: rssi, + isPaired: isPaired, + isSystemDevice: isSystemDevice, services: services ?.where((e) => e != null) .map((e) => UUID(e!).toString()) diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index 5c504d8..28a8c51 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -44,19 +44,21 @@ abstract class UniversalBlePlatform { Future unPair(String deviceId); - Future> getConnectedDevices( + Future isConnected(String deviceId); + + Future> getSystemDevices( List? withServices, ); - void updateScanResult(BleScanResult scanResult) { + void updateScanResult(BleDevice bleDevice) { // Filter by name ScanFilter? scanFilter = _scanFilter; if (scanFilter != null && scanFilter.withNamePrefix.isNotEmpty) { - if (scanResult.name == null || + if (bleDevice.name == null || !scanFilter.withNamePrefix - .any((e) => scanResult.name?.startsWith(e) == true)) return; + .any((e) => bleDevice.name?.startsWith(e) == true)) return; } - onScanResult?.call(scanResult); + onScanResult?.call(bleDevice); } OnAvailabilityChange? onAvailabilityChange; @@ -78,7 +80,7 @@ typedef OnConnectionChanged = void Function( typedef OnValueChanged = void Function( String deviceId, String characteristicId, Uint8List value); -typedef OnScanResult = void Function(BleScanResult scanResult); +typedef OnScanResult = void Function(BleDevice scanResult); typedef OnAvailabilityChange = void Function(AvailabilityState state); diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 4109ac8..c16fcc4 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -19,6 +19,13 @@ class UniversalBleWeb extends UniversalBlePlatform { final Map _connectedDeviceStreamList = {}; final Map _characteristicStreamList = {}; + @override + Future isConnected(String deviceId) async { + // TODO: Test this on Web (All platforms) + BluetoothDevice? device = _getDeviceById(deviceId); + return await device?.connected.first ?? false; + } + @override Future connect( String deviceId, { @@ -289,7 +296,7 @@ class UniversalBleWeb extends UniversalBlePlatform { } @override - Future> getConnectedDevices( + Future> getSystemDevices( List? withServices, ) { throw UnimplementedError(); @@ -351,12 +358,12 @@ class UniversalBleWeb extends UniversalBlePlatform { } extension _BluetoothDeviceExtension on BluetoothDevice { - BleScanResult toBleScanResult({ + BleDevice toBleScanResult({ int? rssi, UnmodifiableMapView? manufacturerDataMap, List services = const [], }) { - return BleScanResult( + return BleDevice( name: name, deviceId: id, manufacturerData: manufacturerDataMap?.toUint8List(), diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 6a6c5a7..336dbe3 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -73,9 +73,11 @@ abstract class UniversalBlePlatformChannel { void unPair(String deviceId); @async - List getConnectedDevices( + List getSystemDevices( List withServices, ); + + bool isConnected(String deviceId); } /// Native -> Flutter diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 5ab3c9b..68ec08c 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -897,7 +897,7 @@ void UniversalBlePlatformChannel::SetUp( } } { - BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices" + prepended_suffix, &GetCodec()); + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices" + prepended_suffix, &GetCodec()); if (api != nullptr) { channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { try { @@ -908,7 +908,7 @@ void UniversalBlePlatformChannel::SetUp( return; } const auto& with_services_arg = std::get(encodable_with_services_arg); - api->GetConnectedDevices(with_services_arg, [reply](ErrorOr&& output) { + api->GetSystemDevices(with_services_arg, [reply](ErrorOr&& output) { if (output.has_error()) { reply(WrapError(output.error())); return; @@ -925,6 +925,34 @@ void UniversalBlePlatformChannel::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isConnected" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + ErrorOr output = api->IsConnected(device_id_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } } EncodableValue UniversalBlePlatformChannel::WrapError(std::string_view error_message) { diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index 89f5b75..cf59743 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -310,9 +310,10 @@ class UniversalBlePlatformChannel { std::function reply)> result) = 0; virtual std::optional Pair(const std::string& device_id) = 0; virtual std::optional UnPair(const std::string& device_id) = 0; - virtual void GetConnectedDevices( + virtual void GetSystemDevices( const flutter::EncodableList& with_services, std::function reply)> result) = 0; + virtual ErrorOr IsConnected(const std::string& device_id) = 0; // The codec used by UniversalBlePlatformChannel. static const flutter::StandardMessageCodec& GetCodec(); diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 97c431a..5af46ae 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -164,6 +164,15 @@ namespace universal_ble } }; + ErrorOr UniversalBlePlugin::IsConnected(const std::string &device_id) + { + auto it = connectedDevices.find(_str_to_mac_address(device_id)); + if (it == connectedDevices.end()) + return false; + auto deviceAgent = *it->second; + return deviceAgent.device.ConnectionStatus() == BluetoothConnectionStatus::Connected; + } + std::optional UniversalBlePlugin::Connect(const std::string &device_id) { ConnectAsync(_str_to_mac_address(device_id)); @@ -463,7 +472,7 @@ namespace universal_ble } }; - void UniversalBlePlugin::GetConnectedDevices( + void UniversalBlePlugin::GetSystemDevices( const flutter::EncodableList &with_services, std::function reply)> result) { @@ -473,7 +482,7 @@ namespace universal_ble auto serviceId = std::get(item); with_services_str.push_back(serviceId); } - GetConnectedDevicesAsync(with_services_str, result); + GetSystemDevicesAsync(with_services_str, result); } /// Helper Methods @@ -1088,7 +1097,7 @@ namespace universal_ble } } - winrt::fire_and_forget UniversalBlePlugin::GetConnectedDevicesAsync( + winrt::fire_and_forget UniversalBlePlugin::GetSystemDevicesAsync( std::vector with_services, std::function reply)> result) { @@ -1143,7 +1152,7 @@ namespace universal_ble } catch (...) { - std::cout << "Unknown error GetConnectedDevicesAsync" << std::endl; + std::cout << "Unknown error GetSystemDevicesAsyncAsync" << std::endl; result(FlutterError("Unknown error")); } } diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 5bdd2e9..d7a4a9b 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -118,7 +118,7 @@ namespace universal_ble void GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args); AvailabilityState getAvailabilityStateFromRadio(RadioState radioState); std::string parsePairingFailError(Enumeration::DevicePairingResult result); - winrt::fire_and_forget GetConnectedDevicesAsync(std::vector with_services, + winrt::fire_and_forget GetSystemDevicesAsync(std::vector with_services, std::function reply)> result); winrt::fire_and_forget IsPairedAsync(std::string device_id, std::function reply)> result); winrt::fire_and_forget WriteAsync(GattCharacteristic characteristic, GattWriteOption writeOption, @@ -132,6 +132,7 @@ namespace universal_ble // UniversalBlePlatformChannel implementation. void GetBluetoothAvailabilityState(std::function reply)> result) override; void EnableBluetooth(std::function reply)> result) override; + ErrorOr IsConnected(const std::string& device_id) override; std::optional StartScan(const UniversalScanFilter *filter) override; std::optional StopScan() override; std::optional Connect(const std::string &device_id) override; @@ -166,7 +167,7 @@ namespace universal_ble std::function reply)> result) override; std::optional Pair(const std::string &device_id) override; std::optional UnPair(const std::string &device_id) override; - void GetConnectedDevices( + void GetSystemDevices( const flutter::EncodableList &with_services, std::function reply)> result); };