From 90df6137ae726636d5a47aa99e809fc007cb4802 Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Mon, 7 Sep 2026 11:59:30 +0200 Subject: [PATCH] iOS/macOS: Handle write-without-response transmit buffer backpressure --- CHANGELOG.md | 1 + .../universal_ble/UniversalBleHelper.swift | 18 +++++++ .../universal_ble/UniversalBlePlugin.swift | 50 +++++++++++-------- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bbc2bbc..158ab360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Android: close the GATT client once the disconnect completes instead of right after `disconnect()`, and report the real disconnect status * Android: Fix peripheral `getReadinessState()` to check permissions and adapter power before advertising support, and throttle `startAdvertising` Bluetooth enable prompts to at most one dialog. * readRssi commands are not queued anymore +* iOS/macOS: Handle write-without-response transmit buffer backpressure ## 2.2.0 diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift index 64ac123d..ff14471a 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift @@ -246,6 +246,24 @@ class CharacteristicWriteFuture: DeviceFuture { } } +class PendingWriteWithoutResponse: DeviceFuture { + let deviceId: String + let characteristic: CBCharacteristic + let data: Data + let result: (Result) -> Void + + init(deviceId: String, characteristic: CBCharacteristic, data: Data, result: @escaping (Result) -> Void) { + self.deviceId = deviceId + self.characteristic = characteristic + self.data = data + self.result = result + } + + func fail(with error: Error) { + result(.failure(error)) + } +} + class CharacteristicNotifyFuture: DeviceFuture { let deviceId: String let characteristicId: String diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift index 1141b6e0..26657998 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift @@ -95,7 +95,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral private var activeServiceDiscoveries: [String: UniversalBleAsyncServiceDiscovery] = [:] private var characteristicReadFutures = [CharacteristicReadFuture]() private var characteristicWriteFutures = [CharacteristicWriteFuture]() - private var characteristicWriteWithoutResponseFutures = [CharacteristicWriteFuture]() + private var pendingWriteWithoutResponse = [PendingWriteWithoutResponse]() private var characteristicNotifyFutures = [CharacteristicNotifyFuture]() private var descriptorReadFutures = [DescriptorReadFuture]() private var descriptorWriteFutures = [DescriptorWriteFuture]() @@ -282,7 +282,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral characteristicReadFutures.failAndRemoveAll(matching: deviceId, with: error) characteristicWriteFutures.failAndRemoveAll(matching: deviceId, with: error) - characteristicWriteWithoutResponseFutures.failAndRemoveAll(matching: deviceId, with: error) + pendingWriteWithoutResponse.failAndRemoveAll(matching: deviceId, with: error) characteristicNotifyFutures.failAndRemoveAll(matching: deviceId, with: error) descriptorReadFutures.failAndRemoveAll(matching: deviceId, with: error) descriptorWriteFutures.failAndRemoveAll(matching: deviceId, with: error) @@ -402,27 +402,33 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral return } - let type = bleOutputProperty == .withoutResponse ? CBCharacteristicWriteType.withoutResponse : CBCharacteristicWriteType.withResponse - - if type == CBCharacteristicWriteType.withResponse { - if !gattCharacteristic.properties.contains(.write) { + switch bleOutputProperty { + case .withResponse: + guard gattCharacteristic.properties.contains(.write) else { completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportWrite, message: "Characteristic does not support write withResponse"))) return } - } else if type == CBCharacteristicWriteType.withoutResponse { - if !gattCharacteristic.properties.contains(.writeWithoutResponse) { + characteristicWriteFutures.append(CharacteristicWriteFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion)) + peripheral.writeValue(value.data, for: gattCharacteristic, type: .withResponse) + case .withoutResponse: + guard gattCharacteristic.properties.contains(.writeWithoutResponse) else { completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportWriteWithoutResponse, message: "Characteristic does not support write withoutResponse"))) return } - } - 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 { - characteristicWriteFutures.append(future) - } else { - characteristicWriteWithoutResponseFutures.append(future) + if peripheral.canSendWriteWithoutResponse { + peripheral.writeValue(value.data, for: gattCharacteristic, type: .withoutResponse) + completion(Result.success(())) + } else { + pendingWriteWithoutResponse.append( + PendingWriteWithoutResponse( + deviceId: deviceId, + characteristic: gattCharacteristic, + data: value.data, + result: completion + ) + ) + } } } @@ -693,12 +699,14 @@ 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 + let deviceId = peripheral.uuid.uuidString + while peripheral.state == .connected && peripheral.canSendWriteWithoutResponse { + guard let index = pendingWriteWithoutResponse.firstIndex(where: { $0.deviceId == deviceId }) else { + return } - return false + let pending = pendingWriteWithoutResponse.remove(at: index) + peripheral.writeValue(pending.data, for: pending.characteristic, type: .withoutResponse) + pending.result(Result.success(())) } }