Wait for writeWithoutResponse completion (#166)

* Wait for writeWithoutResponse request completion on Android

* Wait for writeWithoutResponse request completion on Darwin

* Add WriteWithoutResponse button in example app

* Update changelog and bump version

* Apply suggestions from code review

Co-authored-by: Foti Dim <foti@navideck.com>

---------

Co-authored-by: Foti Dim <foti@navideck.com>
This commit is contained in:
Rohit Sangwan
2025-06-26 11:35:59 +05:30
committed by GitHub
parent 0008231bdf
commit d56851cbfb
7 changed files with 60 additions and 34 deletions
+3
View File
@@ -1,3 +1,6 @@
## 0.20.1
* Fix `writeWithoutResponse()` which was not properly waiting for completion on Android and Apple
## 0.20.0 ## 0.20.0
* Add new high level API. Services are auto-discovered. the BleDevice class offers convenient member methods and properties. * Add new high level API. Services are auto-discovered. the BleDevice class offers convenient member methods and properties.
* BREAKING CHANGE: `bleDevice.isPaired` is now `bleDevice.paired` * BREAKING CHANGE: `bleDevice.isPaired` is now `bleDevice.paired`
@@ -153,6 +153,9 @@ fun Int.parseGattErrorCode(): String? {
BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION -> "GATT_INSUFFICIENT_ENCRYPTION" BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION -> "GATT_INSUFFICIENT_ENCRYPTION"
BluetoothGatt.GATT_CONNECTION_CONGESTED -> "GATT_CONNECTION_CONGESTED" BluetoothGatt.GATT_CONNECTION_CONGESTED -> "GATT_CONNECTION_CONGESTED"
BluetoothGatt.GATT_FAILURE -> "GATT_FAILURE" BluetoothGatt.GATT_FAILURE -> "GATT_FAILURE"
BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED -> "ERROR_GATT_WRITE_NOT_ALLOWED"
BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY -> "ERROR_GATT_WRITE_REQUEST_BUSY"
BluetoothStatusCodes.FEATURE_NOT_CONFIGURED -> "FEATURE_NOT_CONFIGURED"
0x01 -> "GATT_INVALID_HANDLE" 0x01 -> "GATT_INVALID_HANDLE"
0x04 -> "GATT_INVALID_PDU" 0x04 -> "GATT_INVALID_PDU"
0x09 -> "GATT_PREPARE_QUEUE_FULL" 0x09 -> "GATT_PREPARE_QUEUE_FULL"
@@ -469,34 +469,39 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
} }
val writeFuture = WriteResultFuture(
gatt.device.address,
gattCharacteristic.uuid.toString(),
gattCharacteristic.service.uuid.toString(),
callback
)
// Wait for the result
writeResultFutureList.add(writeFuture)
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val writeResult = gatt.writeCharacteristic(gattCharacteristic, value, writeType) gatt.writeCharacteristic(gattCharacteristic, value, writeType)
writeResult == BluetoothGatt.GATT_SUCCESS
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
gattCharacteristic.value = value gattCharacteristic.value = value
gattCharacteristic.writeType = writeType gattCharacteristic.writeType = writeType
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
gatt.writeCharacteristic(gattCharacteristic) val status = gatt.writeCharacteristic(gattCharacteristic)
if (status) BluetoothGatt.GATT_SUCCESS else BluetoothGatt.GATT_FAILURE
} }
if (!result) { if (result != BluetoothGatt.GATT_SUCCESS) {
callback(Result.failure(FlutterError("Failed", "Failed to write", null))) writeResultFutureList.remove(writeFuture)
return callback(
} Result.failure(
FlutterError(
if (writeType == BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) { "WriteError",
// wait for the result "Failed to write: ${result.parseGattErrorCode()}",
writeResultFutureList.add( null
WriteResultFuture( )
gatt.device.address,
gattCharacteristic.uuid.toString(),
gattCharacteristic.service.uuid.toString(),
callback
) )
) )
} else {
callback(Result.success(Unit))
} }
} catch (e: FlutterError) { } catch (e: FlutterError) {
callback(Result.failure(e)) callback(Result.failure(e))
+15 -3
View File
@@ -36,6 +36,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
private var discoveredServicesProgressMap: [String: [UniversalBleService]] = [:] private var discoveredServicesProgressMap: [String: [UniversalBleService]] = [:]
private var characteristicReadFutures = [CharacteristicReadFuture]() private var characteristicReadFutures = [CharacteristicReadFuture]()
private var characteristicWriteFutures = [CharacteristicWriteFuture]() private var characteristicWriteFutures = [CharacteristicWriteFuture]()
private var characteristicWriteWithoutResponseFutures = [CharacteristicWriteFuture]()
private var characteristicNotifyFutures = [CharacteristicNotifyFuture]() private var characteristicNotifyFutures = [CharacteristicNotifyFuture]()
private var discoverServicesFutures = [DiscoverServicesFuture]() private var discoverServicesFutures = [DiscoverServicesFuture]()
@@ -263,11 +264,12 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
} }
peripheral.writeValue(value.data, for: gattCharacteristic, type: type) peripheral.writeValue(value.data, for: gattCharacteristic, type: type)
// Wait for future response
let future = CharacteristicWriteFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion)
if type == CBCharacteristicWriteType.withResponse { if type == CBCharacteristicWriteType.withResponse {
// Wait for future response characteristicWriteFutures.append(future)
characteristicWriteFutures.append(CharacteristicWriteFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion))
} else { } else {
completion(Result.success({}())) characteristicWriteWithoutResponseFutures.append(future)
} }
} }
@@ -412,6 +414,16 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral
} }
} }
public func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
characteristicWriteWithoutResponseFutures.removeAll { future in
if future.deviceId == peripheral.uuid.uuidString {
future.result(Result.success({}()))
return true
}
return false
}
}
public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
characteristicWriteFutures.removeAll { future in characteristicWriteFutures.removeAll { future in
if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr {
@@ -129,7 +129,7 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
} }
} }
Future<void> _writeValue() async { Future<void> _writeValue({required bool withResponse}) async {
BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic; BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic;
if (selectedCharacteristic == null || if (selectedCharacteristic == null ||
!valueFormKey.currentState!.validate() || !valueFormKey.currentState!.validate() ||
@@ -146,13 +146,8 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
} }
try { try {
await selectedCharacteristic.write( await selectedCharacteristic.write(value, withResponse: withResponse);
value, _addLog('Write${withResponse ? "" : "WithoutResponse"}', value);
withResponse: _hasSelectedCharacteristicProperty(
[CharacteristicProperty.writeWithoutResponse],
),
);
_addLog('Write', value);
} catch (e) { } catch (e) {
debugPrint(e.toString()); debugPrint(e.toString());
_addLog('WriteError', e); _addLog('WriteError', e);
@@ -377,11 +372,19 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
discoveredServices.isNotEmpty && discoveredServices.isNotEmpty &&
_hasSelectedCharacteristicProperty([ _hasSelectedCharacteristicProperty([
CharacteristicProperty.write, CharacteristicProperty.write,
CharacteristicProperty.writeWithoutResponse
]), ]),
onPressed: _writeValue, onPressed: () => _writeValue(withResponse: true),
text: 'Write', text: 'Write',
), ),
PlatformButton(
enabled: isConnected &&
discoveredServices.isNotEmpty &&
_hasSelectedCharacteristicProperty([
CharacteristicProperty.writeWithoutResponse,
]),
onPressed: () => _writeValue(withResponse: false),
text: 'WriteWithoutResponse',
),
PlatformButton( PlatformButton(
enabled: isConnected && enabled: isConnected &&
discoveredServices.isNotEmpty && discoveredServices.isNotEmpty &&
+2 -2
View File
@@ -20,9 +20,9 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin :path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin
SPEC CHECKSUMS: SPEC CHECKSUMS:
device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
universal_ble: ff19787898040d721109c6324472e5dd4bc86adc universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
+1 -1
View File
@@ -1,6 +1,6 @@
name: universal_ble name: universal_ble
description: A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE) plugin for Flutter description: A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE) plugin for Flutter
version: 0.20.0 version: 0.20.1
homepage: https://navideck.com homepage: https://navideck.com
repository: https://github.com/Navideck/universal_ble repository: https://github.com/Navideck/universal_ble
issue_tracker: https://github.com/Navideck/universal_ble/issues issue_tracker: https://github.com/Navideck/universal_ble/issues