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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user