diff --git a/CHANGELOG.md b/CHANGELOG.md index 787c929..603443c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.2.0 +* Add `autoConnect` parameter to `connect()` method for automatic reconnection support on Android and iOS/macOS + ## 1.1.0 * Add readRssi method diff --git a/README.md b/README.md index 343e495..c7c55dc 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE | :---------------------------- | :-----: | :-: | :---: | :-----: | :---: | :-: | | startScan/stopScan | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | connect/disconnect | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| autoConnect | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | | getSystemDevices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | | discoverServices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | read | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | @@ -231,6 +232,13 @@ bool isConnected = await bleDevice.isConnected; BleConnectionState connectionState = await bleDevice.connectionState; ``` +#### Auto-connect +You can enable automatic reconnection by setting the `autoConnect` parameter to `true`. When enabled, the system will automatically attempt to reconnect to the device when it becomes available again. + +```dart +await bleDevice.connect(autoConnect: true); +``` + ### Discovering Services After establishing a connection, services need to be discovered. This method will discover all services and their characteristics. 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 08e4e98..0c9a6ce 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 @@ -517,7 +517,7 @@ interface UniversalBlePlatformChannel { fun startScan(filter: UniversalScanFilter?) fun stopScan() fun isScanning(): Boolean - fun connect(deviceId: String) + fun connect(deviceId: String, autoConnect: Boolean?) fun disconnect(deviceId: String) fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Long, callback: (Result) -> Unit) fun discoverServices(deviceId: String, withDescriptors: Boolean, callback: (Result>) -> Unit) @@ -686,8 +686,9 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val deviceIdArg = args[0] as String + val autoConnectArg = args[1] as Boolean? val wrapped: List = try { - api.connect(deviceIdArg) + api.connect(deviceIdArg, autoConnectArg) listOf(null) } catch (exception: Throwable) { UniversalBlePigeonUtils.wrapError(exception) 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 1682199..9c7647e 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -61,6 +61,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), private val subscriptionResultFutureList = mutableListOf() private val pairResultFutures = mutableMapOf) -> Unit>() private val rssiResultFutureList = mutableListOf() + private val autoConnectDevices = mutableSetOf() override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { UniversalBlePlatformChannel.setUp(flutterPluginBinding.binaryMessenger, this) @@ -213,7 +214,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return safeScanner.isScanning() } - override fun connect(deviceId: String) { + override fun connect(deviceId: String, autoConnect: Boolean?) { // If already connected, send connected message, // if connecting, do nothing deviceId.findGatt()?.let { @@ -232,24 +233,31 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - + val shouldAutoConnect = autoConnect ?: false + if (shouldAutoConnect) { + autoConnectDevices.add(deviceId) + } else { + autoConnectDevices.remove(deviceId) + } val remoteDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { remoteDevice.connectGatt( context, - false, + shouldAutoConnect, this, BluetoothDevice.TRANSPORT_LE ) } else { - remoteDevice.connectGatt(context, false, this) + remoteDevice.connectGatt(context, shouldAutoConnect, this) } gatt.saveCacheIfNeeded() } override fun disconnect(deviceId: String) { + autoConnectDevices.remove(deviceId) val gatt = deviceId.findGatt() if (gatt == null) { + cleanUpConnection(deviceId) mainThreadHandler?.post { callbackChannel?.onConnectionChanged(deviceId, false, null) {} } @@ -955,15 +963,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - private fun cleanConnection(gatt: BluetoothGatt) { - gatt.removeCache() - gatt.disconnect() + private fun cleanUpConnection(deviceId: String) { val deviceDisconnectedError: FlutterError = createFlutterError( UniversalBleErrorCode.DEVICE_DISCONNECTED, "Device Disconnected", ) readResultFutureList.removeAll { - if (it.deviceId == gatt.device.address) { + if (it.deviceId == deviceId) { it.result(Result.failure(deviceDisconnectedError)) true } else { @@ -971,7 +977,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } writeResultFutureList.removeAll { - if (it.deviceId == gatt.device.address) { + if (it.deviceId == deviceId) { it.result(Result.failure(deviceDisconnectedError)) true } else { @@ -979,7 +985,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } subscriptionResultFutureList.removeAll { - if (it.deviceId == gatt.device.address) { + if (it.deviceId == deviceId) { it.result(Result.failure(deviceDisconnectedError)) true } else { @@ -987,7 +993,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } mtuResultFutureList.removeAll { - if (it.deviceId == gatt.device.address) { + if (it.deviceId == deviceId) { it.result(Result.failure(deviceDisconnectedError)) true } else { @@ -995,7 +1001,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } discoverServicesFutureList.removeAll { - if (it.deviceId == gatt.device.address) { + if (it.deviceId == deviceId) { it.result(Result.failure(deviceDisconnectedError)) true } else { @@ -1003,7 +1009,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } rssiResultFutureList.removeAll { - if (it.deviceId == gatt.device.address) { + if (it.deviceId == deviceId) { it.result(Result.failure(deviceDisconnectedError)) true } else { @@ -1012,6 +1018,12 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } + private fun cleanConnection(gatt: BluetoothGatt) { + gatt.removeCache() + gatt.disconnect() + cleanUpConnection(gatt.device.address) + } + private fun onBondStateUpdate(deviceId: String, bonded: Boolean, error: String? = null) { val future = pairResultFutures.remove(deviceId) future?.let { it(Result.success(bonded)) } @@ -1134,14 +1146,27 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), ) {} } } else if (newState == BluetoothGatt.STATE_DISCONNECTED) { - cleanConnection(gatt) + val deviceId = gatt.device.address + val shouldAutoConnect = autoConnectDevices.contains(deviceId) + + // Always clean up internal state (futures, etc.) + cleanUpConnection(deviceId) + + // Send connection changed callback mainThreadHandler?.post { callbackChannel?.onConnectionChanged( - gatt.device.address, false, status.parseHciErrorCode() + deviceId, false, status.parseHciErrorCode() ) {} } - UniversalBleLogger.logDebug("Closing gatt for ${gatt.device.name}") - gatt.close() + + if (!shouldAutoConnect) { + // Only close GATT resources when autoConnect is disabled + gatt.removeCache() + gatt.disconnect() + UniversalBleLogger.logDebug("Closing gatt for ${gatt.device.name}") + gatt.close() + } + // When autoConnect is enabled, keep GATT open for Android to reconnect } } diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index bfeb176..19c91db 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -536,7 +536,7 @@ protocol UniversalBlePlatformChannel { func startScan(filter: UniversalScanFilter?) throws func stopScan() throws func isScanning() throws -> Bool - func connect(deviceId: String) throws + func connect(deviceId: String, autoConnect: Bool?) throws func disconnect(deviceId: String) throws func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64, completion: @escaping (Result) -> Void) func discoverServices(deviceId: String, withDescriptors: Bool, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) @@ -681,8 +681,9 @@ class UniversalBlePlatformChannelSetup { connectChannel.setMessageHandler { message, reply in let args = message as! [Any?] let deviceIdArg = args[0] as! String + let autoConnectArg: Bool? = nilOrValue(args[1]) do { - try api.connect(deviceId: deviceIdArg) + try api.connect(deviceId: deviceIdArg, autoConnect: autoConnectArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index b6cf0a2..477ace0 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -42,6 +42,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral private var discoverServicesFutures = [DiscoverServicesFuture]() private var rssiReadFutures = [RssiReadFuture]() private var isManageScanning = false + private var autoConnectDevices = Set() init(callbackChannel: UniversalBleCallbackChannel) { self.callbackChannel = callbackChannel @@ -129,15 +130,41 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral UniversalBleLogger.shared.setLogLevel(logLevel) } - func connect(deviceId: String) throws { + func connect(deviceId: String, autoConnect: Bool?) throws { let peripheral = try deviceId.getPeripheral(manager: manager) peripheral.delegate = self - manager.connect(peripheral) + let shouldAutoConnect = autoConnect ?? false + + if shouldAutoConnect { + autoConnectDevices.insert(deviceId) + if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { + let options: [String: Any] = [CBConnectPeripheralOptionEnableAutoReconnect: true] + manager.connect(peripheral, options: options) + } else { + // Auto-reconnect via CBConnectPeripheralOptionEnableAutoReconnect is only + // available on iOS 17.0 / macOS 14.0 / watchOS 10.0 / tvOS 17.0 and later. + // On earlier OS versions, enabling `autoConnect` will NOT provide automatic + // reconnection behavior. Any desired reconnection must be handled manually + // (e.g., in central manager delegate callbacks). + UniversalBleLogger.shared.logInfo( + "autoConnect requested for device \(deviceId), " + + "but automatic reconnection via CBConnectPeripheralOptionEnableAutoReconnect " + + "is only available on iOS 17+/macOS 14+/watchOS 10+/tvOS 17+. " + + "On this OS version, reconnections must be handled manually." + ) + manager.connect(peripheral) + } + } else { + autoConnectDevices.remove(deviceId) + manager.connect(peripheral) + } } func disconnect(deviceId: String) throws { + autoConnectDevices.remove(deviceId) guard let peripheral = deviceId.findPeripheral(manager: manager) else { callbackChannel.onConnectionChanged(deviceId: deviceId, connected: false, error: nil) { _ in } + cleanUpConnection(deviceId: deviceId) return } if peripheral.state != CBPeripheralState.disconnected { @@ -445,9 +472,33 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral callbackChannel.onConnectionChanged(deviceId: peripheral.uuid.uuidString, connected: true, error: nil) { _ in } } - public func centralManager(_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error _: Error?) { - callbackChannel.onConnectionChanged(deviceId: peripheral.uuid.uuidString, connected: false, error: nil) { _ in } - cleanUpConnection(deviceId: peripheral.uuid.uuidString) + private func handlePeripheralDisconnection(deviceId: String, error: Error?) { + autoConnectDevices.remove(deviceId) + callbackChannel.onConnectionChanged(deviceId: deviceId, connected: false, error: error?.localizedDescription) { _ in } + cleanUpConnection(deviceId: deviceId) + } + + public func centralManager( + _: CBCentralManager, + didDisconnectPeripheral peripheral: CBPeripheral, + timestamp: CFAbsoluteTime, + isReconnecting: Bool, + error: Error? + ) { + let deviceId = peripheral.uuid.uuidString + + if #available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) { + if isReconnecting { + return + } + } + + handlePeripheralDisconnection(deviceId: deviceId, error: error) + } + + public func centralManager(_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + let deviceId = peripheral.uuid.uuidString + handlePeripheralDisconnection(deviceId: deviceId, error: error) } public func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { diff --git a/example/CHANGELOG.md b/example/CHANGELOG.md new file mode 100644 index 0000000..f5487fd --- /dev/null +++ b/example/CHANGELOG.md @@ -0,0 +1,8 @@ +## 1.1.0 +* Add support for `autoConnect` parameter +* Display RSSI values in device details +* Persist filters +* Fix clear log button + +## 1.0.0 +* Initial release \ No newline at end of file diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index 076f81f..a00813e 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -52,7 +52,7 @@ class MockUniversalBle extends UniversalBlePlatform { } @override - Future connect(String deviceId, {Duration? connectionTimeout}) async { + Future connect(String deviceId, {bool autoConnect = false, Duration? connectionTimeout}) async { updateConnection(deviceId, true); _connectionStateMap[deviceId] = BleConnectionState.connected; } diff --git a/example/lib/home/widgets/drawer.dart b/example/lib/home/widgets/drawer.dart index 0b3e058..63c5236 100644 --- a/example/lib/home/widgets/drawer.dart +++ b/example/lib/home/widgets/drawer.dart @@ -77,7 +77,7 @@ class _AppDrawerState extends State { applicationName: 'Universal BLE', applicationVersion: "${snapshot.data?.version} (${snapshot.data?.buildNumber})", - applicationLegalese: '\u{a9} 2025 Navideck', + applicationLegalese: '\u{a9} 2023 Navideck', aboutBoxChildren: [ const SizedBox(height: 24), RichText( diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index 4bd056a..62ae5d1 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -32,6 +32,7 @@ class _PeripheralDetailPageState extends State { bool _isDeviceInfoExpanded = false; bool _isDeviceActionsExpanded = true; final Map _subscribedCharacteristics = {}; + bool _autoConnect = false; StreamSubscription? connectionStreamSubscription; StreamSubscription? pairingStateSubscription; @@ -688,59 +689,155 @@ class _PeripheralDetailPageState extends State { Widget _buildConnectDisconnectButton() { final colorScheme = Theme.of(context).colorScheme; - return SizedBox( - width: double.infinity, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: ElevatedButton.icon( - onPressed: () async { - if (isConnected) { - await _executeWithLoading( - () async { - await bleDevice.disconnect(); - _addLog("DisconnectResult", true); - }, - onError: (error) { - _addLog('DisconnectError', error); - }, - ); - } else { - await _executeWithLoading( - () async { - await bleDevice.connect(); - _addLog("ConnectionResult", true); - }, - onError: (error) { - _addLog( - 'ConnectError (${error.runtimeType})', - error, - ); - }, - ); - } - }, - icon: Icon( - isConnected ? Icons.bluetooth_disabled : Icons.bluetooth_connected, - size: 20, - ), - label: Text(isConnected ? 'Disconnect' : 'Connect'), - style: ElevatedButton.styleFrom( - backgroundColor: - isConnected ? colorScheme.error : colorScheme.primary, - foregroundColor: - isConnected ? colorScheme.onError : colorScheme.onPrimary, - padding: const EdgeInsets.symmetric( - vertical: 16, - ), + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // AutoConnect toggle + Card( + elevation: 1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - elevation: 2, + color: colorScheme.surfaceContainerHighest, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Row( + children: [ + Icon( + Icons.autorenew, + size: 18, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Auto Reconnect', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + Text( + 'Automatically reconnect when device becomes available', + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + Switch( + value: _autoConnect, + onChanged: (value) async { + // If toggling off auto-connect, disconnect to prevent unwanted reconnections + if (!value && _autoConnect) { + if (isConnected) { + // Device is connected, disconnect it to prevent auto-reconnect + await _executeWithLoading( + () async { + await bleDevice.disconnect(); + _addLog("DisconnectResult", + "Disconnected to disable auto-reconnect"); + }, + onError: (error) { + _addLog('DisconnectError', error); + }, + ); + } else { + // Device is already disconnected, but call disconnect() anyway + // to ensure cleanup and prevent any pending auto-reconnection attempts + await _executeWithLoading( + () async { + await bleDevice.disconnect(); + _addLog("DisconnectResult", + "Cleanup performed to prevent auto-reconnect"); + }, + onError: (error) { + _addLog('DisconnectError', error); + }, + ); + } + } + setState(() { + _autoConnect = value; + }); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], + ), + ), ), - ), + const SizedBox(height: 12), + // Connect/Disconnect button + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () async { + if (isConnected) { + await _executeWithLoading( + () async { + await bleDevice.disconnect(); + _addLog("DisconnectResult", true); + }, + onError: (error) { + _addLog('DisconnectError', error); + }, + ); + } else { + await _executeWithLoading( + () async { + await bleDevice.connect(autoConnect: _autoConnect); + _addLog( + "ConnectionResult", + "Connected${_autoConnect ? ' (Auto-reconnect enabled)' : ''}", + ); + }, + onError: (error) { + _addLog( + 'ConnectError (${error.runtimeType})', + error, + ); + }, + ); + } + }, + icon: Icon( + isConnected + ? Icons.bluetooth_disabled + : Icons.bluetooth_connected, + size: 20, + ), + label: Text(isConnected ? 'Disconnect' : 'Connect'), + style: ElevatedButton.styleFrom( + backgroundColor: + isConnected ? colorScheme.error : colorScheme.primary, + foregroundColor: + isConnected ? colorScheme.onError : colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + ), + ), + ], ), ); } diff --git a/example/lib/universal_ble_app.dart b/example/lib/universal_ble_app.dart index e2b5976..45ae809 100644 --- a/example/lib/universal_ble_app.dart +++ b/example/lib/universal_ble_app.dart @@ -9,7 +9,7 @@ import 'package:universal_ble_example/home/scanner_screen.dart'; Future initializeApp() async { WidgetsFlutterBinding.ensureInitialized(); await StorageService.instance.init(); - // await UniversalBle.setLogLevel(BleLogLevel.verbose); + await UniversalBle.setLogLevel(BleLogLevel.verbose); return await UniversalBle.hasPermissions( withAndroidFineLocation: false, ); diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig index fd7c4c2..96515de 100644 --- a/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = Universal BLE PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalble // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 com.navideck. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2023 com.navideck. All rights reserved. diff --git a/example/package_rename_config.yaml b/example/package_rename_config.yaml index 6d4b4b8..252744b 100644 --- a/example/package_rename_config.yaml +++ b/example/package_rename_config.yaml @@ -18,12 +18,12 @@ package_rename_config: macos: app_name: "Universal BLE" package_name: "com.navideck.universalble" - copyright_notice: "Copyright © 2025 com.navideck. All rights reserved." + copyright_notice: "Copyright © 2023 com.navideck. All rights reserved." windows: app_name: "Universal BLE" organization: "Navideck" - copyright_notice: "Copyright © 2025 com.navideck. All rights reserved." + copyright_notice: "Copyright © 2023 com.navideck. All rights reserved." exe_name: "universal_ble" web: diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc index 4e55b68..522cec7 100644 --- a/example/windows/runner/Runner.rc +++ b/example/windows/runner/Runner.rc @@ -93,7 +93,7 @@ BEGIN VALUE "FileDescription", "Universal BLE" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "Universal BLE" "\0" - VALUE "LegalCopyright", "Copyright © 2025 com.navideck. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright © 2023 com.navideck. All rights reserved." "\0" VALUE "OriginalFilename", "universal_ble.exe" "\0" VALUE "ProductName", "Universal BLE" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" diff --git a/lib/src/extensions/ble_device_extension.dart b/lib/src/extensions/ble_device_extension.dart index 753b721..11bda12 100644 --- a/lib/src/extensions/ble_device_extension.dart +++ b/lib/src/extensions/ble_device_extension.dart @@ -17,7 +17,9 @@ extension BleDeviceExtension on BleDevice { BleConnectionState.connected; /// Connects to the device. - Future connect() => UniversalBle.connect(deviceId); + /// [autoConnect] enables automatic reconnection when the device becomes available. + Future connect({bool autoConnect = false, Duration? timeout}) => + UniversalBle.connect(deviceId, autoConnect: autoConnect, timeout: timeout); /// Disconnects from the device. Future disconnect() => UniversalBle.disconnect(deviceId); diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index 6a65ee8..3daade8 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -134,16 +134,26 @@ class UniversalBle { /// It is advised to stop scanning before connecting. /// It throws error if device connection fails. /// Default connection timeout is 60 sec. + /// + /// [autoConnect] enables automatic reconnection when the device becomes available. + /// Default value is `false`. + /// Ignored on `Windows`, `Linux` and `Web`. + /// + /// Call [disconnect] to prevent auto-reconnect even while a device is disconnected. + /// /// Can throw `ConnectionException` or `PlatformException`. static Future connect( String deviceId, { Duration? timeout, + bool autoConnect = false, }) async { timeout ??= const Duration(seconds: 60); Completer completer = _connectionEventCompleter(deviceId, timeout: timeout); - _platform.connect(deviceId, connectionTimeout: timeout).catchError( + _platform + .connect(deviceId, connectionTimeout: timeout, autoConnect: autoConnect) + .catchError( (error) { if (completer.isCompleted) return; completer.completeError(ConnectionException(error)); @@ -169,15 +179,6 @@ class UniversalBle { UniversalLogger.logError("Get connection state failed: $e"); } - if (connectionState == BleConnectionState.disconnected || - connectionState == BleConnectionState.disconnecting) { - _platform.updateConnection(deviceId, false); - UniversalLogger.logInfo( - "Device $deviceId already disconnected: $connectionState", - ); - return; - } - try { Completer completer = _connectionEventCompleter(deviceId, timeout: timeout); @@ -192,6 +193,17 @@ class UniversalBle { }, ); + if (connectionState == BleConnectionState.disconnected || + connectionState == BleConnectionState.disconnecting) { + // Device was already disconnected, but we still called platform disconnect + // to prevent auto-reconnect. Update connection state and return. + _platform.updateConnection(deviceId, false); + UniversalLogger.logInfo( + "Device $deviceId already disconnected: $connectionState. Cleanup performed to prevent auto-reconnect.", + ); + return; + } + if (await completer.future.timeout(timeout)) { UniversalLogger.logError( "Device $deviceId is still connected after disconnect attempt", diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 6035dcf..8ec9627 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -141,7 +141,8 @@ class UniversalBleLinux extends UniversalBlePlatform { } @override - Future connect(String deviceId, {Duration? connectionTimeout}) async { + Future connect(String deviceId, {Duration? connectionTimeout, bool autoConnect = false}) async { + // Note: autoConnect is not directly supported on Linux platform final device = _findDeviceById(deviceId); if (device.connected) { updateConnection(deviceId, true); diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index fd17426..f450d0c 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -776,7 +776,7 @@ class UniversalBlePlatformChannel { } } - Future connect(String deviceId) async { + Future connect(String deviceId, {bool? autoConnect}) async { final pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( @@ -785,7 +785,7 @@ class UniversalBlePlatformChannel { binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = - pigeonVar_channel.send([deviceId]); + pigeonVar_channel.send([deviceId, autoConnect]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); 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 5029646..73629cd 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -69,8 +69,8 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { } @override - Future connect(String deviceId, {Duration? connectionTimeout}) => - _executeWithErrorHandling(() => _channel.connect(deviceId)); + Future connect(String deviceId, {Duration? connectionTimeout, bool autoConnect = false}) => + _executeWithErrorHandling(() => _channel.connect(deviceId, autoConnect: autoConnect)); @override Future disconnect(String deviceId) => diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index 68f930e..fbb14be 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -53,7 +53,7 @@ abstract class UniversalBlePlatform { Future isScanning(); - Future connect(String deviceId, {Duration? connectionTimeout}); + Future connect(String deviceId, {Duration? connectionTimeout, bool autoConnect = false}); Future disconnect(String deviceId); diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 73c4bb8..a436d3c 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -35,7 +35,9 @@ class UniversalBleWeb extends UniversalBlePlatform { Future connect( String deviceId, { Duration? connectionTimeout = const Duration(seconds: 10), + bool autoConnect = false, }) async { + // Note: autoConnect is not directly supported on Web platform var device = _getDeviceById(deviceId); if (device == null) { throw UniversalBleException( diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 9d389e6..2263a8f 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -41,7 +41,7 @@ abstract class UniversalBlePlatformChannel { bool isScanning(); - void connect(String deviceId); + void connect(String deviceId, {bool? autoConnect}); void disconnect(String deviceId); diff --git a/pubspec.yaml b/pubspec.yaml index a6ac4be..f00e1a2 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: 1.1.0 +version: 1.2.0 homepage: https://navideck.com repository: https://github.com/Navideck/universal_ble issue_tracker: https://github.com/Navideck/universal_ble/issues diff --git a/test/universal_ble_test_mock.dart b/test/universal_ble_test_mock.dart index 5eb8bc7..69bbb9c 100644 --- a/test/universal_ble_test_mock.dart +++ b/test/universal_ble_test_mock.dart @@ -3,7 +3,8 @@ import 'package:universal_ble/universal_ble.dart'; abstract class UniversalBlePlatformMock extends UniversalBlePlatform { @override - Future connect(String deviceId, {Duration? connectionTimeout}) { + Future connect(String deviceId, + {bool autoConnect = false, Duration? connectionTimeout}) { throw UnimplementedError(); } diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 40f6e3c..453231b 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -791,7 +791,9 @@ void UniversalBlePlatformChannel::SetUp( return; } const auto& device_id_arg = std::get(encodable_device_id_arg); - std::optional output = api->Connect(device_id_arg); + const auto& encodable_auto_connect_arg = args.at(1); + const auto* auto_connect_arg = std::get_if(&encodable_auto_connect_arg); + std::optional output = api->Connect(device_id_arg, auto_connect_arg); if (output.has_value()) { reply(WrapError(output.value())); return; diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index 92e5fdb..b504e70 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -396,7 +396,9 @@ class UniversalBlePlatformChannel { virtual std::optional StartScan(const UniversalScanFilter* filter) = 0; virtual std::optional StopScan() = 0; virtual ErrorOr IsScanning() = 0; - virtual std::optional Connect(const std::string& device_id) = 0; + virtual std::optional Connect( + const std::string& device_id, + const bool* auto_connect) = 0; virtual std::optional Disconnect(const std::string& device_id) = 0; virtual void SetNotifiable( const std::string& device_id, diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 523b9d8..07b95a5 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -261,7 +261,8 @@ UniversalBlePlugin::SetLogLevel(const UniversalBleLogLevel &log_level) { } std::optional -UniversalBlePlugin::Connect(const std::string &device_id) { +UniversalBlePlugin::Connect(const std::string &device_id, const bool *auto_connect) { + // Note: autoConnect is not directly supported on Windows platform ConnectAsync(str_to_mac_address(device_id)); return std::nullopt; }; diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 1e0ac42..28688a1 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -175,7 +175,7 @@ private: StartScan(const UniversalScanFilter *filter) override; std::optional StopScan() override; ErrorOr IsScanning() override; - std::optional Connect(const std::string &device_id) override; + std::optional Connect(const std::string &device_id, const bool *auto_connect) override; std::optional Disconnect(const std::string &device_id) override; ErrorOr HasPermissions(bool with_android_fine_location) override; void RequestPermissions(