Add auto-connect support (#206)

* Add autoConnect support

* Enhance autoConnect functionality for Bluetooth devices

- Added support for managing a set of auto-connect devices in both Android and iOS implementations.
- Updated connection and disconnection logic to handle auto-reconnect scenarios appropriately.
- Improved cleanup processes to prevent unwanted reconnections when devices are manually disconnected.

* Refactor connection button and add auto-reconnect toggle

- Updated the connection button layout for better UI consistency.
- Introduced an auto-reconnect toggle to manage automatic reconnections when devices become available.
- Enhanced connection and disconnection logic to respect the auto-connect setting.

* Update version to 1.2.0 and add CHANGELOG for new features

- Bumped version to 1.2.0.
- Added CHANGELOG.md detailing new features including support for `autoConnect` and UI updates for automatic reconnection.

* Improve readme

* Refactor variable naming for clarity in Bluetooth connection logic

- Changed variable name from `isAutoConnect` to `shouldAutoConnect` for better readability and understanding of its purpose in the connection state handling.

* Refactor connection handling in UniversalBlePlugin

- Simplified connection logic by always cleaning up internal state before sending connection change callbacks.
- Ensured GATT resources are only closed when autoConnect is disabled, allowing for better management of Bluetooth connections.

* Refactor disconnection handling in UniversalBlePlugin

- Introduced a new method `handlePeripheralDisconnection` to encapsulate disconnection logic, improving code readability and maintainability.
- Updated the `centralManager` methods to utilize the new disconnection handler, ensuring consistent behavior across connection state changes.

* Update darwin/Classes/UniversalBlePlugin.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Foti Dim <fotios.dimanidis@a2zebra.de>
Co-authored-by: Navideck Labs <130186950+navidecklabs@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Foti Dim
2026-01-16 07:06:24 +01:00
committed by GitHub
parent a30e84a70c
commit 8557a46d05
28 changed files with 323 additions and 106 deletions
+3
View File
@@ -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
+8
View File
@@ -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.
@@ -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>) -> Unit)
fun discoverServices(deviceId: String, withDescriptors: Boolean, callback: (Result<List<UniversalBleService>>) -> Unit)
@@ -686,8 +686,9 @@ interface UniversalBlePlatformChannel {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val deviceIdArg = args[0] as String
val autoConnectArg = args[1] as Boolean?
val wrapped: List<Any?> = try {
api.connect(deviceIdArg)
api.connect(deviceIdArg, autoConnectArg)
listOf(null)
} catch (exception: Throwable) {
UniversalBlePigeonUtils.wrapError(exception)
@@ -61,6 +61,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
private val subscriptionResultFutureList = mutableListOf<SubscriptionResultFuture>()
private val pairResultFutures = mutableMapOf<String, (Result<Boolean>) -> Unit>()
private val rssiResultFutureList = mutableListOf<RssiResultFuture>()
private val autoConnectDevices = mutableSetOf<String>()
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
}
}
+3 -2
View File
@@ -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, Error>) -> 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))
+56 -5
View File
@@ -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<String>()
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?) {
+8
View File
@@ -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
+1 -1
View File
@@ -52,7 +52,7 @@ class MockUniversalBle extends UniversalBlePlatform {
}
@override
Future<void> connect(String deviceId, {Duration? connectionTimeout}) async {
Future<void> connect(String deviceId, {bool autoConnect = false, Duration? connectionTimeout}) async {
updateConnection(deviceId, true);
_connectionStateMap[deviceId] = BleConnectionState.connected;
}
+1 -1
View File
@@ -77,7 +77,7 @@ class _AppDrawerState extends State<AppDrawer> {
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(
@@ -32,6 +32,7 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
bool _isDeviceInfoExpanded = false;
bool _isDeviceActionsExpanded = true;
final Map<String, bool> _subscribedCharacteristics = {};
bool _autoConnect = false;
StreamSubscription? connectionStreamSubscription;
StreamSubscription? pairingStateSubscription;
@@ -688,59 +689,155 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
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,
),
),
),
],
),
);
}
+1 -1
View File
@@ -9,7 +9,7 @@ import 'package:universal_ble_example/home/scanner_screen.dart';
Future<bool> initializeApp() async {
WidgetsFlutterBinding.ensureInitialized();
await StorageService.instance.init();
// await UniversalBle.setLogLevel(BleLogLevel.verbose);
await UniversalBle.setLogLevel(BleLogLevel.verbose);
return await UniversalBle.hasPermissions(
withAndroidFineLocation: false,
);
@@ -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.
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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"
+3 -1
View File
@@ -17,7 +17,9 @@ extension BleDeviceExtension on BleDevice {
BleConnectionState.connected;
/// Connects to the device.
Future<void> connect() => UniversalBle.connect(deviceId);
/// [autoConnect] enables automatic reconnection when the device becomes available.
Future<void> connect({bool autoConnect = false, Duration? timeout}) =>
UniversalBle.connect(deviceId, autoConnect: autoConnect, timeout: timeout);
/// Disconnects from the device.
Future<void> disconnect() => UniversalBle.disconnect(deviceId);
+22 -10
View File
@@ -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<void> connect(
String deviceId, {
Duration? timeout,
bool autoConnect = false,
}) async {
timeout ??= const Duration(seconds: 60);
Completer<bool> 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<bool> 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",
@@ -141,7 +141,8 @@ class UniversalBleLinux extends UniversalBlePlatform {
}
@override
Future<void> connect(String deviceId, {Duration? connectionTimeout}) async {
Future<void> 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);
@@ -776,7 +776,7 @@ class UniversalBlePlatformChannel {
}
}
Future<void> connect(String deviceId) async {
Future<void> connect(String deviceId, {bool? autoConnect}) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
@@ -785,7 +785,7 @@ class UniversalBlePlatformChannel {
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture =
pigeonVar_channel.send(<Object?>[deviceId]);
pigeonVar_channel.send(<Object?>[deviceId, autoConnect]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
@@ -69,8 +69,8 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
}
@override
Future<void> connect(String deviceId, {Duration? connectionTimeout}) =>
_executeWithErrorHandling(() => _channel.connect(deviceId));
Future<void> connect(String deviceId, {Duration? connectionTimeout, bool autoConnect = false}) =>
_executeWithErrorHandling(() => _channel.connect(deviceId, autoConnect: autoConnect));
@override
Future<void> disconnect(String deviceId) =>
@@ -53,7 +53,7 @@ abstract class UniversalBlePlatform {
Future<bool> isScanning();
Future<void> connect(String deviceId, {Duration? connectionTimeout});
Future<void> connect(String deviceId, {Duration? connectionTimeout, bool autoConnect = false});
Future<void> disconnect(String deviceId);
@@ -35,7 +35,9 @@ class UniversalBleWeb extends UniversalBlePlatform {
Future<void> 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(
+1 -1
View File
@@ -41,7 +41,7 @@ abstract class UniversalBlePlatformChannel {
bool isScanning();
void connect(String deviceId);
void connect(String deviceId, {bool? autoConnect});
void disconnect(String deviceId);
+1 -1
View File
@@ -1,6 +1,6 @@
name: universal_ble
description: A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE) plugin for Flutter
version: 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
+2 -1
View File
@@ -3,7 +3,8 @@ import 'package:universal_ble/universal_ble.dart';
abstract class UniversalBlePlatformMock extends UniversalBlePlatform {
@override
Future<void> connect(String deviceId, {Duration? connectionTimeout}) {
Future<void> connect(String deviceId,
{bool autoConnect = false, Duration? connectionTimeout}) {
throw UnimplementedError();
}
+3 -1
View File
@@ -791,7 +791,9 @@ void UniversalBlePlatformChannel::SetUp(
return;
}
const auto& device_id_arg = std::get<std::string>(encodable_device_id_arg);
std::optional<FlutterError> output = api->Connect(device_id_arg);
const auto& encodable_auto_connect_arg = args.at(1);
const auto* auto_connect_arg = std::get_if<bool>(&encodable_auto_connect_arg);
std::optional<FlutterError> output = api->Connect(device_id_arg, auto_connect_arg);
if (output.has_value()) {
reply(WrapError(output.value()));
return;
+3 -1
View File
@@ -396,7 +396,9 @@ class UniversalBlePlatformChannel {
virtual std::optional<FlutterError> StartScan(const UniversalScanFilter* filter) = 0;
virtual std::optional<FlutterError> StopScan() = 0;
virtual ErrorOr<bool> IsScanning() = 0;
virtual std::optional<FlutterError> Connect(const std::string& device_id) = 0;
virtual std::optional<FlutterError> Connect(
const std::string& device_id,
const bool* auto_connect) = 0;
virtual std::optional<FlutterError> Disconnect(const std::string& device_id) = 0;
virtual void SetNotifiable(
const std::string& device_id,
+2 -1
View File
@@ -261,7 +261,8 @@ UniversalBlePlugin::SetLogLevel(const UniversalBleLogLevel &log_level) {
}
std::optional<FlutterError>
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;
};
+1 -1
View File
@@ -175,7 +175,7 @@ private:
StartScan(const UniversalScanFilter *filter) override;
std::optional<FlutterError> StopScan() override;
ErrorOr<bool> IsScanning() override;
std::optional<FlutterError> Connect(const std::string &device_id) override;
std::optional<FlutterError> Connect(const std::string &device_id, const bool *auto_connect) override;
std::optional<FlutterError> Disconnect(const std::string &device_id) override;
ErrorOr<bool> HasPermissions(bool with_android_fine_location) override;
void RequestPermissions(