Improve disconnect api (#192)
* Improve disconnect api * Get disconnection event on Web from callback * Wait for disconnection result on calling disconnect * Improve linux disconnection * Update Changelog * Fix Ai comments
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
* Unified error codes for all platforms
|
||||
* Add `isScanning` api
|
||||
* Add `requestPermissions` api and auto ask permission on `startScan`
|
||||
* `disconnect` now waits for disconnection confirmation before returning
|
||||
* Improve Windows disconnection event handling and cleanup
|
||||
|
||||
## 0.21.1
|
||||
* Fix device name resolution on Windows
|
||||
|
||||
@@ -98,7 +98,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun requestPermissions(withAndroidFineLocation: Boolean, callback: (Result<Unit>) -> Unit) {
|
||||
override fun requestPermissions(
|
||||
withAndroidFineLocation: Boolean,
|
||||
callback: (Result<Unit>) -> Unit,
|
||||
) {
|
||||
if (permissionHandler == null) {
|
||||
callback(
|
||||
Result.failure(
|
||||
@@ -243,21 +246,31 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
|
||||
}
|
||||
|
||||
override fun disconnect(deviceId: String) {
|
||||
cleanConnection(deviceId.toBluetoothGatt())
|
||||
val gatt = deviceId.findGatt()
|
||||
if (gatt == null) {
|
||||
mainThreadHandler?.post {
|
||||
callbackChannel?.onConnectionChanged(deviceId, false, null) {}
|
||||
}
|
||||
} else {
|
||||
cleanConnection(gatt)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getConnectionState(deviceId: String): Long {
|
||||
val connectionState = bluetoothManager.getConnectionState(
|
||||
bluetoothManager.adapter.getRemoteDevice(deviceId),
|
||||
BluetoothProfile.GATT
|
||||
)
|
||||
|
||||
return if (deviceId.isKnownGatt() || connectionState == BluetoothGatt.STATE_DISCONNECTED || connectionState == BluetoothGatt.STATE_DISCONNECTING) {
|
||||
connectionState.toBleConnectionState().value
|
||||
} else {
|
||||
// Might be connected with device, but not with app
|
||||
Log.e(TAG, "Device might be connected but not known to this app")
|
||||
BleConnectionState.Disconnected.value
|
||||
try {
|
||||
val connectionState = bluetoothManager.getConnectionState(
|
||||
bluetoothManager.adapter.getRemoteDevice(deviceId),
|
||||
BluetoothProfile.GATT
|
||||
)
|
||||
return if (deviceId.isKnownGatt() || connectionState == BluetoothGatt.STATE_DISCONNECTED || connectionState == BluetoothGatt.STATE_DISCONNECTING) {
|
||||
connectionState.toBleConnectionState().value
|
||||
} else {
|
||||
// Might be connected with device, but not with app
|
||||
Log.e(TAG, "Device might be connected but not known to this app")
|
||||
BleConnectionState.Disconnected.value
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return BleConnectionState.Disconnected.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,10 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
|
||||
}
|
||||
|
||||
func disconnect(deviceId: String) throws {
|
||||
let peripheral = try deviceId.getPeripheral(manager: manager)
|
||||
guard let peripheral = deviceId.findPeripheral(manager: manager) else {
|
||||
callbackChannel.onConnectionChanged(deviceId: deviceId, connected: false, error: nil) { _ in }
|
||||
return
|
||||
}
|
||||
if peripheral.state != CBPeripheralState.disconnected {
|
||||
manager.cancelPeripheralConnection(peripheral)
|
||||
}
|
||||
@@ -147,7 +150,9 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
|
||||
}
|
||||
|
||||
func getConnectionState(deviceId: String) throws -> Int64 {
|
||||
let peripheral = try deviceId.getPeripheral(manager: manager)
|
||||
guard let peripheral = deviceId.findPeripheral(manager: manager) else {
|
||||
return BlueConnectionState.disconnected.rawValue
|
||||
}
|
||||
switch peripheral.state {
|
||||
case .connecting:
|
||||
return BlueConnectionState.connecting.rawValue
|
||||
@@ -158,7 +163,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
|
||||
case .disconnected:
|
||||
return BlueConnectionState.disconnected.rawValue
|
||||
@unknown default:
|
||||
fatalError()
|
||||
return BlueConnectionState.disconnected.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -260,8 +260,16 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
|
||||
PlatformButton(
|
||||
text: 'Disconnect',
|
||||
enabled: isConnected,
|
||||
onPressed: () {
|
||||
bleDevice.disconnect();
|
||||
onPressed: () async {
|
||||
try {
|
||||
await bleDevice.disconnect();
|
||||
_addLog("DisconnectResult", true);
|
||||
} catch (e) {
|
||||
_addLog(
|
||||
'DisconnectError (${e.runtimeType})',
|
||||
e,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
+95
-39
@@ -120,42 +120,18 @@ class UniversalBle {
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
timeout ??= const Duration(seconds: 60);
|
||||
StreamSubscription? connectionSubscription;
|
||||
Completer<bool> completer = Completer();
|
||||
Completer<bool> completer =
|
||||
_connectionEventCompleter(deviceId, timeout: timeout);
|
||||
|
||||
void handleError(dynamic error) {
|
||||
if (completer.isCompleted) return;
|
||||
connectionSubscription?.cancel();
|
||||
completer.completeError(ConnectionException(error));
|
||||
}
|
||||
_platform.connect(deviceId, connectionTimeout: timeout).catchError(
|
||||
(error) {
|
||||
if (completer.isCompleted) return;
|
||||
completer.completeError(ConnectionException(error));
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
connectionSubscription = _platform
|
||||
.bleConnectionUpdateStreamController.stream
|
||||
.where((e) => e.deviceId == deviceId)
|
||||
.listen(
|
||||
(e) {
|
||||
if (e.error != null) {
|
||||
handleError(e.error);
|
||||
} else {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(e.isConnected);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: handleError,
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
_platform
|
||||
.connect(deviceId, connectionTimeout: timeout)
|
||||
.catchError(handleError);
|
||||
|
||||
if (!await completer.future.timeout(timeout)) {
|
||||
throw ConnectionException("Failed to connect");
|
||||
}
|
||||
} finally {
|
||||
connectionSubscription?.cancel();
|
||||
if (!await completer.future.timeout(timeout)) {
|
||||
throw ConnectionException("Failed to connect");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,11 +141,45 @@ class UniversalBle {
|
||||
String deviceId, {
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
return await _bleCommandQueue.queueCommand(
|
||||
() => _platform.disconnect(deviceId),
|
||||
timeout: timeout,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
timeout ??= const Duration(seconds: 60);
|
||||
BleConnectionState? connectionState;
|
||||
try {
|
||||
connectionState = await _platform.getConnectionState(deviceId);
|
||||
} catch (e) {
|
||||
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);
|
||||
|
||||
await _bleCommandQueue
|
||||
.queueCommand(() => _platform.disconnect(deviceId),
|
||||
timeout: timeout, deviceId: deviceId)
|
||||
.catchError(
|
||||
(error) {
|
||||
if (completer.isCompleted) return;
|
||||
completer.completeError(ConnectionException(error));
|
||||
},
|
||||
);
|
||||
|
||||
if (await completer.future.timeout(timeout)) {
|
||||
UniversalLogger.logError(
|
||||
"Device $deviceId is still connected after disconnect attempt",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
UniversalLogger.logError("Disconnect failed: $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// Discover services of a device.
|
||||
@@ -505,6 +515,52 @@ class UniversalBle {
|
||||
return read(deviceId, service, characteristic, timeout: timeout);
|
||||
}
|
||||
|
||||
static Completer<bool> _connectionEventCompleter(
|
||||
String deviceId, {
|
||||
Duration? timeout,
|
||||
}) {
|
||||
timeout ??= const Duration(seconds: 60);
|
||||
StreamSubscription? connectionSubscription;
|
||||
Completer<bool> completer = Completer();
|
||||
|
||||
void cancelSubscription() {
|
||||
connectionSubscription?.cancel();
|
||||
connectionSubscription = null;
|
||||
}
|
||||
|
||||
void handleError(dynamic error) {
|
||||
cancelSubscription();
|
||||
if (completer.isCompleted) return;
|
||||
completer.completeError(ConnectionException(error));
|
||||
}
|
||||
|
||||
connectionSubscription = _platform
|
||||
.bleConnectionUpdateStreamController.stream
|
||||
.where((e) => e.deviceId == deviceId)
|
||||
.listen(
|
||||
(e) {
|
||||
cancelSubscription();
|
||||
if (e.error != null) {
|
||||
handleError(e.error);
|
||||
} else {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(e.isConnected);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: handleError,
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
completer.future.timeout(timeout).then((_) {
|
||||
cancelSubscription();
|
||||
}).catchError((_) {
|
||||
cancelSubscription();
|
||||
});
|
||||
|
||||
return completer;
|
||||
}
|
||||
|
||||
static Future<void> _sendBleInputPropertyCommand(
|
||||
String deviceId,
|
||||
String service,
|
||||
|
||||
@@ -133,10 +133,7 @@ class UniversalBleLinux extends UniversalBlePlatform {
|
||||
|
||||
@override
|
||||
Future<BleConnectionState> getConnectionState(String deviceId) async {
|
||||
BlueZDevice? device = _devices[deviceId] ??
|
||||
_client.devices.cast<BlueZDevice?>().firstWhere(
|
||||
(device) => device?.address == deviceId,
|
||||
orElse: () => null);
|
||||
BlueZDevice? device = _getDeviceById(deviceId);
|
||||
bool connected = device?.connected ?? false;
|
||||
return connected
|
||||
? BleConnectionState.connected
|
||||
@@ -155,12 +152,11 @@ class UniversalBleLinux extends UniversalBlePlatform {
|
||||
|
||||
@override
|
||||
Future<void> disconnect(String deviceId) async {
|
||||
final device = _findDeviceById(deviceId);
|
||||
if (!device.connected) {
|
||||
updateConnection(deviceId, false);
|
||||
return;
|
||||
final device = _getDeviceById(deviceId);
|
||||
if (device?.connected == true) {
|
||||
await device?.disconnect();
|
||||
}
|
||||
await device.disconnect();
|
||||
updateConnection(deviceId, false);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -414,11 +410,10 @@ class UniversalBleLinux extends UniversalBlePlatform {
|
||||
: AvailabilityState.poweredOff;
|
||||
}
|
||||
|
||||
/// Find device by id from cache or from client
|
||||
/// Throws exception if device not found
|
||||
BlueZDevice _findDeviceById(String deviceId) {
|
||||
final device = _devices[deviceId] ??
|
||||
_client.devices.cast<BlueZDevice?>().firstWhere(
|
||||
(device) => device?.address == deviceId,
|
||||
orElse: () => null);
|
||||
final device = _getDeviceById(deviceId);
|
||||
if (device == null) {
|
||||
throw UniversalBleException(
|
||||
code: UniversalBleErrorCode.deviceNotFound,
|
||||
@@ -428,6 +423,14 @@ class UniversalBleLinux extends UniversalBlePlatform {
|
||||
return device;
|
||||
}
|
||||
|
||||
/// Get device by id from cache or from client
|
||||
BlueZDevice? _getDeviceById(String deviceId) {
|
||||
return _devices[deviceId] ??
|
||||
_client.devices.cast<BlueZDevice?>().firstWhere(
|
||||
(device) => device?.address == deviceId,
|
||||
orElse: () => null);
|
||||
}
|
||||
|
||||
Future<void> _ensureInitialized() async {
|
||||
if (isInitialized) return;
|
||||
|
||||
|
||||
@@ -58,8 +58,6 @@ class UniversalBleWeb extends UniversalBlePlatform {
|
||||
|
||||
@override
|
||||
Future<void> disconnect(String deviceId) async {
|
||||
_cleanConnection(deviceId);
|
||||
updateConnection(deviceId, false);
|
||||
_getDeviceById(deviceId)?.disconnect();
|
||||
}
|
||||
|
||||
|
||||
@@ -255,14 +255,16 @@ UniversalBlePlugin::Connect(const std::string &device_id) {
|
||||
std::optional<FlutterError>
|
||||
UniversalBlePlugin::Disconnect(const std::string &device_id) {
|
||||
auto device_address = str_to_mac_address(device_id);
|
||||
CleanConnection(device_address);
|
||||
// TODO: send disconnect event only after disconnect is complete
|
||||
ui_thread_handler_.Post([device_address] {
|
||||
callback_channel->OnConnectionChanged(mac_address_to_str(device_address),
|
||||
false, nullptr, SuccessCallback,
|
||||
ErrorCallback);
|
||||
});
|
||||
|
||||
const auto it = connected_devices_.find(device_address);
|
||||
if (it != connected_devices_.end()) {
|
||||
it->second->device.Close();
|
||||
DisposeServices(it->second);
|
||||
} else {
|
||||
ui_thread_handler_.Post([device_id] {
|
||||
callback_channel->OnConnectionChanged(device_id, false, nullptr,
|
||||
SuccessCallback, ErrorCallback);
|
||||
});
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -1033,18 +1035,22 @@ void UniversalBlePlugin::CleanConnection(const uint64_t bluetooth_address) {
|
||||
const auto device_agent = std::move(node.mapped());
|
||||
device_agent->device.ConnectionStatusChanged(
|
||||
device_agent->connection_status_changed_token);
|
||||
// Clean up all characteristics tokens
|
||||
for (auto &[service_id, service] : device_agent->gatt_map) {
|
||||
for (auto &[char_id, characteristic] : service.characteristics) {
|
||||
if (characteristic.subscription_token.has_value()) {
|
||||
characteristic.obj.ValueChanged(
|
||||
characteristic.subscription_token.value());
|
||||
characteristic.subscription_token = std::nullopt;
|
||||
DisposeServices(device_agent);
|
||||
}
|
||||
}
|
||||
|
||||
void UniversalBlePlugin::DisposeServices(const std::unique_ptr<BluetoothDeviceAgent> &device_agent)
|
||||
{
|
||||
for (auto& [service_id, service] : device_agent->gatt_map) {
|
||||
for (auto& [char_id, characteristic] : service.characteristics) {
|
||||
if (characteristic.subscription_token.has_value()) {
|
||||
characteristic.obj.ValueChanged(
|
||||
characteristic.subscription_token.value());
|
||||
characteristic.subscription_token = std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
device_agent->gatt_map.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fire_and_forget UniversalBlePlugin::GetSystemDevicesAsync(
|
||||
|
||||
@@ -147,6 +147,7 @@ namespace universal_ble
|
||||
void OnDeviceInfoReceived(const DeviceInformation& device_info);
|
||||
void BluetoothLeDeviceConnectionStatusChanged(const BluetoothLEDevice& sender, const IInspectable& args);
|
||||
void CleanConnection(uint64_t bluetooth_address);
|
||||
void DisposeServices(const std::unique_ptr<BluetoothDeviceAgent> &device_agent);
|
||||
|
||||
|
||||
void GattCharacteristicValueChanged(const GattCharacteristic& sender, const GattValueChangedEventArgs& args);
|
||||
|
||||
Reference in New Issue
Block a user