From 5fe774be3f94d1c1c0e35e05aa272325a15aee23 Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Fri, 16 Oct 2026 09:39:43 +0200 Subject: [PATCH 1/5] Migrate platform channels to Pigeon 29 Regenerate the Pigeon outputs with 29.0.10 (from 26.3.4). The generated Kotlin and Swift channels are now coroutine/async based, so the native plugins had to be adapted: - Android: bridge the previous callback implementations to the new suspend interface, deliver Dart callbacks from coroutines, and depend on kotlinx-coroutines-android. - Darwin: bridge the completion-based implementations to the new async protocol, deliver Dart callbacks with @MainActor Tasks, and answer peripheral read/write requests asynchronously. - Windows: only generated helpers (operator<<, PigeonInternalToString) were added; the abstract interfaces are unchanged, so no hand-written changes were needed. - Tests: adapt MtuCompletionTest and DisconnectCloseTest to the suspend API and provide a Main dispatcher on the JVM. No change to the public Dart API. --- CHANGELOG.md | 2 +- android/build.gradle | 4 + .../navideck/universal_ble/UniversalBle.g.kt | 766 +++++----- .../UniversalBlePeripheralPlugin.kt | 128 +- .../universal_ble/UniversalBlePlugin.kt | 193 ++- .../universal_ble/DisconnectCloseTest.kt | 17 + .../universal_ble/MtuCompletionTest.kt | 48 +- .../universal_ble/UniversalBle.g.swift | 1318 +++++++++-------- .../UniversalBlePeripheralPlugin.swift | 94 +- .../universal_ble/UniversalBlePlugin.swift | 214 ++- lib/src/universal_ble.g.dart | 196 ++- windows/src/generated/universal_ble.g.cpp | 673 ++++++++- windows/src/generated/universal_ble.g.h | 53 +- 13 files changed, 2563 insertions(+), 1143 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81a014cd..f7164d6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## 3.1.1 * Bump `bluez` to `^0.9.0`. -* Bump the `pigeon` dev dependency to `^29.0.0`. +* Regenerate the platform channels with Pigeon `^29.0.0` (up from 26.3.4). The generated Kotlin and Swift channels are now coroutine/`async` based, so the Android module depends on `kotlinx-coroutines-android` and the native central/peripheral plugins were adapted to the new interfaces. No change to the public Dart API. ## 3.1.0 * `BleDevice.services` is now normalised to full 128-bit lower-case UUIDs on every platform, matching the keys of `BleDevice.serviceData`. Apple was the outlier: it reported 16-bit advertised service UUIDs in their short form (`180D`), so the same field had a different shape per platform and stored keys or comparisons did not transfer between them. Compare against, or store, the 128-bit form; short (`180D`) and `0x`-prefixed forms are still accepted as *input*. Entries that are not valid UUIDs are kept verbatim rather than dropped. diff --git a/android/build.gradle b/android/build.gradle index 536f2b5e..cbe28cf1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -47,7 +47,11 @@ android { } dependencies { + // Required by the Pigeon-generated code (Dispatchers.Main) and the suspend + // bridges in the plugin implementation. + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' testImplementation 'org.mockito:mockito-core:5.12.0' } diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt index 0a412e37..88d990d8 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// Autogenerated from Pigeon (v29.0.10), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -13,6 +13,12 @@ import io.flutter.plugin.common.StandardMethodCodec import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException private object UniversalBlePigeonUtils { fun createConnectionError(channelName: String): FlutterError { @@ -548,6 +554,9 @@ data class UniversalBleScanResult ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.connectable) return result } + override fun toString(): String { + return "UniversalBleScanResult(deviceId=$deviceId, name=$name, isPaired=$isPaired, rssi=$rssi, manufacturerDataList=$manufacturerDataList, serviceData=$serviceData, services=$services, timestamp=$timestamp, timestampMicroseconds=$timestampMicroseconds, connectable=$connectable)" + } } /** @@ -592,6 +601,9 @@ data class UniversalDevicePickerResult ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.name) return result } + override fun toString(): String { + return "UniversalDevicePickerResult(deviceId=$deviceId, name=$name)" + } } /** @@ -634,6 +646,9 @@ data class UniversalBleService ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.characteristics) return result } + override fun toString(): String { + return "UniversalBleService(uuid=$uuid, characteristics=$characteristics)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -676,6 +691,9 @@ data class UniversalBleCharacteristic ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.descriptors) return result } + override fun toString(): String { + return "UniversalBleCharacteristic(uuid=$uuid, properties=$properties, descriptors=$descriptors)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -710,6 +728,9 @@ data class UniversalBleDescriptor ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.uuid) return result } + override fun toString(): String { + return "UniversalBleDescriptor(uuid=$uuid)" + } } /** @@ -767,6 +788,9 @@ data class BleConnectionParametersChange ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.status) return result } + override fun toString(): String { + return "BleConnectionParametersChange(deviceId=$deviceId, interval=$interval, latency=$latency, supervisionTimeout=$supervisionTimeout, status=$status)" + } } /** @@ -855,6 +879,9 @@ data class AndroidOptions ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.legacy) return result } + override fun toString(): String { + return "AndroidOptions(requestLocationPermission=$requestLocationPermission, scanMode=$scanMode, reportDelayMillis=$reportDelayMillis, callbackType=$callbackType, matchMode=$matchMode, numOfMatches=$numOfMatches, legacy=$legacy)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -899,6 +926,9 @@ data class UniversalScanConfig ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.skipDuplicates) return result } + override fun toString(): String { + return "UniversalScanConfig(android=$android, skipDuplicates=$skipDuplicates)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -950,6 +980,9 @@ data class UniversalScanFilter ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.withServiceData) return result } + override fun toString(): String { + return "UniversalScanFilter(withServices=$withServices, withNamePrefix=$withNamePrefix, withManufacturerData=$withManufacturerData, withServiceData=$withServiceData)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -999,6 +1032,9 @@ data class ManufacturerDataFilter ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.payloadMask) return result } + override fun toString(): String { + return "ManufacturerDataFilter(companyIdentifier=$companyIdentifier, payloadPrefix=${payloadPrefix?.contentToString()}, payloadMask=${payloadMask?.contentToString()})" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1037,6 +1073,9 @@ data class UniversalManufacturerData ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.data) return result } + override fun toString(): String { + return "UniversalManufacturerData(companyIdentifier=$companyIdentifier, data=${data.contentToString()})" + } } /** @@ -1107,6 +1146,9 @@ data class AppleConnectionOptions ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.notifyOnNotification) return result } + override fun toString(): String { + return "AppleConnectionOptions(notifyOnConnection=$notifyOnConnection, notifyOnDisconnection=$notifyOnDisconnection, notifyOnNotification=$notifyOnNotification)" + } } /** @@ -1172,6 +1214,9 @@ data class AppleAccessoryPickerItem ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.supportsBluetoothPairing) return result } + override fun toString(): String { + return "AppleAccessoryPickerItem(displayName=$displayName, imageAsset=$imageAsset, serviceUuid=$serviceUuid, nameSubstring=$nameSubstring, requiresImmediateRange=$requiresImmediateRange, supportsBluetoothPairing=$supportsBluetoothPairing)" + } } /** @@ -1212,6 +1257,9 @@ data class AppleAccessorySetupOptions ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.items) return result } + override fun toString(): String { + return "AppleAccessorySetupOptions(items=$items)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1255,6 +1303,9 @@ data class AndroidConnectionOptions ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.closeGattOnDetach) return result } + override fun toString(): String { + return "AndroidConnectionOptions(closeGattOnDetach=$closeGattOnDetach)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1293,6 +1344,9 @@ data class ConnectionPlatformConfig ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.android) return result } + override fun toString(): String { + return "ConnectionPlatformConfig(apple=$apple, android=$android)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1335,6 +1389,9 @@ data class AndroidDiscoverServicesOptions ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.clearGattCache) return result } + override fun toString(): String { + return "AndroidDiscoverServicesOptions(clearGattCache=$clearGattCache)" + } } /** @@ -1374,6 +1431,9 @@ data class DiscoverServicesPlatformConfig ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.android) return result } + override fun toString(): String { + return "DiscoverServicesPlatformConfig(android=$android)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1420,6 +1480,9 @@ data class PeripheralAndroidOptions ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.addServicesInScanResponse) return result } + override fun toString(): String { + return "PeripheralAndroidOptions(addManufacturerDataInScanResponse=$addManufacturerDataInScanResponse, addServicesInScanResponse=$addServicesInScanResponse)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1454,6 +1517,9 @@ data class PeripheralPlatformConfig ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.android) return result } + override fun toString(): String { + return "PeripheralPlatformConfig(android=$android)" + } } /** @@ -1500,6 +1566,9 @@ data class PeripheralService ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.characteristics) return result } + override fun toString(): String { + return "PeripheralService(uuid=$uuid, primary=$primary, characteristics=$characteristics)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1550,6 +1619,9 @@ data class PeripheralCharacteristic ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.value) return result } + override fun toString(): String { + return "PeripheralCharacteristic(uuid=$uuid, properties=$properties, permissions=$permissions, descriptors=$descriptors, value=${value?.contentToString()})" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1592,6 +1664,9 @@ data class PeripheralDescriptor ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.permissions) return result } + override fun toString(): String { + return "PeripheralDescriptor(uuid=$uuid, value=${value?.contentToString()}, permissions=$permissions)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1634,6 +1709,9 @@ data class PeripheralReadRequestResult ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.status) return result } + override fun toString(): String { + return "PeripheralReadRequestResult(value=${value.contentToString()}, offset=$offset, status=$status)" + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -1676,6 +1754,9 @@ data class PeripheralWriteRequestResult ( result = 31 * result + UniversalBlePigeonUtils.deepHash(this.status) return result } + override fun toString(): String { + return "PeripheralWriteRequestResult(value=${value?.contentToString()}, offset=$offset, status=$status)" + } } private open class UniversalBlePigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { @@ -2059,11 +2140,11 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { * Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface UniversalBlePlatformChannel { - fun getBluetoothAvailabilityState(callback: (Result) -> Unit) + suspend fun getBluetoothAvailabilityState(): AvailabilityState fun hasPermissions(withAndroidFineLocation: Boolean): Boolean - fun requestPermissions(withAndroidFineLocation: Boolean, callback: (Result) -> Unit) - fun enableBluetooth(callback: (Result) -> Unit) - fun disableBluetooth(callback: (Result) -> Unit) + suspend fun requestPermissions(withAndroidFineLocation: Boolean) + suspend fun enableBluetooth(): Boolean + suspend fun disableBluetooth(): Boolean fun startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) fun stopScan() fun isScanning(): Boolean @@ -2071,23 +2152,23 @@ interface UniversalBlePlatformChannel { * Shows the platform's native device picker (iOS 18+ AccessorySetupKit) and * returns the selected device. */ - fun requestDevice(options: AppleAccessorySetupOptions, callback: (Result) -> Unit) + suspend fun requestDevice(options: AppleAccessorySetupOptions): UniversalDevicePickerResult fun connect(deviceId: String, autoConnect: Boolean?, platformConfig: ConnectionPlatformConfig?) fun disconnect(deviceId: String) - fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, callback: (Result) -> Unit) - fun discoverServices(deviceId: String, withDescriptors: Boolean, platformConfig: DiscoverServicesPlatformConfig?, callback: (Result>) -> Unit) - fun readValue(deviceId: String, service: String, characteristic: String, callback: (Result) -> Unit) - fun readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, callback: (Result) -> Unit) - fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) - fun writeValue(deviceId: String, service: String, characteristic: String, value: ByteArray, bleOutputProperty: BleOutputProperty, callback: (Result) -> Unit) - fun writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: ByteArray, callback: (Result) -> Unit) - fun isPaired(deviceId: String, callback: (Result) -> Unit) - fun pair(deviceId: String, callback: (Result) -> Unit) - fun unPair(deviceId: String, callback: (Result) -> Unit) - fun getSystemDevices(withServices: List, callback: (Result>) -> Unit) + suspend fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty) + suspend fun discoverServices(deviceId: String, withDescriptors: Boolean, platformConfig: DiscoverServicesPlatformConfig?): List + suspend fun readValue(deviceId: String, service: String, characteristic: String): ByteArray + suspend fun readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String): ByteArray + suspend fun requestMtu(deviceId: String, expectedMtu: Long): Long + suspend fun writeValue(deviceId: String, service: String, characteristic: String, value: ByteArray, bleOutputProperty: BleOutputProperty) + suspend fun writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: ByteArray) + suspend fun isPaired(deviceId: String): Boolean + suspend fun pair(deviceId: String): Boolean + suspend fun unPair(deviceId: String) + suspend fun getSystemDevices(withServices: List): List fun getConnectionState(deviceId: String): BleConnectionState - fun readRssi(deviceId: String, callback: (Result) -> Unit) - fun requestConnectionPriority(deviceId: String, priority: BleConnectionPriority, callback: (Result) -> Unit) + suspend fun readRssi(deviceId: String): Long + suspend fun requestConnectionPriority(deviceId: String, priority: BleConnectionPriority) fun setLogLevel(logLevel: BleLogLevel) companion object { @@ -2103,14 +2184,13 @@ interface UniversalBlePlatformChannel { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - api.getBluetoothAvailabilityState{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.getBluetoothAvailabilityState()) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2140,13 +2220,14 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val withAndroidFineLocationArg = args[0] as Boolean - api.requestPermissions(withAndroidFineLocationArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - reply.reply(UniversalBlePigeonUtils.wrapResult(null)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + api.requestPermissions(withAndroidFineLocationArg) + listOf(null) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2157,14 +2238,13 @@ interface UniversalBlePlatformChannel { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - api.enableBluetooth{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.enableBluetooth()) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2175,14 +2255,13 @@ interface UniversalBlePlatformChannel { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - api.disableBluetooth{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.disableBluetooth()) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2245,14 +2324,13 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val optionsArg = args[0] as AppleAccessorySetupOptions - api.requestDevice(optionsArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.requestDevice(optionsArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2306,13 +2384,14 @@ interface UniversalBlePlatformChannel { val serviceArg = args[1] as String val characteristicArg = args[2] as String val bleInputPropertyArg = args[3] as BleInputProperty - api.setNotifiable(deviceIdArg, serviceArg, characteristicArg, bleInputPropertyArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - reply.reply(UniversalBlePigeonUtils.wrapResult(null)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + api.setNotifiable(deviceIdArg, serviceArg, characteristicArg, bleInputPropertyArg) + listOf(null) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2327,14 +2406,13 @@ interface UniversalBlePlatformChannel { val deviceIdArg = args[0] as String val withDescriptorsArg = args[1] as Boolean val platformConfigArg = args[2] as DiscoverServicesPlatformConfig? - api.discoverServices(deviceIdArg, withDescriptorsArg, platformConfigArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.discoverServices(deviceIdArg, withDescriptorsArg, platformConfigArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2349,14 +2427,13 @@ interface UniversalBlePlatformChannel { val deviceIdArg = args[0] as String val serviceArg = args[1] as String val characteristicArg = args[2] as String - api.readValue(deviceIdArg, serviceArg, characteristicArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.readValue(deviceIdArg, serviceArg, characteristicArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2372,14 +2449,13 @@ interface UniversalBlePlatformChannel { val serviceArg = args[1] as String val characteristicArg = args[2] as String val descriptorArg = args[3] as String - api.readDescriptorValue(deviceIdArg, serviceArg, characteristicArg, descriptorArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.readDescriptorValue(deviceIdArg, serviceArg, characteristicArg, descriptorArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2393,14 +2469,13 @@ interface UniversalBlePlatformChannel { val args = message as List val deviceIdArg = args[0] as String val expectedMtuArg = args[1] as Long - api.requestMtu(deviceIdArg, expectedMtuArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.requestMtu(deviceIdArg, expectedMtuArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2417,13 +2492,14 @@ interface UniversalBlePlatformChannel { val characteristicArg = args[2] as String val valueArg = args[3] as ByteArray val bleOutputPropertyArg = args[4] as BleOutputProperty - api.writeValue(deviceIdArg, serviceArg, characteristicArg, valueArg, bleOutputPropertyArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - reply.reply(UniversalBlePigeonUtils.wrapResult(null)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + api.writeValue(deviceIdArg, serviceArg, characteristicArg, valueArg, bleOutputPropertyArg) + listOf(null) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2440,13 +2516,14 @@ interface UniversalBlePlatformChannel { val characteristicArg = args[2] as String val descriptorArg = args[3] as String val valueArg = args[4] as ByteArray - api.writeDescriptorValue(deviceIdArg, serviceArg, characteristicArg, descriptorArg, valueArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - reply.reply(UniversalBlePigeonUtils.wrapResult(null)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + api.writeDescriptorValue(deviceIdArg, serviceArg, characteristicArg, descriptorArg, valueArg) + listOf(null) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2459,14 +2536,13 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val deviceIdArg = args[0] as String - api.isPaired(deviceIdArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.isPaired(deviceIdArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2479,14 +2555,13 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val deviceIdArg = args[0] as String - api.pair(deviceIdArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.pair(deviceIdArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2499,13 +2574,14 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val deviceIdArg = args[0] as String - api.unPair(deviceIdArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - reply.reply(UniversalBlePigeonUtils.wrapResult(null)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + api.unPair(deviceIdArg) + listOf(null) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2518,14 +2594,13 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val withServicesArg = args[0] as List - api.getSystemDevices(withServicesArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.getSystemDevices(withServicesArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2555,14 +2630,13 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val deviceIdArg = args[0] as String - api.readRssi(deviceIdArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.readRssi(deviceIdArg)) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2576,13 +2650,14 @@ interface UniversalBlePlatformChannel { val args = message as List val deviceIdArg = args[0] as String val priorityArg = args[1] as BleConnectionPriority - api.requestConnectionPriority(deviceIdArg, priorityArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - reply.reply(UniversalBlePigeonUtils.wrapResult(null)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + api.requestConnectionPriority(deviceIdArg, priorityArg) + listOf(null) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -2622,21 +2697,23 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger, UniversalBlePigeonCodec() } } - fun onAvailabilityChanged(stateArg: AvailabilityState, callback: (Result) -> Unit) + suspend fun onAvailabilityChanged(stateArg: AvailabilityState) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(stateArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(stateArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } /** @@ -2644,55 +2721,61 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger, * pairing state — so the Dart side can bypass its state de-duplication * instead of swallowing every failure after the first (#325). */ - fun onPairStateChange(deviceIdArg: String, isPairedArg: Boolean, isFailureArg: Boolean, callback: (Result) -> Unit) + suspend fun onPairStateChange(deviceIdArg: String, isPairedArg: Boolean, isFailureArg: Boolean) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, isPairedArg, isFailureArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, isPairedArg, isFailureArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onScanResult(resultArg: UniversalBleScanResult, callback: (Result) -> Unit) + suspend fun onScanResult(resultArg: UniversalBleScanResult) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(resultArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(resultArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onValueChanged(deviceIdArg: String, characteristicIdArg: String, valueArg: ByteArray, timestampArg: Long?, callback: (Result) -> Unit) + suspend fun onValueChanged(deviceIdArg: String, characteristicIdArg: String, valueArg: ByteArray, timestampArg: Long?) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, characteristicIdArg, valueArg, timestampArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, characteristicIdArg, valueArg, timestampArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } /** @@ -2702,38 +2785,42 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger, * `status` (Android). [nativeErrorCode] is that raw platform value. * Both are `null` when there is no error or the platform has no code. */ - fun onConnectionChanged(deviceIdArg: String, connectedArg: Boolean, errorArg: String?, errorCodeArg: UniversalBleErrorCode?, nativeErrorCodeArg: Long?, callback: (Result) -> Unit) + suspend fun onConnectionChanged(deviceIdArg: String, connectedArg: Boolean, errorArg: String?, errorCodeArg: UniversalBleErrorCode?, nativeErrorCodeArg: Long?) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, connectedArg, errorArg, errorCodeArg, nativeErrorCodeArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, connectedArg, errorArg, errorCodeArg, nativeErrorCodeArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onConnectionParametersUpdated(updateArg: BleConnectionParametersChange, callback: (Result) -> Unit) + suspend fun onConnectionParametersUpdated(updateArg: BleConnectionParametersChange) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionParametersUpdated$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(updateArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionParametersUpdated$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(updateArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } } @@ -2968,7 +3055,7 @@ interface UniversalBlePeripheralChannel { */ interface UniversalBleAndroidChannel { fun hasBluetoothAdvertisePermission(): Boolean - fun requestBluetoothAdvertisePermission(callback: (Result) -> Unit) + suspend fun requestBluetoothAdvertisePermission(): Boolean companion object { /** The codec used by UniversalBleAndroidChannel. */ @@ -2998,14 +3085,13 @@ interface UniversalBleAndroidChannel { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleAndroidChannel.requestBluetoothAdvertisePermission$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> - api.requestBluetoothAdvertisePermission{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(UniversalBlePigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(UniversalBlePigeonUtils.wrapResult(data)) + CoroutineScope(Dispatchers.Main).launch { + val wrapped: List = try { + listOf(api.requestBluetoothAdvertisePermission()) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) } + reply.reply(wrapped) } } } else { @@ -3027,161 +3113,179 @@ class UniversalBlePeripheralCallback(private val binaryMessenger: BinaryMessenge UniversalBlePigeonCodec() } } - fun onReadRequest(deviceIdArg: String, characteristicIdArg: String, offsetArg: Long, valueArg: ByteArray?, callback: (Result) -> Unit) + suspend fun onReadRequest(deviceIdArg: String, characteristicIdArg: String, offsetArg: Long, valueArg: ByteArray?): PeripheralReadRequestResult? { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onReadRequest$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, characteristicIdArg, offsetArg, valueArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onReadRequest$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, characteristicIdArg, offsetArg, valueArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + val output = it[0] as PeripheralReadRequestResult? + continuation.resume(output) + } } else { - val output = it[0] as PeripheralReadRequestResult? - callback(Result.success(output)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onWriteRequest(deviceIdArg: String, characteristicIdArg: String, offsetArg: Long, valueArg: ByteArray?, callback: (Result) -> Unit) + suspend fun onWriteRequest(deviceIdArg: String, characteristicIdArg: String, offsetArg: Long, valueArg: ByteArray?): PeripheralWriteRequestResult? { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onWriteRequest$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, characteristicIdArg, offsetArg, valueArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onWriteRequest$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, characteristicIdArg, offsetArg, valueArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + val output = it[0] as PeripheralWriteRequestResult? + continuation.resume(output) + } } else { - val output = it[0] as PeripheralWriteRequestResult? - callback(Result.success(output)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onDescriptorReadRequest(deviceIdArg: String, characteristicIdArg: String, descriptorIdArg: String, offsetArg: Long, valueArg: ByteArray?, callback: (Result) -> Unit) + suspend fun onDescriptorReadRequest(deviceIdArg: String, characteristicIdArg: String, descriptorIdArg: String, offsetArg: Long, valueArg: ByteArray?): PeripheralReadRequestResult? { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorReadRequest$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorReadRequest$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + val output = it[0] as PeripheralReadRequestResult? + continuation.resume(output) + } } else { - val output = it[0] as PeripheralReadRequestResult? - callback(Result.success(output)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onDescriptorWriteRequest(deviceIdArg: String, characteristicIdArg: String, descriptorIdArg: String, offsetArg: Long, valueArg: ByteArray?, callback: (Result) -> Unit) + suspend fun onDescriptorWriteRequest(deviceIdArg: String, characteristicIdArg: String, descriptorIdArg: String, offsetArg: Long, valueArg: ByteArray?): PeripheralWriteRequestResult? { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorWriteRequest$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorWriteRequest$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + val output = it[0] as PeripheralWriteRequestResult? + continuation.resume(output) + } } else { - val output = it[0] as PeripheralWriteRequestResult? - callback(Result.success(output)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onCharacteristicSubscriptionChange(deviceIdArg: String, characteristicIdArg: String, isSubscribedArg: Boolean, nameArg: String?, callback: (Result) -> Unit) + suspend fun onCharacteristicSubscriptionChange(deviceIdArg: String, characteristicIdArg: String, isSubscribedArg: Boolean, nameArg: String?) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onCharacteristicSubscriptionChange$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, characteristicIdArg, isSubscribedArg, nameArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onCharacteristicSubscriptionChange$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, characteristicIdArg, isSubscribedArg, nameArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onAdvertisingStateChange(stateArg: PeripheralAdvertisingState, errorArg: String?, callback: (Result) -> Unit) + suspend fun onAdvertisingStateChange(stateArg: PeripheralAdvertisingState, errorArg: String?) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onAdvertisingStateChange$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(stateArg, errorArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onAdvertisingStateChange$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(stateArg, errorArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onServiceAdded(serviceIdArg: String, errorArg: String?, callback: (Result) -> Unit) + suspend fun onServiceAdded(serviceIdArg: String, errorArg: String?) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onServiceAdded$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(serviceIdArg, errorArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onServiceAdded$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(serviceIdArg, errorArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onMtuChange(deviceIdArg: String, mtuArg: Long, callback: (Result) -> Unit) + suspend fun onMtuChange(deviceIdArg: String, mtuArg: Long) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onMtuChange$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, mtuArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onMtuChange$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, mtuArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } - fun onConnectionStateChange(deviceIdArg: String, connectedArg: Boolean, callback: (Result) -> Unit) + suspend fun onConnectionStateChange(deviceIdArg: String, connectedArg: Boolean) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onConnectionStateChange$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, connectedArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + return suspendCancellableCoroutine { continuation -> + val channelName = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onConnectionStateChange$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(deviceIdArg, connectedArg)) { + if (it is List<*>) { + if (it.size > 1) { + continuation.resumeWithException(FlutterError(it[0] as String, it[1] as String, it[2] as String?)) + } else { + continuation.resume(Unit) + } } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) - } + continuation.resumeWithException(UniversalBlePigeonUtils.createConnectionError(channelName)) + } + } } } } diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt index 9a2c0811..2e911748 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt @@ -26,6 +26,9 @@ import android.os.Handler import android.os.ParcelUuid import android.util.Log import io.flutter.plugin.common.BinaryMessenger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch private const val TAG = "UniversalBlePeripheral" private const val REQUEST_CODE_ENABLE_BLUETOOTH = 2342717 @@ -54,6 +57,17 @@ class UniversalBlePeripheralPlugin( this.activity = activity } + /** Fire-and-forget a Dart callback on the main dispatcher, ignoring failures. */ + private fun notify(block: suspend (UniversalBlePeripheralCallback) -> Unit) { + CoroutineScope(Dispatchers.Main).launch { + try { + block(callback) + } catch (_: Throwable) { + // Callback delivery failures are non-fatal. + } + } + } + fun dispose() { kotlin.runCatching { bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback) } restoreAdapterNameIfNeeded() @@ -165,35 +179,35 @@ class UniversalBlePeripheralPlugin( ) { if (!hasRequiredPermissions()) { advertisingState = PeripheralAdvertisingState.ERROR - callback.onAdvertisingStateChange( + notify { it.onAdvertisingStateChange( PeripheralAdvertisingState.ERROR, "Bluetooth permission not granted", - ) {} + ) } throw SecurityException("Bluetooth permission not granted") } val isEnabled = try { bluetoothManager.isBluetoothEnabled() } catch (e: SecurityException) { advertisingState = PeripheralAdvertisingState.ERROR - callback.onAdvertisingStateChange( + notify { it.onAdvertisingStateChange( PeripheralAdvertisingState.ERROR, "Bluetooth permission not granted", - ) {} + ) } throw e } if (!isEnabled) { advertisingState = PeripheralAdvertisingState.ERROR - callback.onAdvertisingStateChange( + notify { it.onAdvertisingStateChange( PeripheralAdvertisingState.ERROR, "Bluetooth is not enabled", - ) {} + ) } activity?.let { promptEnableBluetooth(it) } throw Exception("Bluetooth is not enabled") } isEnableBluetoothPromptPending = false ensurePeripheralInitialized() advertisingState = PeripheralAdvertisingState.STARTING - callback.onAdvertisingStateChange(PeripheralAdvertisingState.STARTING, null) {} + notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.STARTING, null) } handler.post { val adapter = bluetoothManager.adapter @@ -250,12 +264,12 @@ class UniversalBlePeripheralPlugin( return } advertisingState = PeripheralAdvertisingState.STOPPING - callback.onAdvertisingStateChange(PeripheralAdvertisingState.STOPPING, null) {} + notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.STOPPING, null) } handler.post { kotlin.runCatching { bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback) } restoreAdapterNameIfNeeded() advertisingState = PeripheralAdvertisingState.IDLE - callback.onAdvertisingStateChange(PeripheralAdvertisingState.IDLE, null) {} + notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.IDLE, null) } } } @@ -306,10 +320,10 @@ class UniversalBlePeripheralPlugin( private fun onConnectionUpdate(device: BluetoothDevice, status: Int, newState: Int) { Log.e(TAG, "onConnectionStateChange: $status -> $newState") handler.post { - callback.onConnectionStateChange( + notify { it.onConnectionStateChange( device.address, newState == BluetoothProfile.STATE_CONNECTED, - ) {} + ) } } if (newState == BluetoothProfile.STATE_DISCONNECTED) cleanConnection(device) } @@ -323,12 +337,12 @@ class UniversalBlePeripheralPlugin( } subscribedCharUUID.forEach { charUUID -> handler.post { - callback.onCharacteristicSubscriptionChange( + notify { it.onCharacteristicSubscriptionChange( deviceAddress, charUUID, false, device.name, - ) {} + ) } } } } @@ -347,7 +361,7 @@ class UniversalBlePeripheralPlugin( else -> "Failed to start advertising: $errorCode" } advertisingState = PeripheralAdvertisingState.ERROR - callback.onAdvertisingStateChange(PeripheralAdvertisingState.ERROR, errorMessage) {} + notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.ERROR, errorMessage) } } } @@ -355,7 +369,7 @@ class UniversalBlePeripheralPlugin( super.onStartSuccess(settingsInEffect) advertisingState = PeripheralAdvertisingState.ADVERTISING handler.post { - callback.onAdvertisingStateChange(PeripheralAdvertisingState.ADVERTISING, null) {} + notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.ADVERTISING, null) } } } } @@ -385,7 +399,7 @@ class UniversalBlePeripheralPlugin( super.onMtuChanged(device, mtu) device?.address?.let { address -> synchronized(mtuByDeviceId) { mtuByDeviceId[address] = mtu } - handler.post { callback.onMtuChange(address, mtu.toLong()) {} } + handler.post { notify { it.onMtuChange(address, mtu.toLong()) } } } } @@ -398,13 +412,17 @@ class UniversalBlePeripheralPlugin( super.onCharacteristicReadRequest(device, requestId, offset, characteristic) if (gattServer == null) return handler.post { - callback.onReadRequest( - deviceIdArg = device.address, - characteristicIdArg = characteristic.uuid.toString(), - offsetArg = offset.toLong(), - valueArg = characteristic.value, - ) { result -> - val readResult = result.getOrNull() + CoroutineScope(Dispatchers.Main).launch { + val readResult = try { + callback.onReadRequest( + deviceIdArg = device.address, + characteristicIdArg = characteristic.uuid.toString(), + offsetArg = offset.toLong(), + valueArg = characteristic.value, + ) + } catch (_: Throwable) { + null + } if (readResult == null) { gattServer?.sendResponse( device, requestId, BluetoothGatt.GATT_FAILURE, 0, emptyBytes, @@ -435,13 +453,17 @@ class UniversalBlePeripheralPlugin( device, requestId, characteristic, preparedWrite, responseNeeded, offset, value, ) handler.post { - callback.onWriteRequest( - deviceIdArg = device.address, - characteristicIdArg = characteristic.uuid.toString(), - offsetArg = offset.toLong(), - valueArg = value, - ) { writeResponse -> - val writeResult = writeResponse.getOrNull() + CoroutineScope(Dispatchers.Main).launch { + val writeResult = try { + callback.onWriteRequest( + deviceIdArg = device.address, + characteristicIdArg = characteristic.uuid.toString(), + offsetArg = offset.toLong(), + valueArg = value, + ) + } catch (_: Throwable) { + null + } if (responseNeeded) { gattServer?.sendResponse( device, @@ -458,7 +480,7 @@ class UniversalBlePeripheralPlugin( override fun onServiceAdded(status: Int, service: BluetoothGattService) { super.onServiceAdded(status, service) val error = if (status != BluetoothGatt.GATT_SUCCESS) "Adding Service failed" else null - handler.post { callback.onServiceAdded(service.uuid.toString(), error) {} } + handler.post { notify { it.onServiceAdded(service.uuid.toString(), error) } } } override fun onDescriptorReadRequest( @@ -469,14 +491,18 @@ class UniversalBlePeripheralPlugin( ) { super.onDescriptorReadRequest(device, requestId, offset, descriptor) handler.post { - callback.onDescriptorReadRequest( - deviceIdArg = device.address, - characteristicIdArg = descriptor.characteristic.uuid.toString(), - descriptorIdArg = descriptor.uuid.toString(), - offsetArg = offset.toLong(), - valueArg = descriptor.getCacheValue(), - ) { result -> - val readResult = result.getOrNull() + CoroutineScope(Dispatchers.Main).launch { + val readResult = try { + callback.onDescriptorReadRequest( + deviceIdArg = device.address, + characteristicIdArg = descriptor.characteristic.uuid.toString(), + descriptorIdArg = descriptor.uuid.toString(), + offsetArg = offset.toLong(), + valueArg = descriptor.getCacheValue(), + ) + } catch (_: Throwable) { + null + } if (readResult == null) { gattServer?.sendResponse( device, @@ -518,9 +544,9 @@ class UniversalBlePeripheralPlugin( val characteristicId = descriptor.characteristic.uuid.toString() device?.address?.let { address -> handler.post { - callback.onCharacteristicSubscriptionChange( + notify { it.onCharacteristicSubscriptionChange( address, characteristicId, isSubscribed, device.name, - ) {} + ) } } synchronized(subscribedCharDevicesMap) { val charSet = subscribedCharDevicesMap.getOrPut(address) { mutableSetOf() } @@ -534,14 +560,18 @@ class UniversalBlePeripheralPlugin( } if (responseNeeded) { handler.post { - callback.onDescriptorWriteRequest( - deviceIdArg = device?.address ?: "", - characteristicIdArg = descriptor.characteristic.uuid.toString(), - descriptorIdArg = descriptor.uuid.toString(), - offsetArg = offset.toLong(), - valueArg = value, - ) { writeResponse -> - val writeResult = writeResponse.getOrNull() + CoroutineScope(Dispatchers.Main).launch { + val writeResult = try { + callback.onDescriptorWriteRequest( + deviceIdArg = device?.address ?: "", + characteristicIdArg = descriptor.characteristic.uuid.toString(), + descriptorIdArg = descriptor.uuid.toString(), + offsetArg = offset.toLong(), + valueArg = value, + ) + } catch (_: Throwable) { + null + } gattServer?.sendResponse( device, requestId, diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index 4bea74ed..5a3fe6d0 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -33,6 +33,12 @@ import java.util.WeakHashMap import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import androidx.core.content.edit @SuppressLint("MissingPermission") @@ -147,7 +153,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), permissionHandler = null } - override fun getBluetoothAvailabilityState(callback: (Result) -> Unit) { + private fun getBluetoothAvailabilityStateInternal(callback: (Result) -> Unit) { callback( Result.success( bluetoothManager.adapter?.state?.toAvailabilityState() @@ -160,7 +166,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return permissionHandler?.hasPermissions(withAndroidFineLocation) ?: false } - override fun requestPermissions( + private fun requestPermissionsInternal( withAndroidFineLocation: Boolean, callback: (Result) -> Unit, ) { @@ -177,7 +183,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), permissionHandler?.requestPermissions(withAndroidFineLocation, callback) } - override fun enableBluetooth(callback: (Result) -> Unit) { + private fun enableBluetoothInternal(callback: (Result) -> Unit) { if (bluetoothManager.adapter.isEnabled) { callback(Result.success(true)) return @@ -198,7 +204,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), bluetoothEnableRequestFuture = callback } - override fun disableBluetooth(callback: (Result) -> Unit) { + private fun disableBluetoothInternal(callback: (Result) -> Unit) { if (!bluetoothManager.adapter.isEnabled) { callback(Result.success(true)) return @@ -308,7 +314,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return safeScanner.isScanning() } - override fun requestDevice( + private fun requestDeviceInternal( options: AppleAccessorySetupOptions, callback: (Result) -> Unit, ) { @@ -337,7 +343,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (currentState == BluetoothGatt.STATE_CONNECTED) { UniversalBleLogger.logError("$deviceId Already connected") mainThreadHandler?.post { - callbackChannel?.onConnectionChanged(deviceId, true, null, null, null) {} + notify { it.onConnectionChanged(deviceId, true, null, null, null) } } return } else if (currentState == BluetoothGatt.STATE_CONNECTING) { @@ -374,7 +380,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (gatt == null) { cleanUpConnection(deviceId) mainThreadHandler?.post { - callbackChannel?.onConnectionChanged(deviceId, false, null, null, null) {} + notify { it.onConnectionChanged(deviceId, false, null, null, null) } } } else { cleanConnection(gatt) @@ -407,7 +413,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return permissionHandler?.hasBluetoothAdvertisePermission() ?: false } - override fun requestBluetoothAdvertisePermission(callback: (Result) -> Unit) { + private fun requestBluetoothAdvertisePermissionInternal(callback: (Result) -> Unit) { val handler = permissionHandler if (handler == null) { callback( @@ -423,7 +429,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), handler.requestBluetoothAdvertisePermission(callback) } - override fun readRssi(deviceId: String, callback: (Result) -> Unit) { + private fun readRssiInternal(deviceId: String, callback: (Result) -> Unit) { try { val gatt = gattRegistry.get(deviceId) if (gatt.readRemoteRssi()) { @@ -466,7 +472,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - override fun discoverServices( + private fun discoverServicesInternal( deviceId: String, withDescriptors: Boolean, platformConfig: DiscoverServicesPlatformConfig?, @@ -552,7 +558,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } - override fun setNotifiable( + private fun setNotifiableInternal( deviceId: String, service: String, characteristic: String, @@ -659,7 +665,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - override fun readValue( + private fun readValueInternal( deviceId: String, service: String, characteristic: String, @@ -749,7 +755,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - override fun writeValue( + private fun writeValueInternal( deviceId: String, service: String, characteristic: String, @@ -907,7 +913,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - override fun readDescriptorValue( + private fun readDescriptorValueInternal( deviceId: String, service: String, characteristic: String, @@ -1022,7 +1028,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), onDescriptorRead(gatt, descriptor, status, descriptor.value ?: byteArrayOf()) } - override fun writeDescriptorValue( + private fun writeDescriptorValueInternal( deviceId: String, service: String, characteristic: String, @@ -1108,7 +1114,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - override fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) { + private fun requestMtuInternal(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) { UniversalBleLogger.logDebug("REQUEST_MTU -> $deviceId expected=$expectedMtu") val gatt = try { gattRegistry.get(deviceId) @@ -1177,7 +1183,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), for (future in pending) postMtuResult(future.result, result) } - override fun requestConnectionPriority( + private fun requestConnectionPriorityInternal( deviceId: String, priority: BleConnectionPriority, callback: (Result) -> Unit, @@ -1234,7 +1240,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), "onConnectionUpdated -> $deviceId interval=$interval latency=$latency timeout=$timeout status=$status" ) mainThreadHandler?.post { - callbackChannel?.onConnectionParametersUpdated( + notify { it.onConnectionParametersUpdated( BleConnectionParametersChange( deviceId = deviceId, interval = interval.toLong(), @@ -1242,7 +1248,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), supervisionTimeout = timeout.toLong(), status = status.toLong(), ) - ) {} + ) } } } @@ -1265,13 +1271,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), }) } - override fun isPaired(deviceId: String, callback: (Result) -> Unit) { + private fun isPairedInternal(deviceId: String, callback: (Result) -> Unit) { val remoteDevice: BluetoothDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) callback(Result.success(remoteDevice.isBonded())) } - override fun pair(deviceId: String, callback: (Result) -> Unit) { + private fun pairInternal(deviceId: String, callback: (Result) -> Unit) { try { val remoteDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) val pendingFuture = pairResultFutures.remove(deviceId) @@ -1319,7 +1325,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } - override fun unPair(deviceId: String, callback: (Result) -> Unit) { + private fun unPairInternal(deviceId: String, callback: (Result) -> Unit) { try { val remoteDevice: BluetoothDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) @@ -1330,7 +1336,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - override fun getSystemDevices( + private fun getSystemDevicesInternal( withServices: List, callback: (Result>) -> Unit, ) { @@ -1584,7 +1590,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), gatt.close() // A newer connect() owns this address now; its state is not ours to report. if (!gattRegistry.isKnown(deviceId)) { - callbackChannel?.onConnectionChanged(deviceId, false, null, null, null) {} + notify { it.onConnectionChanged(deviceId, false, null, null, null) } } } }, gattCloseTimeoutMs) @@ -1594,7 +1600,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), val future = pairResultFutures.remove(deviceId) future?.let { it(Result.success(bonded)) } mainThreadHandler?.post { - callbackChannel?.onPairStateChange(deviceId, bonded, isFailure) {} + notify { it.onPairStateChange(deviceId, bonded, isFailure) } } } @@ -1602,10 +1608,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), override fun onReceive(context: Context, intent: Intent) { if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) { mainThreadHandler?.post { - callbackChannel?.onAvailabilityChanged( + notify { it.onAvailabilityChanged( bluetoothManager.adapter?.state?.toAvailabilityState() ?: AvailabilityState.UNKNOWN - ) {} + ) } } } else if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) { val bondStateChange = intent.getBondStateChange() @@ -1680,7 +1686,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), mainThreadHandler?.post { - callbackChannel?.onScanResult( + notify { it.onScanResult( UniversalBleScanResult( name = name, deviceId = result.device.address, @@ -1693,7 +1699,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), timestampMicroseconds = timestampMicroseconds, connectable = result.isConnectable ) - ) {} + ) } } } @@ -1715,9 +1721,9 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (newState == BluetoothGatt.STATE_CONNECTED) { connectedGatts.add(gatt) mainThreadHandler?.post { - callbackChannel?.onConnectionChanged( + notify { it.onConnectionChanged( gatt.device.address, true, status.parseHciErrorCode(), status.toConnectionErrorCode(), status.toGattErrorCode() - ) {} + ) } } } else if (newState == BluetoothGatt.STATE_DISCONNECTED) { val deviceId = gatt.device.address @@ -1734,13 +1740,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), // Send connection changed callback mainThreadHandler?.post { - callbackChannel?.onConnectionChanged( + notify { it.onConnectionChanged( deviceId, false, status.parseHciErrorCode(), status.toConnectionErrorCode(wasConnected), status.toGattErrorCode(), - ) {} + ) } } } @@ -1764,12 +1770,12 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), "NOTIFY <- ${gatt.device.address} ${characteristic.uuid} len=${value.size}" ) mainThreadHandler?.post { - callbackChannel?.onValueChanged( + notify { it.onValueChanged( deviceIdArg = gatt.device.address, characteristicIdArg = characteristic.uuid.toString(), valueArg = value, timestampArg = System.currentTimeMillis() - ) {} + ) } } } @@ -1952,4 +1958,121 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return permissionHandler?.handlePermissionResult(requestCode, permissions, grantResults) ?: false } + + // --- Pigeon 29 suspend bridges ------------------------------------------------- + // The generated Pigeon 29 interface is suspend-based. The implementations below + // keep the previous callback-based logic (renamed to *Internal) and bridge it + // to coroutines here. + + private suspend fun awaitResult(block: ((Result) -> Unit) -> Unit): T = + suspendCancellableCoroutine { continuation -> + block { result -> + result.fold( + onSuccess = { continuation.resume(it) }, + onFailure = { continuation.resumeWithException(it) }, + ) + } + } + + /** Fire-and-forget a Dart callback on the main dispatcher, ignoring failures. */ + private fun notify(block: suspend (UniversalBleCallbackChannel) -> Unit) { + val channel = callbackChannel ?: return + CoroutineScope(Dispatchers.Main).launch { + try { + block(channel) + } catch (_: Throwable) { + // Callback delivery failures are non-fatal. + } + } + } + + override suspend fun getBluetoothAvailabilityState(): AvailabilityState = + awaitResult { getBluetoothAvailabilityStateInternal(it) } + + override suspend fun requestPermissions(withAndroidFineLocation: Boolean) = + awaitResult { requestPermissionsInternal(withAndroidFineLocation, it) } + + override suspend fun enableBluetooth(): Boolean = + awaitResult { enableBluetoothInternal(it) } + + override suspend fun disableBluetooth(): Boolean = + awaitResult { disableBluetoothInternal(it) } + + override suspend fun requestDevice(options: AppleAccessorySetupOptions): UniversalDevicePickerResult = + awaitResult { requestDeviceInternal(options, it) } + + override suspend fun setNotifiable( + deviceId: String, + service: String, + characteristic: String, + bleInputProperty: BleInputProperty, + ) = awaitResult { + setNotifiableInternal(deviceId, service, characteristic, bleInputProperty, it) + } + + override suspend fun discoverServices( + deviceId: String, + withDescriptors: Boolean, + platformConfig: DiscoverServicesPlatformConfig?, + ): List = + awaitResult { discoverServicesInternal(deviceId, withDescriptors, platformConfig, it) } + + override suspend fun readValue( + deviceId: String, + service: String, + characteristic: String, + ): ByteArray = awaitResult { readValueInternal(deviceId, service, characteristic, it) } + + override suspend fun readDescriptorValue( + deviceId: String, + service: String, + characteristic: String, + descriptor: String, + ): ByteArray = awaitResult { + readDescriptorValueInternal(deviceId, service, characteristic, descriptor, it) + } + + override suspend fun requestMtu(deviceId: String, expectedMtu: Long): Long = + awaitResult { requestMtuInternal(deviceId, expectedMtu, it) } + + override suspend fun writeValue( + deviceId: String, + service: String, + characteristic: String, + value: ByteArray, + bleOutputProperty: BleOutputProperty, + ) = awaitResult { + writeValueInternal(deviceId, service, characteristic, value, bleOutputProperty, it) + } + + override suspend fun writeDescriptorValue( + deviceId: String, + service: String, + characteristic: String, + descriptor: String, + value: ByteArray, + ) = awaitResult { + writeDescriptorValueInternal(deviceId, service, characteristic, descriptor, value, it) + } + + override suspend fun isPaired(deviceId: String): Boolean = + awaitResult { isPairedInternal(deviceId, it) } + + override suspend fun pair(deviceId: String): Boolean = + awaitResult { pairInternal(deviceId, it) } + + override suspend fun unPair(deviceId: String) = + awaitResult { unPairInternal(deviceId, it) } + + override suspend fun getSystemDevices(withServices: List): List = + awaitResult { getSystemDevicesInternal(withServices, it) } + + override suspend fun readRssi(deviceId: String): Long = + awaitResult { readRssiInternal(deviceId, it) } + + override suspend fun requestConnectionPriority(deviceId: String, priority: BleConnectionPriority) = + awaitResult { requestConnectionPriorityInternal(deviceId, priority, it) } + + override suspend fun requestBluetoothAdvertisePermission(): Boolean = + awaitResult { requestBluetoothAdvertisePermissionInternal(it) } } diff --git a/android/src/test/kotlin/com/navideck/universal_ble/DisconnectCloseTest.kt b/android/src/test/kotlin/com/navideck/universal_ble/DisconnectCloseTest.kt index 7522a336..ac143387 100644 --- a/android/src/test/kotlin/com/navideck/universal_ble/DisconnectCloseTest.kt +++ b/android/src/test/kotlin/com/navideck/universal_ble/DisconnectCloseTest.kt @@ -3,10 +3,15 @@ package com.navideck.universal_ble import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.os.Handler +import kotlin.test.AfterTest +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertSame import kotlin.test.assertTrue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain import org.mockito.ArgumentMatchers.any import org.mockito.Mockito @@ -20,6 +25,18 @@ import org.mockito.Mockito internal class DisconnectCloseTest { private val deviceAddress = "AA:BB:CC:DD:EE:FF" + // Callbacks are delivered through `Dispatchers.Main`; the JVM test dispatcher + // stands in for Android's main looper. + @BeforeTest + fun setUpMainDispatcher() { + Dispatchers.setMain(Dispatchers.Unconfined) + } + + @AfterTest + fun tearDownMainDispatcher() { + Dispatchers.resetMain() + } + // (deviceId, connected, error, errorCode, nativeErrorCode) of every onConnectionChanged // delivered to Dart. private val connectionChanges = mutableListOf>() diff --git a/android/src/test/kotlin/com/navideck/universal_ble/MtuCompletionTest.kt b/android/src/test/kotlin/com/navideck/universal_ble/MtuCompletionTest.kt index 5c4cda78..4b6bcecd 100644 --- a/android/src/test/kotlin/com/navideck/universal_ble/MtuCompletionTest.kt +++ b/android/src/test/kotlin/com/navideck/universal_ble/MtuCompletionTest.kt @@ -5,6 +5,9 @@ import android.bluetooth.BluetoothGatt import android.os.Handler import java.util.concurrent.ConcurrentLinkedQueue import kotlin.test.AfterTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -37,6 +40,21 @@ internal class MtuCompletionTest { while (true) (posted.poll() ?: return).run() } + /** + * `requestMtu` is now a suspend function. Run it on an unconfined coroutine and + * record the outcome exactly like the previous callback-based API did, so the + * delivery-timing assertions below still apply. + */ + private fun requestMtuAsync(deviceId: String, mtu: Long, values: MutableList>) { + CoroutineScope(Dispatchers.Unconfined).launch { + try { + values.add(Result.success(plugin.requestMtu(deviceId, mtu))) + } catch (e: Throwable) { + values.add(Result.failure(e)) + } + } + } + @AfterTest fun clearConnections() { plugin.gattRegistry().snapshot().forEach { plugin.gattRegistry().remove(it) } @@ -46,7 +64,7 @@ internal class MtuCompletionTest { fun binderReplyIsDeliveredOnMainThreadExactlyOnce() { val gatt = gatt() val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) Thread { plugin.onMtuChanged(gatt, 247, BluetoothGatt.GATT_SUCCESS) }.apply { start(); join() } assertTrue(values.isEmpty(), "MTU replies should use the main-looper delivery shared by other BLE completions") drainMain() @@ -64,7 +82,7 @@ internal class MtuCompletionTest { true }.`when`(gatt).requestMtu(247) val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) drainMain() assertEquals(247L, values.single().getOrThrow()) } @@ -74,7 +92,7 @@ internal class MtuCompletionTest { val gatt = gatt() plugin.onMtuChanged(gatt, 247, BluetoothGatt.GATT_SUCCESS) val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) drainMain() assertEquals(247L, values.single().getOrThrow()) Mockito.verify(gatt, Mockito.never()).requestMtu(Mockito.anyInt()) @@ -84,7 +102,7 @@ internal class MtuCompletionTest { fun insufficientNegotiatedMtuIsReportedWithoutInventingTheRequestedSize() { val gatt = gatt() val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) plugin.onMtuChanged(gatt, 23, BluetoothGatt.GATT_SUCCESS) drainMain() assertEquals(23L, values.single().getOrThrow()) @@ -104,7 +122,7 @@ internal class MtuCompletionTest { val gatt = gatt() plugin.onMtuChanged(gatt, 23, BluetoothGatt.GATT_SUCCESS) val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) drainMain() assertTrue(values.isEmpty()) Mockito.verify(gatt).requestMtu(247) @@ -118,10 +136,10 @@ internal class MtuCompletionTest { val gatt = gatt() Mockito.`when`(gatt.requestMtu(247)).thenReturn(false, true) val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) drainMain() assertTrue(values.single().isFailure) - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) plugin.onMtuChanged(gatt, 247, BluetoothGatt.GATT_SUCCESS) drainMain() assertEquals(2, values.size) @@ -132,8 +150,8 @@ internal class MtuCompletionTest { fun simultaneousRequestsShareOneNegotiation() { val gatt = gatt() val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } - plugin.requestMtu(address, 517L) { values.add(it) } + requestMtuAsync(address, 247L, values) + requestMtuAsync(address, 517L, values) plugin.onMtuChanged(gatt, 247, BluetoothGatt.GATT_SUCCESS) drainMain() assertEquals(listOf(247L, 247L), values.map { it.getOrThrow() }) @@ -148,7 +166,7 @@ internal class MtuCompletionTest { drainMain() val current = gatt() val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) plugin.onMtuChanged(old, 247, BluetoothGatt.GATT_SUCCESS) drainMain() assertTrue(values.isEmpty()) @@ -162,7 +180,7 @@ internal class MtuCompletionTest { fun disconnectFailsPendingMtuAndLateSuccessCannotAcknowledgeIt() { val gatt = gatt() val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) plugin.disconnect(address) plugin.onMtuChanged(gatt, 247, BluetoothGatt.GATT_SUCCESS) drainMain() @@ -173,11 +191,11 @@ internal class MtuCompletionTest { fun failedMtuStatusDoesNotBecomeACachedValue() { val gatt = gatt() val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) plugin.onMtuChanged(gatt, 247, BluetoothGatt.GATT_FAILURE) drainMain() assertTrue(values.single().isFailure) - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) plugin.onMtuChanged(gatt, 185, BluetoothGatt.GATT_SUCCESS) drainMain() assertEquals(185L, values.last().getOrThrow()) @@ -195,7 +213,7 @@ internal class MtuCompletionTest { ) drainMain() val values = mutableListOf>() - plugin.requestMtu(address, 185L) { values.add(it) } + requestMtuAsync(address, 185L, values) drainMain() assertEquals(185L, values.single().getOrThrow()) Mockito.verify(current, Mockito.never()).requestMtu(Mockito.anyInt()) @@ -206,7 +224,7 @@ internal class MtuCompletionTest { val old = gatt() val current = gatt() val values = mutableListOf>() - plugin.requestMtu(address, 247L) { values.add(it) } + requestMtuAsync(address, 247L, values) plugin.onConnectionStateChange( old, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_DISCONNECTED ) diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift index 6d38a796..d0068798 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBle.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// Autogenerated from Pigeon (v29.0.10), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -25,7 +25,7 @@ final class PigeonError: Error { var localizedDescription: String { return - "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? ""))" } } @@ -59,126 +59,136 @@ private func createConnectionError(withChannelName channelName: String) -> Pigeo return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") } -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} +enum UniversalBlePigeonInternal { + static func isNullish(_ value: Any?) -> Bool { + guard let innerValue = value else { + return true + } -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} + if case Optional.some(Optional.none) = value { + return true + } -private func doubleEqualsUniversalBle(_ lhs: Double, _ rhs: Double) -> Bool { - return (lhs.isNaN && rhs.isNaN) || lhs == rhs -} + return innerValue is NSNull + } + static func doubleEquals(_ lhs: Double, _ rhs: Double) -> Bool { + return (lhs.isNaN && rhs.isNaN) || lhs == rhs + } -private func doubleHashUniversalBle(_ value: Double, _ hasher: inout Hasher) { - if value.isNaN { - hasher.combine(0x7FF8000000000000) - } else { - // Normalize -0.0 to 0.0 - hasher.combine(value == 0 ? 0 : value) + static func doubleHash(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8000000000000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } } -} -func deepEqualsUniversalBle(_ lhs: Any?, _ rhs: Any?) -> Bool { - let cleanLhs = nilOrValue(lhs) as Any? - let cleanRhs = nilOrValue(rhs) as Any? - switch (cleanLhs, cleanRhs) { - case (nil, nil): - return true + static func deepEquals(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true - case (nil, _), (_, nil): - return false + case (nil, _), (_, nil): + return false - case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: - return true + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: + return true - case is (Void, Void): - return true + case is (Void, Void): + return true - case (let lhsArray, let rhsArray) as ([Any?], [Any?]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !deepEqualsUniversalBle(element, rhsArray[index]) { - return false + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEquals(element, rhsArray[index]) { + return false + } } - } - return true + return true - case (let lhsArray, let rhsArray) as ([Double], [Double]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !doubleEqualsUniversalBle(element, rhsArray[index]) { - return false + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEquals(element, rhsArray[index]) { + return false + } } - } - return true - - case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard lhsDictionary.count == rhsDictionary.count else { return false } - for (lhsKey, lhsValue) in lhsDictionary { - var found = false - for (rhsKey, rhsValue) in rhsDictionary { - if deepEqualsUniversalBle(lhsKey, rhsKey) { - if deepEqualsUniversalBle(lhsValue, rhsValue) { - found = true - break - } else { - return false + return true + + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEquals(lhsKey, rhsKey) { + if deepEquals(lhsValue, rhsValue) { + found = true + break + } else { + return false + } } } + if !found { return false } } - if !found { return false } - } - return true + return true - case (let lhs as Double, let rhs as Double): - return doubleEqualsUniversalBle(lhs, rhs) + case (let lhs as Double, let rhs as Double): + return doubleEquals(lhs, rhs) - case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): - return lhsHashable == rhsHashable + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable - default: - return false + default: + return false + } } -} -func deepHashUniversalBle(value: Any?, hasher: inout Hasher) { - let cleanValue = nilOrValue(value) as Any? - if let cleanValue = cleanValue { - if let doubleValue = cleanValue as? Double { - doubleHashUniversalBle(doubleValue, &hasher) - } else if let valueList = cleanValue as? [Any?] { - for item in valueList { - deepHashUniversalBle(value: item, hasher: &hasher) - } - } else if let valueList = cleanValue as? [Double] { - for item in valueList { - doubleHashUniversalBle(item, &hasher) - } - } else if let valueDict = cleanValue as? [AnyHashable: Any?] { - var result = 0 - for (key, value) in valueDict { - var entryKeyHasher = Hasher() - deepHashUniversalBle(value: key, hasher: &entryKeyHasher) - var entryValueHasher = Hasher() - deepHashUniversalBle(value: value, hasher: &entryValueHasher) - result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) - } - hasher.combine(result) - } else if let hashableValue = cleanValue as? AnyHashable { - hasher.combine(hashableValue) + static func deepHash(value: Any?, hasher: inout Hasher) { + let cleanValue = nilOrValue(value) as Any? + if let cleanValue = cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHash(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHash(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHash(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHash(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHash(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) + } } else { - hasher.combine(String(describing: cleanValue)) + hasher.combine(0) } - } else { - hasher.combine(0) } + } +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} -enum BleLogLevel: Int { + +enum BleLogLevel: Int, CaseIterable { case none = 0 case error = 1 case warning = 2 @@ -187,7 +197,7 @@ enum BleLogLevel: Int { case verbose = 5 } -enum AvailabilityState: Int { +enum AvailabilityState: Int, CaseIterable { case unknown = 0 case resetting = 1 case unsupported = 2 @@ -196,20 +206,20 @@ enum AvailabilityState: Int { case poweredOn = 5 } -enum BleConnectionState: Int { +enum BleConnectionState: Int, CaseIterable { case connected = 0 case disconnected = 1 case connecting = 2 case disconnecting = 3 } -enum BleInputProperty: Int { +enum BleInputProperty: Int, CaseIterable { case disabled = 0 case notification = 1 case indication = 2 } -enum BleOutputProperty: Int { +enum BleOutputProperty: Int, CaseIterable { case withResponse = 0 case withoutResponse = 1 } @@ -217,7 +227,7 @@ enum BleOutputProperty: Int { /// Connection priority hint passed to [requestConnectionPriority]. /// /// Maps to Android `BluetoothGatt.CONNECTION_PRIORITY_*` constants. -enum BleConnectionPriority: Int { +enum BleConnectionPriority: Int, CaseIterable { /// Default OS-managed interval (~30-50 ms). Android constant: 0. case balanced = 0 /// Low-latency interval (~7.5-15 ms), higher power draw. Android constant: 1. @@ -226,7 +236,7 @@ enum BleConnectionPriority: Int { case lowPower = 2 } -enum AndroidScanMode: Int { +enum AndroidScanMode: Int, CaseIterable { case balanced = 0 case lowLatency = 1 case lowPower = 2 @@ -245,7 +255,7 @@ enum AndroidScanMode: Int { /// older devices. /// /// See https://developer.android.com/reference/android/bluetooth/le/ScanSettings -enum AndroidScanCallbackType: Int { +enum AndroidScanCallbackType: Int, CaseIterable { case allMatches = 0 case firstMatch = 1 case matchLost = 2 @@ -253,19 +263,19 @@ enum AndroidScanCallbackType: Int { } /// Mirrors `android.bluetooth.le.ScanSettings#setMatchMode`. -enum AndroidScanMatchMode: Int { +enum AndroidScanMatchMode: Int, CaseIterable { case aggressive = 0 case sticky = 1 } /// Mirrors `android.bluetooth.le.ScanSettings#setNumOfMatches`. -enum AndroidScanNumOfMatches: Int { +enum AndroidScanNumOfMatches: Int, CaseIterable { case one = 0 case few = 1 case max = 2 } -enum CharacteristicProperty: Int { +enum CharacteristicProperty: Int, CaseIterable { case broadcast = 0 case read = 1 case writeWithoutResponse = 2 @@ -276,7 +286,7 @@ enum CharacteristicProperty: Int { case extendedProperties = 7 } -enum PeripheralReadinessState: Int { +enum PeripheralReadinessState: Int, CaseIterable { case unknown = 0 case ready = 1 case bluetoothOff = 2 @@ -284,14 +294,14 @@ enum PeripheralReadinessState: Int { case unsupported = 4 } -enum PeripheralAttributePermission: Int { +enum PeripheralAttributePermission: Int, CaseIterable { case readable = 0 case writeable = 1 case readEncryptionRequired = 2 case writeEncryptionRequired = 3 } -enum PeripheralAdvertisingState: Int { +enum PeripheralAdvertisingState: Int, CaseIterable { case idle = 0 case starting = 1 case advertising = 2 @@ -300,7 +310,7 @@ enum PeripheralAdvertisingState: Int { } /// Unified error codes for all platforms -enum UniversalBleErrorCode: Int { +enum UniversalBleErrorCode: Int, CaseIterable { case unknownError = 0 case failed = 1 case notSupported = 2 @@ -368,7 +378,7 @@ enum UniversalBleErrorCode: Int { /// ------------------------------------------------------------ /// /// Generated class from Pigeon that represents data sent in messages. -struct UniversalBleScanResult: Hashable { +struct UniversalBleScanResult: Hashable, CustomStringConvertible { var deviceId: String var name: String? = nil var isPaired: Bool? = nil @@ -425,28 +435,32 @@ struct UniversalBleScanResult: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.deviceId, rhs.deviceId) && deepEqualsUniversalBle(lhs.name, rhs.name) && deepEqualsUniversalBle(lhs.isPaired, rhs.isPaired) && deepEqualsUniversalBle(lhs.rssi, rhs.rssi) && deepEqualsUniversalBle(lhs.manufacturerDataList, rhs.manufacturerDataList) && deepEqualsUniversalBle(lhs.serviceData, rhs.serviceData) && deepEqualsUniversalBle(lhs.services, rhs.services) && deepEqualsUniversalBle(lhs.timestamp, rhs.timestamp) && deepEqualsUniversalBle(lhs.timestampMicroseconds, rhs.timestampMicroseconds) && deepEqualsUniversalBle(lhs.connectable, rhs.connectable) + return UniversalBlePigeonInternal.deepEquals(lhs.deviceId, rhs.deviceId) && UniversalBlePigeonInternal.deepEquals(lhs.name, rhs.name) && UniversalBlePigeonInternal.deepEquals(lhs.isPaired, rhs.isPaired) && UniversalBlePigeonInternal.deepEquals(lhs.rssi, rhs.rssi) && UniversalBlePigeonInternal.deepEquals(lhs.manufacturerDataList, rhs.manufacturerDataList) && UniversalBlePigeonInternal.deepEquals(lhs.serviceData, rhs.serviceData) && UniversalBlePigeonInternal.deepEquals(lhs.services, rhs.services) && UniversalBlePigeonInternal.deepEquals(lhs.timestamp, rhs.timestamp) && UniversalBlePigeonInternal.deepEquals(lhs.timestampMicroseconds, rhs.timestampMicroseconds) && UniversalBlePigeonInternal.deepEquals(lhs.connectable, rhs.connectable) } func hash(into hasher: inout Hasher) { hasher.combine("UniversalBleScanResult") - deepHashUniversalBle(value: deviceId, hasher: &hasher) - deepHashUniversalBle(value: name, hasher: &hasher) - deepHashUniversalBle(value: isPaired, hasher: &hasher) - deepHashUniversalBle(value: rssi, hasher: &hasher) - deepHashUniversalBle(value: manufacturerDataList, hasher: &hasher) - deepHashUniversalBle(value: serviceData, hasher: &hasher) - deepHashUniversalBle(value: services, hasher: &hasher) - deepHashUniversalBle(value: timestamp, hasher: &hasher) - deepHashUniversalBle(value: timestampMicroseconds, hasher: &hasher) - deepHashUniversalBle(value: connectable, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: deviceId, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: name, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: isPaired, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: rssi, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: manufacturerDataList, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: serviceData, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: services, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: timestamp, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: timestampMicroseconds, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: connectable, hasher: &hasher) + } + + public var description: String { + return "UniversalBleScanResult(deviceId: \(String(describing: deviceId)), name: \(String(describing: name)), isPaired: \(String(describing: isPaired)), rssi: \(String(describing: rssi)), manufacturerDataList: \(String(describing: manufacturerDataList)), serviceData: \(String(describing: serviceData)), services: \(String(describing: services)), timestamp: \(String(describing: timestamp)), timestampMicroseconds: \(String(describing: timestampMicroseconds)), connectable: \(String(describing: connectable)))" } } /// The device a platform's native picker returned. /// /// Generated class from Pigeon that represents data sent in messages. -struct UniversalDevicePickerResult: Hashable { +struct UniversalDevicePickerResult: Hashable, CustomStringConvertible { /// Identifier to pass to `connect`. var deviceId: String /// Name the platform reported for the selected device, if any. @@ -473,20 +487,24 @@ struct UniversalDevicePickerResult: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.deviceId, rhs.deviceId) && deepEqualsUniversalBle(lhs.name, rhs.name) + return UniversalBlePigeonInternal.deepEquals(lhs.deviceId, rhs.deviceId) && UniversalBlePigeonInternal.deepEquals(lhs.name, rhs.name) } func hash(into hasher: inout Hasher) { hasher.combine("UniversalDevicePickerResult") - deepHashUniversalBle(value: deviceId, hasher: &hasher) - deepHashUniversalBle(value: name, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: deviceId, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: name, hasher: &hasher) + } + + public var description: String { + return "UniversalDevicePickerResult(deviceId: \(String(describing: deviceId)), name: \(String(describing: name)))" } } /// Central/GATT models /// /// Generated class from Pigeon that represents data sent in messages. -struct UniversalBleService: Hashable { +struct UniversalBleService: Hashable, CustomStringConvertible { var uuid: String var characteristics: [UniversalBleCharacteristic]? = nil @@ -511,18 +529,22 @@ struct UniversalBleService: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.characteristics, rhs.characteristics) + return UniversalBlePigeonInternal.deepEquals(lhs.uuid, rhs.uuid) && UniversalBlePigeonInternal.deepEquals(lhs.characteristics, rhs.characteristics) } func hash(into hasher: inout Hasher) { hasher.combine("UniversalBleService") - deepHashUniversalBle(value: uuid, hasher: &hasher) - deepHashUniversalBle(value: characteristics, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: uuid, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: characteristics, hasher: &hasher) + } + + public var description: String { + return "UniversalBleService(uuid: \(String(describing: uuid)), characteristics: \(String(describing: characteristics)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct UniversalBleCharacteristic: Hashable { +struct UniversalBleCharacteristic: Hashable, CustomStringConvertible { var uuid: String var properties: [CharacteristicProperty] var descriptors: [UniversalBleDescriptor] @@ -551,19 +573,23 @@ struct UniversalBleCharacteristic: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.properties, rhs.properties) && deepEqualsUniversalBle(lhs.descriptors, rhs.descriptors) + return UniversalBlePigeonInternal.deepEquals(lhs.uuid, rhs.uuid) && UniversalBlePigeonInternal.deepEquals(lhs.properties, rhs.properties) && UniversalBlePigeonInternal.deepEquals(lhs.descriptors, rhs.descriptors) } func hash(into hasher: inout Hasher) { hasher.combine("UniversalBleCharacteristic") - deepHashUniversalBle(value: uuid, hasher: &hasher) - deepHashUniversalBle(value: properties, hasher: &hasher) - deepHashUniversalBle(value: descriptors, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: uuid, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: properties, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: descriptors, hasher: &hasher) + } + + public var description: String { + return "UniversalBleCharacteristic(uuid: \(String(describing: uuid)), properties: \(String(describing: properties)), descriptors: \(String(describing: descriptors)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct UniversalBleDescriptor: Hashable { +struct UniversalBleDescriptor: Hashable, CustomStringConvertible { var uuid: String @@ -584,12 +610,16 @@ struct UniversalBleDescriptor: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) + return UniversalBlePigeonInternal.deepEquals(lhs.uuid, rhs.uuid) } func hash(into hasher: inout Hasher) { hasher.combine("UniversalBleDescriptor") - deepHashUniversalBle(value: uuid, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: uuid, hasher: &hasher) + } + + public var description: String { + return "UniversalBleDescriptor(uuid: \(String(describing: uuid)))" } } @@ -599,7 +629,7 @@ struct UniversalBleDescriptor: Hashable { /// (multiply interval by 1.25 for ms; supervisionTimeout by 10 for ms). /// /// Generated class from Pigeon that represents data sent in messages. -struct BleConnectionParametersChange: Hashable { +struct BleConnectionParametersChange: Hashable, CustomStringConvertible { var deviceId: String var interval: Int64 var latency: Int64 @@ -636,16 +666,20 @@ struct BleConnectionParametersChange: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.deviceId, rhs.deviceId) && deepEqualsUniversalBle(lhs.interval, rhs.interval) && deepEqualsUniversalBle(lhs.latency, rhs.latency) && deepEqualsUniversalBle(lhs.supervisionTimeout, rhs.supervisionTimeout) && deepEqualsUniversalBle(lhs.status, rhs.status) + return UniversalBlePigeonInternal.deepEquals(lhs.deviceId, rhs.deviceId) && UniversalBlePigeonInternal.deepEquals(lhs.interval, rhs.interval) && UniversalBlePigeonInternal.deepEquals(lhs.latency, rhs.latency) && UniversalBlePigeonInternal.deepEquals(lhs.supervisionTimeout, rhs.supervisionTimeout) && UniversalBlePigeonInternal.deepEquals(lhs.status, rhs.status) } func hash(into hasher: inout Hasher) { hasher.combine("BleConnectionParametersChange") - deepHashUniversalBle(value: deviceId, hasher: &hasher) - deepHashUniversalBle(value: interval, hasher: &hasher) - deepHashUniversalBle(value: latency, hasher: &hasher) - deepHashUniversalBle(value: supervisionTimeout, hasher: &hasher) - deepHashUniversalBle(value: status, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: deviceId, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: interval, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: latency, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: supervisionTimeout, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: status, hasher: &hasher) + } + + public var description: String { + return "BleConnectionParametersChange(deviceId: \(String(describing: deviceId)), interval: \(String(describing: interval)), latency: \(String(describing: latency)), supervisionTimeout: \(String(describing: supervisionTimeout)), status: \(String(describing: status)))" } } @@ -678,7 +712,7 @@ struct BleConnectionParametersChange: Hashable { /// See https://developer.android.com/reference/android/bluetooth/le/ScanSettings /// /// Generated class from Pigeon that represents data sent in messages. -struct AndroidOptions: Hashable { +struct AndroidOptions: Hashable, CustomStringConvertible { var requestLocationPermission: Bool? = nil var scanMode: AndroidScanMode? = nil var reportDelayMillis: Int64? = nil @@ -723,23 +757,27 @@ struct AndroidOptions: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.requestLocationPermission, rhs.requestLocationPermission) && deepEqualsUniversalBle(lhs.scanMode, rhs.scanMode) && deepEqualsUniversalBle(lhs.reportDelayMillis, rhs.reportDelayMillis) && deepEqualsUniversalBle(lhs.callbackType, rhs.callbackType) && deepEqualsUniversalBle(lhs.matchMode, rhs.matchMode) && deepEqualsUniversalBle(lhs.numOfMatches, rhs.numOfMatches) && deepEqualsUniversalBle(lhs.legacy, rhs.legacy) + return UniversalBlePigeonInternal.deepEquals(lhs.requestLocationPermission, rhs.requestLocationPermission) && UniversalBlePigeonInternal.deepEquals(lhs.scanMode, rhs.scanMode) && UniversalBlePigeonInternal.deepEquals(lhs.reportDelayMillis, rhs.reportDelayMillis) && UniversalBlePigeonInternal.deepEquals(lhs.callbackType, rhs.callbackType) && UniversalBlePigeonInternal.deepEquals(lhs.matchMode, rhs.matchMode) && UniversalBlePigeonInternal.deepEquals(lhs.numOfMatches, rhs.numOfMatches) && UniversalBlePigeonInternal.deepEquals(lhs.legacy, rhs.legacy) } func hash(into hasher: inout Hasher) { hasher.combine("AndroidOptions") - deepHashUniversalBle(value: requestLocationPermission, hasher: &hasher) - deepHashUniversalBle(value: scanMode, hasher: &hasher) - deepHashUniversalBle(value: reportDelayMillis, hasher: &hasher) - deepHashUniversalBle(value: callbackType, hasher: &hasher) - deepHashUniversalBle(value: matchMode, hasher: &hasher) - deepHashUniversalBle(value: numOfMatches, hasher: &hasher) - deepHashUniversalBle(value: legacy, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: requestLocationPermission, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: scanMode, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: reportDelayMillis, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: callbackType, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: matchMode, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: numOfMatches, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: legacy, hasher: &hasher) + } + + public var description: String { + return "AndroidOptions(requestLocationPermission: \(String(describing: requestLocationPermission)), scanMode: \(String(describing: scanMode)), reportDelayMillis: \(String(describing: reportDelayMillis)), callbackType: \(String(describing: callbackType)), matchMode: \(String(describing: matchMode)), numOfMatches: \(String(describing: numOfMatches)), legacy: \(String(describing: legacy)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct UniversalScanConfig: Hashable { +struct UniversalScanConfig: Hashable, CustomStringConvertible { var android: AndroidOptions? = nil /// When `true`, each device is reported only once per scan session: repeated /// scan results for a device are suppressed. Platforms with a native lever @@ -768,18 +806,22 @@ struct UniversalScanConfig: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.android, rhs.android) && deepEqualsUniversalBle(lhs.skipDuplicates, rhs.skipDuplicates) + return UniversalBlePigeonInternal.deepEquals(lhs.android, rhs.android) && UniversalBlePigeonInternal.deepEquals(lhs.skipDuplicates, rhs.skipDuplicates) } func hash(into hasher: inout Hasher) { hasher.combine("UniversalScanConfig") - deepHashUniversalBle(value: android, hasher: &hasher) - deepHashUniversalBle(value: skipDuplicates, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: android, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: skipDuplicates, hasher: &hasher) + } + + public var description: String { + return "UniversalScanConfig(android: \(String(describing: android)), skipDuplicates: \(String(describing: skipDuplicates)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct UniversalScanFilter: Hashable { +struct UniversalScanFilter: Hashable, CustomStringConvertible { var withServices: [String] var withNamePrefix: [String] var withManufacturerData: [ManufacturerDataFilter] @@ -815,20 +857,24 @@ struct UniversalScanFilter: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.withServices, rhs.withServices) && deepEqualsUniversalBle(lhs.withNamePrefix, rhs.withNamePrefix) && deepEqualsUniversalBle(lhs.withManufacturerData, rhs.withManufacturerData) && deepEqualsUniversalBle(lhs.withServiceData, rhs.withServiceData) + return UniversalBlePigeonInternal.deepEquals(lhs.withServices, rhs.withServices) && UniversalBlePigeonInternal.deepEquals(lhs.withNamePrefix, rhs.withNamePrefix) && UniversalBlePigeonInternal.deepEquals(lhs.withManufacturerData, rhs.withManufacturerData) && UniversalBlePigeonInternal.deepEquals(lhs.withServiceData, rhs.withServiceData) } func hash(into hasher: inout Hasher) { hasher.combine("UniversalScanFilter") - deepHashUniversalBle(value: withServices, hasher: &hasher) - deepHashUniversalBle(value: withNamePrefix, hasher: &hasher) - deepHashUniversalBle(value: withManufacturerData, hasher: &hasher) - deepHashUniversalBle(value: withServiceData, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: withServices, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: withNamePrefix, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: withManufacturerData, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: withServiceData, hasher: &hasher) + } + + public var description: String { + return "UniversalScanFilter(withServices: \(String(describing: withServices)), withNamePrefix: \(String(describing: withNamePrefix)), withManufacturerData: \(String(describing: withManufacturerData)), withServiceData: \(String(describing: withServiceData)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct ManufacturerDataFilter: Hashable { +struct ManufacturerDataFilter: Hashable, CustomStringConvertible { /// Must be of integer type, in hex or decimal form (e.g. 0x004c or 76). var companyIdentifier: Int64 /// Matches as prefix the device's advertised data. @@ -862,19 +908,23 @@ struct ManufacturerDataFilter: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.companyIdentifier, rhs.companyIdentifier) && deepEqualsUniversalBle(lhs.payloadPrefix, rhs.payloadPrefix) && deepEqualsUniversalBle(lhs.payloadMask, rhs.payloadMask) + return UniversalBlePigeonInternal.deepEquals(lhs.companyIdentifier, rhs.companyIdentifier) && UniversalBlePigeonInternal.deepEquals(lhs.payloadPrefix, rhs.payloadPrefix) && UniversalBlePigeonInternal.deepEquals(lhs.payloadMask, rhs.payloadMask) } func hash(into hasher: inout Hasher) { hasher.combine("ManufacturerDataFilter") - deepHashUniversalBle(value: companyIdentifier, hasher: &hasher) - deepHashUniversalBle(value: payloadPrefix, hasher: &hasher) - deepHashUniversalBle(value: payloadMask, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: companyIdentifier, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: payloadPrefix, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: payloadMask, hasher: &hasher) + } + + public var description: String { + return "ManufacturerDataFilter(companyIdentifier: \(String(describing: companyIdentifier)), payloadPrefix: \(String(describing: payloadPrefix)), payloadMask: \(String(describing: payloadMask)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct UniversalManufacturerData: Hashable { +struct UniversalManufacturerData: Hashable, CustomStringConvertible { var companyIdentifier: Int64 var data: FlutterStandardTypedData @@ -899,13 +949,17 @@ struct UniversalManufacturerData: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.companyIdentifier, rhs.companyIdentifier) && deepEqualsUniversalBle(lhs.data, rhs.data) + return UniversalBlePigeonInternal.deepEquals(lhs.companyIdentifier, rhs.companyIdentifier) && UniversalBlePigeonInternal.deepEquals(lhs.data, rhs.data) } func hash(into hasher: inout Hasher) { hasher.combine("UniversalManufacturerData") - deepHashUniversalBle(value: companyIdentifier, hasher: &hasher) - deepHashUniversalBle(value: data, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: companyIdentifier, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: data, hasher: &hasher) + } + + public var description: String { + return "UniversalManufacturerData(companyIdentifier: \(String(describing: companyIdentifier)), data: \(String(describing: data)))" } } @@ -923,7 +977,7 @@ struct UniversalManufacturerData: Hashable { /// delivered while suspended). /// /// Generated class from Pigeon that represents data sent in messages. -struct AppleConnectionOptions: Hashable { +struct AppleConnectionOptions: Hashable, CustomStringConvertible { /// `CBConnectPeripheralOptionNotifyOnConnectionKey`: /// notify when a connection to the device succeeds while the app is suspended. var notifyOnConnection: Bool? = nil @@ -959,14 +1013,18 @@ struct AppleConnectionOptions: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.notifyOnConnection, rhs.notifyOnConnection) && deepEqualsUniversalBle(lhs.notifyOnDisconnection, rhs.notifyOnDisconnection) && deepEqualsUniversalBle(lhs.notifyOnNotification, rhs.notifyOnNotification) + return UniversalBlePigeonInternal.deepEquals(lhs.notifyOnConnection, rhs.notifyOnConnection) && UniversalBlePigeonInternal.deepEquals(lhs.notifyOnDisconnection, rhs.notifyOnDisconnection) && UniversalBlePigeonInternal.deepEquals(lhs.notifyOnNotification, rhs.notifyOnNotification) } func hash(into hasher: inout Hasher) { hasher.combine("AppleConnectionOptions") - deepHashUniversalBle(value: notifyOnConnection, hasher: &hasher) - deepHashUniversalBle(value: notifyOnDisconnection, hasher: &hasher) - deepHashUniversalBle(value: notifyOnNotification, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: notifyOnConnection, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: notifyOnDisconnection, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: notifyOnNotification, hasher: &hasher) + } + + public var description: String { + return "AppleConnectionOptions(notifyOnConnection: \(String(describing: notifyOnConnection)), notifyOnDisconnection: \(String(describing: notifyOnDisconnection)), notifyOnNotification: \(String(describing: notifyOnNotification)))" } } @@ -974,7 +1032,7 @@ struct AppleConnectionOptions: Hashable { /// a separate entry in the system picker. /// /// Generated class from Pigeon that represents data sent in messages. -struct AppleAccessoryPickerItem: Hashable { +struct AppleAccessoryPickerItem: Hashable, CustomStringConvertible { /// Name shown in the system accessory picker. var displayName: String /// Name of the product image in the iOS app's asset catalog. @@ -1021,17 +1079,21 @@ struct AppleAccessoryPickerItem: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.displayName, rhs.displayName) && deepEqualsUniversalBle(lhs.imageAsset, rhs.imageAsset) && deepEqualsUniversalBle(lhs.serviceUuid, rhs.serviceUuid) && deepEqualsUniversalBle(lhs.nameSubstring, rhs.nameSubstring) && deepEqualsUniversalBle(lhs.requiresImmediateRange, rhs.requiresImmediateRange) && deepEqualsUniversalBle(lhs.supportsBluetoothPairing, rhs.supportsBluetoothPairing) + return UniversalBlePigeonInternal.deepEquals(lhs.displayName, rhs.displayName) && UniversalBlePigeonInternal.deepEquals(lhs.imageAsset, rhs.imageAsset) && UniversalBlePigeonInternal.deepEquals(lhs.serviceUuid, rhs.serviceUuid) && UniversalBlePigeonInternal.deepEquals(lhs.nameSubstring, rhs.nameSubstring) && UniversalBlePigeonInternal.deepEquals(lhs.requiresImmediateRange, rhs.requiresImmediateRange) && UniversalBlePigeonInternal.deepEquals(lhs.supportsBluetoothPairing, rhs.supportsBluetoothPairing) } func hash(into hasher: inout Hasher) { hasher.combine("AppleAccessoryPickerItem") - deepHashUniversalBle(value: displayName, hasher: &hasher) - deepHashUniversalBle(value: imageAsset, hasher: &hasher) - deepHashUniversalBle(value: serviceUuid, hasher: &hasher) - deepHashUniversalBle(value: nameSubstring, hasher: &hasher) - deepHashUniversalBle(value: requiresImmediateRange, hasher: &hasher) - deepHashUniversalBle(value: supportsBluetoothPairing, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: displayName, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: imageAsset, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: serviceUuid, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: nameSubstring, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: requiresImmediateRange, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: supportsBluetoothPairing, hasher: &hasher) + } + + public var description: String { + return "AppleAccessoryPickerItem(displayName: \(String(describing: displayName)), imageAsset: \(String(describing: imageAsset)), serviceUuid: \(String(describing: serviceUuid)), nameSubstring: \(String(describing: nameSubstring)), requiresImmediateRange: \(String(describing: requiresImmediateRange)), supportsBluetoothPairing: \(String(describing: supportsBluetoothPairing)))" } } @@ -1039,7 +1101,7 @@ struct AppleAccessoryPickerItem: Hashable { /// AccessorySetupKit before connecting to them. /// /// Generated class from Pigeon that represents data sent in messages. -struct AppleAccessorySetupOptions: Hashable { +struct AppleAccessorySetupOptions: Hashable, CustomStringConvertible { /// Accessories the picker offers. At least one item is required. var items: [AppleAccessoryPickerItem] @@ -1061,17 +1123,21 @@ struct AppleAccessorySetupOptions: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.items, rhs.items) + return UniversalBlePigeonInternal.deepEquals(lhs.items, rhs.items) } func hash(into hasher: inout Hasher) { hasher.combine("AppleAccessorySetupOptions") - deepHashUniversalBle(value: items, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: items, hasher: &hasher) + } + + public var description: String { + return "AppleAccessorySetupOptions(items: \(String(describing: items)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct AndroidConnectionOptions: Hashable { +struct AndroidConnectionOptions: Hashable, CustomStringConvertible { /// Close the GATT client when the FlutterEngine is detached (for /// example, when the app is "killed"). The plugin otherwise leaves the /// GATT open, keeping the peripheral occupied until its supervision @@ -1099,17 +1165,21 @@ struct AndroidConnectionOptions: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.closeGattOnDetach, rhs.closeGattOnDetach) + return UniversalBlePigeonInternal.deepEquals(lhs.closeGattOnDetach, rhs.closeGattOnDetach) } func hash(into hasher: inout Hasher) { hasher.combine("AndroidConnectionOptions") - deepHashUniversalBle(value: closeGattOnDetach, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: closeGattOnDetach, hasher: &hasher) + } + + public var description: String { + return "AndroidConnectionOptions(closeGattOnDetach: \(String(describing: closeGattOnDetach)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct ConnectionPlatformConfig: Hashable { +struct ConnectionPlatformConfig: Hashable, CustomStringConvertible { var apple: AppleConnectionOptions? = nil var android: AndroidConnectionOptions? = nil @@ -1134,18 +1204,22 @@ struct ConnectionPlatformConfig: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.apple, rhs.apple) && deepEqualsUniversalBle(lhs.android, rhs.android) + return UniversalBlePigeonInternal.deepEquals(lhs.apple, rhs.apple) && UniversalBlePigeonInternal.deepEquals(lhs.android, rhs.android) } func hash(into hasher: inout Hasher) { hasher.combine("ConnectionPlatformConfig") - deepHashUniversalBle(value: apple, hasher: &hasher) - deepHashUniversalBle(value: android, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: apple, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: android, hasher: &hasher) + } + + public var description: String { + return "ConnectionPlatformConfig(apple: \(String(describing: apple)), android: \(String(describing: android)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct AndroidDiscoverServicesOptions: Hashable { +struct AndroidDiscoverServicesOptions: Hashable, CustomStringConvertible { /// Clear the OS-level GATT cache of the connection (`BluetoothGatt.refresh()`) /// before discovering, so a peripheral whose GATT layout changed while it /// was disconnected is read again instead of served from the cache. @@ -1172,12 +1246,16 @@ struct AndroidDiscoverServicesOptions: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.clearGattCache, rhs.clearGattCache) + return UniversalBlePigeonInternal.deepEquals(lhs.clearGattCache, rhs.clearGattCache) } func hash(into hasher: inout Hasher) { hasher.combine("AndroidDiscoverServicesOptions") - deepHashUniversalBle(value: clearGattCache, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: clearGattCache, hasher: &hasher) + } + + public var description: String { + return "AndroidDiscoverServicesOptions(clearGattCache: \(String(describing: clearGattCache)))" } } @@ -1185,7 +1263,7 @@ struct AndroidDiscoverServicesOptions: Hashable { /// that have no equivalent. /// /// Generated class from Pigeon that represents data sent in messages. -struct DiscoverServicesPlatformConfig: Hashable { +struct DiscoverServicesPlatformConfig: Hashable, CustomStringConvertible { var android: AndroidDiscoverServicesOptions? = nil @@ -1206,17 +1284,21 @@ struct DiscoverServicesPlatformConfig: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.android, rhs.android) + return UniversalBlePigeonInternal.deepEquals(lhs.android, rhs.android) } func hash(into hasher: inout Hasher) { hasher.combine("DiscoverServicesPlatformConfig") - deepHashUniversalBle(value: android, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: android, hasher: &hasher) + } + + public var description: String { + return "DiscoverServicesPlatformConfig(android: \(String(describing: android)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct PeripheralAndroidOptions: Hashable { +struct PeripheralAndroidOptions: Hashable, CustomStringConvertible { var addManufacturerDataInScanResponse: Bool? = nil /// Put advertised service UUIDs in the scan response instead of the primary /// advertisement. The Android primary advertisement and scan response are @@ -1247,18 +1329,22 @@ struct PeripheralAndroidOptions: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.addManufacturerDataInScanResponse, rhs.addManufacturerDataInScanResponse) && deepEqualsUniversalBle(lhs.addServicesInScanResponse, rhs.addServicesInScanResponse) + return UniversalBlePigeonInternal.deepEquals(lhs.addManufacturerDataInScanResponse, rhs.addManufacturerDataInScanResponse) && UniversalBlePigeonInternal.deepEquals(lhs.addServicesInScanResponse, rhs.addServicesInScanResponse) } func hash(into hasher: inout Hasher) { hasher.combine("PeripheralAndroidOptions") - deepHashUniversalBle(value: addManufacturerDataInScanResponse, hasher: &hasher) - deepHashUniversalBle(value: addServicesInScanResponse, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: addManufacturerDataInScanResponse, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: addServicesInScanResponse, hasher: &hasher) + } + + public var description: String { + return "PeripheralAndroidOptions(addManufacturerDataInScanResponse: \(String(describing: addManufacturerDataInScanResponse)), addServicesInScanResponse: \(String(describing: addServicesInScanResponse)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct PeripheralPlatformConfig: Hashable { +struct PeripheralPlatformConfig: Hashable, CustomStringConvertible { var android: PeripheralAndroidOptions? = nil @@ -1279,19 +1365,23 @@ struct PeripheralPlatformConfig: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.android, rhs.android) + return UniversalBlePigeonInternal.deepEquals(lhs.android, rhs.android) } func hash(into hasher: inout Hasher) { hasher.combine("PeripheralPlatformConfig") - deepHashUniversalBle(value: android, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: android, hasher: &hasher) + } + + public var description: String { + return "PeripheralPlatformConfig(android: \(String(describing: android)))" } } /// Peripheral/GATT server models /// /// Generated class from Pigeon that represents data sent in messages. -struct PeripheralService: Hashable { +struct PeripheralService: Hashable, CustomStringConvertible { var uuid: String var primary: Bool var characteristics: [PeripheralCharacteristic] @@ -1320,19 +1410,23 @@ struct PeripheralService: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.primary, rhs.primary) && deepEqualsUniversalBle(lhs.characteristics, rhs.characteristics) + return UniversalBlePigeonInternal.deepEquals(lhs.uuid, rhs.uuid) && UniversalBlePigeonInternal.deepEquals(lhs.primary, rhs.primary) && UniversalBlePigeonInternal.deepEquals(lhs.characteristics, rhs.characteristics) } func hash(into hasher: inout Hasher) { hasher.combine("PeripheralService") - deepHashUniversalBle(value: uuid, hasher: &hasher) - deepHashUniversalBle(value: primary, hasher: &hasher) - deepHashUniversalBle(value: characteristics, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: uuid, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: primary, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: characteristics, hasher: &hasher) + } + + public var description: String { + return "PeripheralService(uuid: \(String(describing: uuid)), primary: \(String(describing: primary)), characteristics: \(String(describing: characteristics)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct PeripheralCharacteristic: Hashable { +struct PeripheralCharacteristic: Hashable, CustomStringConvertible { var uuid: String var properties: [CharacteristicProperty] var permissions: [PeripheralAttributePermission] @@ -1369,21 +1463,25 @@ struct PeripheralCharacteristic: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.properties, rhs.properties) && deepEqualsUniversalBle(lhs.permissions, rhs.permissions) && deepEqualsUniversalBle(lhs.descriptors, rhs.descriptors) && deepEqualsUniversalBle(lhs.value, rhs.value) + return UniversalBlePigeonInternal.deepEquals(lhs.uuid, rhs.uuid) && UniversalBlePigeonInternal.deepEquals(lhs.properties, rhs.properties) && UniversalBlePigeonInternal.deepEquals(lhs.permissions, rhs.permissions) && UniversalBlePigeonInternal.deepEquals(lhs.descriptors, rhs.descriptors) && UniversalBlePigeonInternal.deepEquals(lhs.value, rhs.value) } func hash(into hasher: inout Hasher) { hasher.combine("PeripheralCharacteristic") - deepHashUniversalBle(value: uuid, hasher: &hasher) - deepHashUniversalBle(value: properties, hasher: &hasher) - deepHashUniversalBle(value: permissions, hasher: &hasher) - deepHashUniversalBle(value: descriptors, hasher: &hasher) - deepHashUniversalBle(value: value, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: uuid, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: properties, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: permissions, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: descriptors, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: value, hasher: &hasher) + } + + public var description: String { + return "PeripheralCharacteristic(uuid: \(String(describing: uuid)), properties: \(String(describing: properties)), permissions: \(String(describing: permissions)), descriptors: \(String(describing: descriptors)), value: \(String(describing: value)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct PeripheralDescriptor: Hashable { +struct PeripheralDescriptor: Hashable, CustomStringConvertible { var uuid: String var value: FlutterStandardTypedData? = nil var permissions: [PeripheralAttributePermission]? = nil @@ -1412,19 +1510,23 @@ struct PeripheralDescriptor: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.uuid, rhs.uuid) && deepEqualsUniversalBle(lhs.value, rhs.value) && deepEqualsUniversalBle(lhs.permissions, rhs.permissions) + return UniversalBlePigeonInternal.deepEquals(lhs.uuid, rhs.uuid) && UniversalBlePigeonInternal.deepEquals(lhs.value, rhs.value) && UniversalBlePigeonInternal.deepEquals(lhs.permissions, rhs.permissions) } func hash(into hasher: inout Hasher) { hasher.combine("PeripheralDescriptor") - deepHashUniversalBle(value: uuid, hasher: &hasher) - deepHashUniversalBle(value: value, hasher: &hasher) - deepHashUniversalBle(value: permissions, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: uuid, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: value, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: permissions, hasher: &hasher) + } + + public var description: String { + return "PeripheralDescriptor(uuid: \(String(describing: uuid)), value: \(String(describing: value)), permissions: \(String(describing: permissions)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct PeripheralReadRequestResult: Hashable { +struct PeripheralReadRequestResult: Hashable, CustomStringConvertible { var value: FlutterStandardTypedData var offset: Int64? = nil var status: Int64? = nil @@ -1453,19 +1555,23 @@ struct PeripheralReadRequestResult: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.value, rhs.value) && deepEqualsUniversalBle(lhs.offset, rhs.offset) && deepEqualsUniversalBle(lhs.status, rhs.status) + return UniversalBlePigeonInternal.deepEquals(lhs.value, rhs.value) && UniversalBlePigeonInternal.deepEquals(lhs.offset, rhs.offset) && UniversalBlePigeonInternal.deepEquals(lhs.status, rhs.status) } func hash(into hasher: inout Hasher) { hasher.combine("PeripheralReadRequestResult") - deepHashUniversalBle(value: value, hasher: &hasher) - deepHashUniversalBle(value: offset, hasher: &hasher) - deepHashUniversalBle(value: status, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: value, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: offset, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: status, hasher: &hasher) + } + + public var description: String { + return "PeripheralReadRequestResult(value: \(String(describing: value)), offset: \(String(describing: offset)), status: \(String(describing: status)))" } } /// Generated class from Pigeon that represents data sent in messages. -struct PeripheralWriteRequestResult: Hashable { +struct PeripheralWriteRequestResult: Hashable, CustomStringConvertible { var value: FlutterStandardTypedData? = nil var offset: Int64? = nil var status: Int64? = nil @@ -1494,14 +1600,18 @@ struct PeripheralWriteRequestResult: Hashable { if Swift.type(of: lhs) != Swift.type(of: rhs) { return false } - return deepEqualsUniversalBle(lhs.value, rhs.value) && deepEqualsUniversalBle(lhs.offset, rhs.offset) && deepEqualsUniversalBle(lhs.status, rhs.status) + return UniversalBlePigeonInternal.deepEquals(lhs.value, rhs.value) && UniversalBlePigeonInternal.deepEquals(lhs.offset, rhs.offset) && UniversalBlePigeonInternal.deepEquals(lhs.status, rhs.status) } func hash(into hasher: inout Hasher) { hasher.combine("PeripheralWriteRequestResult") - deepHashUniversalBle(value: value, hasher: &hasher) - deepHashUniversalBle(value: offset, hasher: &hasher) - deepHashUniversalBle(value: status, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: value, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: offset, hasher: &hasher) + UniversalBlePigeonInternal.deepHash(value: status, hasher: &hasher) + } + + public var description: String { + return "PeripheralWriteRequestResult(value: \(String(describing: value)), offset: \(String(describing: offset)), status: \(String(describing: status)))" } } @@ -1803,33 +1913,33 @@ class UniversalBlePigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable /// /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol UniversalBlePlatformChannel { - func getBluetoothAvailabilityState(completion: @escaping (Result) -> Void) + func getBluetoothAvailabilityState() async throws -> AvailabilityState func hasPermissions(withAndroidFineLocation: Bool) throws -> Bool - func requestPermissions(withAndroidFineLocation: Bool, completion: @escaping (Result) -> Void) - func enableBluetooth(completion: @escaping (Result) -> Void) - func disableBluetooth(completion: @escaping (Result) -> Void) + func requestPermissions(withAndroidFineLocation: Bool) async throws + func enableBluetooth() async throws -> Bool + func disableBluetooth() async throws -> Bool func startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) throws func stopScan() throws func isScanning() throws -> Bool /// Shows the platform's native device picker (iOS 18+ AccessorySetupKit) and /// returns the selected device. - func requestDevice(options: AppleAccessorySetupOptions, completion: @escaping (Result) -> Void) + func requestDevice(options: AppleAccessorySetupOptions) async throws -> UniversalDevicePickerResult func connect(deviceId: String, autoConnect: Bool?, platformConfig: ConnectionPlatformConfig?) throws func disconnect(deviceId: String) throws - func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, completion: @escaping (Result) -> Void) - func discoverServices(deviceId: String, withDescriptors: Bool, platformConfig: DiscoverServicesPlatformConfig?, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) - func readValue(deviceId: String, service: String, characteristic: String, completion: @escaping (Result) -> Void) - func readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, completion: @escaping (Result) -> Void) - func requestMtu(deviceId: String, expectedMtu: Int64, completion: @escaping (Result) -> Void) - func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: BleOutputProperty, completion: @escaping (Result) -> Void) - func writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData, completion: @escaping (Result) -> Void) - func isPaired(deviceId: String, completion: @escaping (Result) -> Void) - func pair(deviceId: String, completion: @escaping (Result) -> Void) - func unPair(deviceId: String, completion: @escaping (Result) -> Void) - func getSystemDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) + func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty) async throws + func discoverServices(deviceId: String, withDescriptors: Bool, platformConfig: DiscoverServicesPlatformConfig?) async throws -> [UniversalBleService] + func readValue(deviceId: String, service: String, characteristic: String) async throws -> FlutterStandardTypedData + func readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String) async throws -> FlutterStandardTypedData + func requestMtu(deviceId: String, expectedMtu: Int64) async throws -> Int64 + func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: BleOutputProperty) async throws + func writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData) async throws + func isPaired(deviceId: String) async throws -> Bool + func pair(deviceId: String) async throws -> Bool + func unPair(deviceId: String) async throws + func getSystemDevices(withServices: [String]) async throws -> [UniversalBleScanResult] func getConnectionState(deviceId: String) throws -> BleConnectionState - func readRssi(deviceId: String, completion: @escaping (Result) -> Void) - func requestConnectionPriority(deviceId: String, priority: BleConnectionPriority, completion: @escaping (Result) -> Void) + func readRssi(deviceId: String) async throws -> Int64 + func requestConnectionPriority(deviceId: String, priority: BleConnectionPriority) async throws func setLogLevel(logLevel: BleLogLevel) throws } @@ -1842,11 +1952,11 @@ class UniversalBlePlatformChannelSetup { let getBluetoothAvailabilityStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getBluetoothAvailabilityStateChannel.setMessageHandler { _, reply in - api.getBluetoothAvailabilityState { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.getBluetoothAvailabilityState() + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -1874,11 +1984,11 @@ class UniversalBlePlatformChannelSetup { requestPermissionsChannel.setMessageHandler { message, reply in let args = message as! [Any?] let withAndroidFineLocationArg = args[0] as! Bool - api.requestPermissions(withAndroidFineLocation: withAndroidFineLocationArg) { result in - switch result { - case .success: + Task { @MainActor in + do { + try await api.requestPermissions(withAndroidFineLocation: withAndroidFineLocationArg) reply(wrapResult(nil)) - case .failure(let error): + } catch { reply(wrapError(error)) } } @@ -1889,11 +1999,11 @@ class UniversalBlePlatformChannelSetup { let enableBluetoothChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { enableBluetoothChannel.setMessageHandler { _, reply in - api.enableBluetooth { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.enableBluetooth() + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -1904,11 +2014,11 @@ class UniversalBlePlatformChannelSetup { let disableBluetoothChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { disableBluetoothChannel.setMessageHandler { _, reply in - api.disableBluetooth { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.disableBluetooth() + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -1965,11 +2075,11 @@ class UniversalBlePlatformChannelSetup { requestDeviceChannel.setMessageHandler { message, reply in let args = message as! [Any?] let optionsArg = args[0] as! AppleAccessorySetupOptions - api.requestDevice(options: optionsArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.requestDevice(options: optionsArg) + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2017,11 +2127,11 @@ class UniversalBlePlatformChannelSetup { let serviceArg = args[1] as! String let characteristicArg = args[2] as! String let bleInputPropertyArg = args[3] as! BleInputProperty - api.setNotifiable(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, bleInputProperty: bleInputPropertyArg) { result in - switch result { - case .success: + Task { @MainActor in + do { + try await api.setNotifiable(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, bleInputProperty: bleInputPropertyArg) reply(wrapResult(nil)) - case .failure(let error): + } catch { reply(wrapError(error)) } } @@ -2036,11 +2146,11 @@ class UniversalBlePlatformChannelSetup { let deviceIdArg = args[0] as! String let withDescriptorsArg = args[1] as! Bool let platformConfigArg: DiscoverServicesPlatformConfig? = nilOrValue(args[2]) - api.discoverServices(deviceId: deviceIdArg, withDescriptors: withDescriptorsArg, platformConfig: platformConfigArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.discoverServices(deviceId: deviceIdArg, withDescriptors: withDescriptorsArg, platformConfig: platformConfigArg) + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2055,11 +2165,11 @@ class UniversalBlePlatformChannelSetup { let deviceIdArg = args[0] as! String let serviceArg = args[1] as! String let characteristicArg = args[2] as! String - api.readValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.readValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg) + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2075,11 +2185,11 @@ class UniversalBlePlatformChannelSetup { let serviceArg = args[1] as! String let characteristicArg = args[2] as! String let descriptorArg = args[3] as! String - api.readDescriptorValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, descriptor: descriptorArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.readDescriptorValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, descriptor: descriptorArg) + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2093,11 +2203,11 @@ class UniversalBlePlatformChannelSetup { let args = message as! [Any?] let deviceIdArg = args[0] as! String let expectedMtuArg = args[1] as! Int64 - api.requestMtu(deviceId: deviceIdArg, expectedMtu: expectedMtuArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.requestMtu(deviceId: deviceIdArg, expectedMtu: expectedMtuArg) + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2114,11 +2224,11 @@ class UniversalBlePlatformChannelSetup { let characteristicArg = args[2] as! String let valueArg = args[3] as! FlutterStandardTypedData let bleOutputPropertyArg = args[4] as! BleOutputProperty - api.writeValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, value: valueArg, bleOutputProperty: bleOutputPropertyArg) { result in - switch result { - case .success: + Task { @MainActor in + do { + try await api.writeValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, value: valueArg, bleOutputProperty: bleOutputPropertyArg) reply(wrapResult(nil)) - case .failure(let error): + } catch { reply(wrapError(error)) } } @@ -2135,11 +2245,11 @@ class UniversalBlePlatformChannelSetup { let characteristicArg = args[2] as! String let descriptorArg = args[3] as! String let valueArg = args[4] as! FlutterStandardTypedData - api.writeDescriptorValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, descriptor: descriptorArg, value: valueArg) { result in - switch result { - case .success: + Task { @MainActor in + do { + try await api.writeDescriptorValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, descriptor: descriptorArg, value: valueArg) reply(wrapResult(nil)) - case .failure(let error): + } catch { reply(wrapError(error)) } } @@ -2152,11 +2262,11 @@ class UniversalBlePlatformChannelSetup { isPairedChannel.setMessageHandler { message, reply in let args = message as! [Any?] let deviceIdArg = args[0] as! String - api.isPaired(deviceId: deviceIdArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.isPaired(deviceId: deviceIdArg) + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2169,11 +2279,11 @@ class UniversalBlePlatformChannelSetup { pairChannel.setMessageHandler { message, reply in let args = message as! [Any?] let deviceIdArg = args[0] as! String - api.pair(deviceId: deviceIdArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.pair(deviceId: deviceIdArg) + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2186,11 +2296,11 @@ class UniversalBlePlatformChannelSetup { unPairChannel.setMessageHandler { message, reply in let args = message as! [Any?] let deviceIdArg = args[0] as! String - api.unPair(deviceId: deviceIdArg) { result in - switch result { - case .success: + Task { @MainActor in + do { + try await api.unPair(deviceId: deviceIdArg) reply(wrapResult(nil)) - case .failure(let error): + } catch { reply(wrapError(error)) } } @@ -2203,11 +2313,11 @@ class UniversalBlePlatformChannelSetup { getSystemDevicesChannel.setMessageHandler { message, reply in let args = message as! [Any?] let withServicesArg = args[0] as! [String] - api.getSystemDevices(withServices: withServicesArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.getSystemDevices(withServices: withServicesArg) + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2235,11 +2345,11 @@ class UniversalBlePlatformChannelSetup { readRssiChannel.setMessageHandler { message, reply in let args = message as! [Any?] let deviceIdArg = args[0] as! String - api.readRssi(deviceId: deviceIdArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.readRssi(deviceId: deviceIdArg) + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2253,11 +2363,11 @@ class UniversalBlePlatformChannelSetup { let args = message as! [Any?] let deviceIdArg = args[0] as! String let priorityArg = args[1] as! BleConnectionPriority - api.requestConnectionPriority(deviceId: deviceIdArg, priority: priorityArg) { result in - switch result { - case .success: + Task { @MainActor in + do { + try await api.requestConnectionPriority(deviceId: deviceIdArg, priority: priorityArg) reply(wrapResult(nil)) - case .failure(let error): + } catch { reply(wrapError(error)) } } @@ -2282,24 +2392,25 @@ class UniversalBlePlatformChannelSetup { } } } + /// Native -> Flutter (central) /// /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. protocol UniversalBleCallbackChannelProtocol { - func onAvailabilityChanged(state stateArg: AvailabilityState, completion: @escaping (Result) -> Void) + @MainActor func onAvailabilityChanged(state stateArg: AvailabilityState) async throws /// [isFailure] marks a failed bond attempt — an event rather than a steady /// pairing state — so the Dart side can bypass its state de-duplication /// instead of swallowing every failure after the first (#325). - func onPairStateChange(deviceId deviceIdArg: String, isPaired isPairedArg: Bool, isFailure isFailureArg: Bool, completion: @escaping (Result) -> Void) - func onScanResult(result resultArg: UniversalBleScanResult, completion: @escaping (Result) -> Void) - func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, timestamp timestampArg: Int64?, completion: @escaping (Result) -> Void) + @MainActor func onPairStateChange(deviceId deviceIdArg: String, isPaired isPairedArg: Bool, isFailure isFailureArg: Bool) async throws + @MainActor func onScanResult(result resultArg: UniversalBleScanResult) async throws + @MainActor func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, timestamp timestampArg: Int64?) async throws /// [errorCode] classifies a failed connection or an unexpected disconnect /// (`deviceDisconnected`, `connectionTimeout`, `connectionFailed`, ...), /// mapped on the native side from `CBError.Code` (Apple) or the GATT /// `status` (Android). [nativeErrorCode] is that raw platform value. /// Both are `null` when there is no error or the platform has no code. - func onConnectionChanged(deviceId deviceIdArg: String, connected connectedArg: Bool, error errorArg: String?, errorCode errorCodeArg: UniversalBleErrorCode?, nativeErrorCode nativeErrorCodeArg: Int64?, completion: @escaping (Result) -> Void) - func onConnectionParametersUpdated(update updateArg: BleConnectionParametersChange, completion: @escaping (Result) -> Void) + @MainActor func onConnectionChanged(deviceId deviceIdArg: String, connected connectedArg: Bool, error errorArg: String?, errorCode errorCodeArg: UniversalBleErrorCode?, nativeErrorCode nativeErrorCodeArg: Int64?) async throws + @MainActor func onConnectionParametersUpdated(update updateArg: BleConnectionParametersChange) async throws } class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { private let binaryMessenger: FlutterBinaryMessenger @@ -2311,78 +2422,86 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { var codec: UniversalBlePigeonCodec { return UniversalBlePigeonCodec.shared } - func onAvailabilityChanged(state stateArg: AvailabilityState, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([stateArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onAvailabilityChanged(state stateArg: AvailabilityState) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([stateArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } /// [isFailure] marks a failed bond attempt — an event rather than a steady /// pairing state — so the Dart side can bypass its state de-duplication /// instead of swallowing every failure after the first (#325). - func onPairStateChange(deviceId deviceIdArg: String, isPaired isPairedArg: Bool, isFailure isFailureArg: Bool, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, isPairedArg, isFailureArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onPairStateChange(deviceId deviceIdArg: String, isPaired isPairedArg: Bool, isFailure isFailureArg: Bool) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, isPairedArg, isFailureArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } - func onScanResult(result resultArg: UniversalBleScanResult, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([resultArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onScanResult(result resultArg: UniversalBleScanResult) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([resultArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } - func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, timestamp timestampArg: Int64?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, characteristicIdArg, valueArg, timestampArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, timestamp timestampArg: Int64?) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, characteristicIdArg, valueArg, timestampArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } @@ -2391,39 +2510,43 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { /// mapped on the native side from `CBError.Code` (Apple) or the GATT /// `status` (Android). [nativeErrorCode] is that raw platform value. /// Both are `null` when there is no error or the platform has no code. - func onConnectionChanged(deviceId deviceIdArg: String, connected connectedArg: Bool, error errorArg: String?, errorCode errorCodeArg: UniversalBleErrorCode?, nativeErrorCode nativeErrorCodeArg: Int64?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, connectedArg, errorArg, errorCodeArg, nativeErrorCodeArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onConnectionChanged(deviceId deviceIdArg: String, connected connectedArg: Bool, error errorArg: String?, errorCode errorCodeArg: UniversalBleErrorCode?, nativeErrorCode nativeErrorCodeArg: Int64?) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, connectedArg, errorArg, errorCodeArg, nativeErrorCodeArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } - func onConnectionParametersUpdated(update updateArg: BleConnectionParametersChange, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionParametersUpdated\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([updateArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onConnectionParametersUpdated(update updateArg: BleConnectionParametersChange) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionParametersUpdated\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([updateArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } @@ -2625,7 +2748,7 @@ class UniversalBlePeripheralChannelSetup { /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol UniversalBleAndroidChannel { func hasBluetoothAdvertisePermission() throws -> Bool - func requestBluetoothAdvertisePermission(completion: @escaping (Result) -> Void) + func requestBluetoothAdvertisePermission() async throws -> Bool } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -2650,11 +2773,11 @@ class UniversalBleAndroidChannelSetup { let requestBluetoothAdvertisePermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleAndroidChannel.requestBluetoothAdvertisePermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { requestBluetoothAdvertisePermissionChannel.setMessageHandler { _, reply in - api.requestBluetoothAdvertisePermission { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): + Task { @MainActor in + do { + let result = try await api.requestBluetoothAdvertisePermission() + reply(wrapResult(result)) + } catch { reply(wrapError(error)) } } @@ -2664,19 +2787,20 @@ class UniversalBleAndroidChannelSetup { } } } + /// Native -> Flutter (peripheral) /// /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. protocol UniversalBlePeripheralCallbackProtocol { - func onReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) - func onWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) - func onDescriptorReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) - func onDescriptorWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) - func onCharacteristicSubscriptionChange(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, isSubscribed isSubscribedArg: Bool, name nameArg: String?, completion: @escaping (Result) -> Void) - func onAdvertisingStateChange(state stateArg: PeripheralAdvertisingState, error errorArg: String?, completion: @escaping (Result) -> Void) - func onServiceAdded(serviceId serviceIdArg: String, error errorArg: String?, completion: @escaping (Result) -> Void) - func onMtuChange(deviceId deviceIdArg: String, mtu mtuArg: Int64, completion: @escaping (Result) -> Void) - func onConnectionStateChange(deviceId deviceIdArg: String, connected connectedArg: Bool, completion: @escaping (Result) -> Void) + @MainActor func onReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?) async throws -> PeripheralReadRequestResult? + @MainActor func onWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?) async throws -> PeripheralWriteRequestResult? + @MainActor func onDescriptorReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?) async throws -> PeripheralReadRequestResult? + @MainActor func onDescriptorWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?) async throws -> PeripheralWriteRequestResult? + @MainActor func onCharacteristicSubscriptionChange(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, isSubscribed isSubscribedArg: Bool, name nameArg: String?) async throws + @MainActor func onAdvertisingStateChange(state stateArg: PeripheralAdvertisingState, error errorArg: String?) async throws + @MainActor func onServiceAdded(serviceId serviceIdArg: String, error errorArg: String?) async throws + @MainActor func onMtuChange(deviceId deviceIdArg: String, mtu mtuArg: Int64) async throws + @MainActor func onConnectionStateChange(deviceId deviceIdArg: String, connected connectedArg: Bool) async throws } class UniversalBlePeripheralCallback: UniversalBlePeripheralCallbackProtocol { private let binaryMessenger: FlutterBinaryMessenger @@ -2688,169 +2812,187 @@ class UniversalBlePeripheralCallback: UniversalBlePeripheralCallbackProtocol { var codec: UniversalBlePigeonCodec { return UniversalBlePigeonCodec.shared } - func onReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onReadRequest\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, characteristicIdArg, offsetArg, valueArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - let result: PeripheralReadRequestResult? = nilOrValue(listResponse[0]) - completion(.success(result)) + @MainActor func onReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?) async throws -> PeripheralReadRequestResult? { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onReadRequest\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, characteristicIdArg, offsetArg, valueArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + let result: PeripheralReadRequestResult? = nilOrValue(listResponse[0]) + continuation.resume(returning: result) + } } } } - func onWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onWriteRequest\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, characteristicIdArg, offsetArg, valueArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - let result: PeripheralWriteRequestResult? = nilOrValue(listResponse[0]) - completion(.success(result)) + @MainActor func onWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?) async throws -> PeripheralWriteRequestResult? { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onWriteRequest\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, characteristicIdArg, offsetArg, valueArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + let result: PeripheralWriteRequestResult? = nilOrValue(listResponse[0]) + continuation.resume(returning: result) + } } } } - func onDescriptorReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorReadRequest\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - let result: PeripheralReadRequestResult? = nilOrValue(listResponse[0]) - completion(.success(result)) + @MainActor func onDescriptorReadRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?) async throws -> PeripheralReadRequestResult? { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorReadRequest\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + let result: PeripheralReadRequestResult? = nilOrValue(listResponse[0]) + continuation.resume(returning: result) + } } } } - func onDescriptorWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorWriteRequest\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - let result: PeripheralWriteRequestResult? = nilOrValue(listResponse[0]) - completion(.success(result)) + @MainActor func onDescriptorWriteRequest(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, descriptorId descriptorIdArg: String, offset offsetArg: Int64, value valueArg: FlutterStandardTypedData?) async throws -> PeripheralWriteRequestResult? { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onDescriptorWriteRequest\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, characteristicIdArg, descriptorIdArg, offsetArg, valueArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + let result: PeripheralWriteRequestResult? = nilOrValue(listResponse[0]) + continuation.resume(returning: result) + } } } } - func onCharacteristicSubscriptionChange(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, isSubscribed isSubscribedArg: Bool, name nameArg: String?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onCharacteristicSubscriptionChange\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, characteristicIdArg, isSubscribedArg, nameArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onCharacteristicSubscriptionChange(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, isSubscribed isSubscribedArg: Bool, name nameArg: String?) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onCharacteristicSubscriptionChange\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, characteristicIdArg, isSubscribedArg, nameArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } - func onAdvertisingStateChange(state stateArg: PeripheralAdvertisingState, error errorArg: String?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onAdvertisingStateChange\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([stateArg, errorArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onAdvertisingStateChange(state stateArg: PeripheralAdvertisingState, error errorArg: String?) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onAdvertisingStateChange\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([stateArg, errorArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } - func onServiceAdded(serviceId serviceIdArg: String, error errorArg: String?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onServiceAdded\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([serviceIdArg, errorArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onServiceAdded(serviceId serviceIdArg: String, error errorArg: String?) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onServiceAdded\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([serviceIdArg, errorArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } - func onMtuChange(deviceId deviceIdArg: String, mtu mtuArg: Int64, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onMtuChange\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, mtuArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onMtuChange(deviceId deviceIdArg: String, mtu mtuArg: Int64) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onMtuChange\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, mtuArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } - func onConnectionStateChange(deviceId deviceIdArg: String, connected connectedArg: Bool, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onConnectionStateChange\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, connectedArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) + @MainActor func onConnectionStateChange(deviceId deviceIdArg: String, connected connectedArg: Bool) async throws { + return try await withCheckedThrowingContinuation { continuation in + let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBlePeripheralCallback.onConnectionStateChange\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, connectedArg] as [Any?]) { response in + guard let listResponse = response as? [Any?] else { + continuation.resume(throwing: createConnectionError(withChannelName: channelName)) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + continuation.resume(throwing: PigeonError(code: code, message: message, details: details)) + } else { + continuation.resume() + } } } } diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift index 2d8e0c1a..a04cc581 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift @@ -59,7 +59,7 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne private func completeStopIfNeeded() { guard advertisingState == .stopping else { return } advertisingState = .idle - callbackChannel.onAdvertisingStateChange(state: .idle, error: nil) { _ in } + Task { @MainActor in try? await callbackChannel.onAdvertisingStateChange(state: .idle, error: nil) } } func getReadinessState() throws -> PeripheralReadinessState { @@ -81,7 +81,7 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne func stopAdvertising() throws { advertisingState = .stopping - callbackChannel.onAdvertisingStateChange(state: .stopping, error: nil) { _ in } + Task { @MainActor in try? await callbackChannel.onAdvertisingStateChange(state: .stopping, error: nil) } guard let peripheralManager else { completeStopIfNeeded() return @@ -144,7 +144,7 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne advertisementData[CBAdvertisementDataManufacturerDataKey] = manufacturerField } advertisingState = .starting - callbackChannel.onAdvertisingStateChange(state: .starting, error: nil) { _ in } + Task { @MainActor in try? await callbackChannel.onAdvertisingStateChange(state: .starting, error: nil) } ensurePeripheralManager().startAdvertising(advertisementData) } @@ -180,16 +180,18 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne error: Error? ) { advertisingState = error == nil ? .advertising : .error - callbackChannel.onAdvertisingStateChange( - state: advertisingState, - error: error?.localizedDescription - ) { _ in } + Task { @MainActor in + try? await callbackChannel.onAdvertisingStateChange( + state: advertisingState, + error: error?.localizedDescription + ) + } } nonisolated func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { if peripheral.state != .poweredOn { advertisingState = .idle - callbackChannel.onAdvertisingStateChange(state: .idle, error: nil) { _ in } + Task { @MainActor in try? await callbackChannel.onAdvertisingStateChange(state: .idle, error: nil) } } } @@ -198,10 +200,12 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne didAdd service: CBService, error: Error? ) { - callbackChannel.onServiceAdded( - serviceId: service.uuid.uuidString, - error: error?.localizedDescription - ) { _ in } + Task { @MainActor in + try? await callbackChannel.onServiceAdded( + serviceId: service.uuid.uuidString, + error: error?.localizedDescription + ) + } } nonisolated func peripheralManager( @@ -213,16 +217,20 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne central: central, characteristicId: characteristic.uuid.uuidString ) - callbackChannel.onCharacteristicSubscriptionChange( - deviceId: central.identifier.uuidString, - characteristicId: characteristic.uuid.uuidString, - isSubscribed: true, - name: nil - ) { _ in } - callbackChannel.onMtuChange( - deviceId: central.identifier.uuidString, - mtu: Int64(central.maximumUpdateValueLength) - ) { _ in } + Task { @MainActor in + try? await callbackChannel.onCharacteristicSubscriptionChange( + deviceId: central.identifier.uuidString, + characteristicId: characteristic.uuid.uuidString, + isSubscribed: true, + name: nil + ) + } + Task { @MainActor in + try? await callbackChannel.onMtuChange( + deviceId: central.identifier.uuidString, + mtu: Int64(central.maximumUpdateValueLength) + ) + } } nonisolated func peripheralManager( @@ -234,26 +242,28 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne centralId: central.identifier.uuidString, characteristicId: characteristic.uuid.uuidString ) - callbackChannel.onCharacteristicSubscriptionChange( - deviceId: central.identifier.uuidString, - characteristicId: characteristic.uuid.uuidString, - isSubscribed: false, - name: nil - ) { _ in } + Task { @MainActor in + try? await callbackChannel.onCharacteristicSubscriptionChange( + deviceId: central.identifier.uuidString, + characteristicId: characteristic.uuid.uuidString, + isSubscribed: false, + name: nil + ) + } } nonisolated func peripheralManager( _ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest ) { - callbackChannel.onReadRequest( - deviceId: request.central.identifier.uuidString, - characteristicId: request.characteristic.uuid.uuidString, - offset: Int64(request.offset), - value: request.value?.toFlutterBytes() - ) { readReq in + Task { @MainActor in do { - let result = try readReq.get() + let result = try await callbackChannel.onReadRequest( + deviceId: request.central.identifier.uuidString, + characteristicId: request.characteristic.uuid.uuidString, + offset: Int64(request.offset), + value: request.value?.toFlutterBytes() + ) let status = result?.status?.toCBATTErrorCode() ?? .success guard status == .success else { peripheral.respond(to: request, withResult: status) @@ -282,14 +292,14 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne didReceiveWrite requests: [CBATTRequest] ) { requests.forEach { req in - callbackChannel.onWriteRequest( - deviceId: req.central.identifier.uuidString, - characteristicId: req.characteristic.uuid.uuidString, - offset: Int64(req.offset), - value: req.value?.toFlutterBytes() - ) { writeRes in + Task { @MainActor in do { - let response = try writeRes.get() + let response = try await callbackChannel.onWriteRequest( + deviceId: req.central.identifier.uuidString, + characteristicId: req.characteristic.uuid.uuidString, + offset: Int64(req.offset), + value: req.value?.toFlutterBytes() + ) let status = response?.status?.toCBATTErrorCode() ?? .success peripheral.respond(to: req, withResult: status) } catch { diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift index cb69ce7b..a7d6250c 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift @@ -125,7 +125,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } #endif - func getBluetoothAvailabilityState(completion: @escaping (Result) -> Void) { + func getBluetoothAvailabilityStateImpl(completion: @escaping (Result) -> Void) { #if os(iOS) if Self.hasBluetoothCentralBackgroundMode, !Self.hasBluetoothPermission { completion(.success(Self.availabilityStateFromAuthorization)) @@ -144,7 +144,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral return CBCentralManager.authorization == .allowedAlways } - func requestPermissions(withAndroidFineLocation _: Bool, completion: @escaping (Result) -> Void) { + func requestPermissionsImpl(withAndroidFineLocation _: Bool, completion: @escaping (Result) -> Void) { if manager.state != .unknown { completePermissionRequest(completion: completion) } else { @@ -165,11 +165,11 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } } - func enableBluetooth(completion: @escaping (Result) -> Void) { + func enableBluetoothImpl(completion: @escaping (Result) -> Void) { completion(Result.failure(createFlutterError(code: .notSupported))) } - func disableBluetooth(completion: @escaping (Result) -> Void) { + func disableBluetoothImpl(completion: @escaping (Result) -> Void) { completion(Result.failure(createFlutterError(code: .notSupported))) } @@ -212,7 +212,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral return isManageScanning } - func requestDevice(options: AppleAccessorySetupOptions, completion: @escaping (Result) -> Void) { + func requestDeviceImpl(options: AppleAccessorySetupOptions, completion: @escaping (Result) -> Void) { #if os(iOS) guard #available(iOS 18.0, *) else { completion(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit requires iOS 18 or later"))) @@ -280,7 +280,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func disconnect(deviceId: String) throws { autoConnectDevices.remove(deviceId) guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - callbackChannel.onConnectionChanged(deviceId: deviceId, connected: false, error: nil, errorCode: nil, nativeErrorCode: nil) { _ in } + Task { @MainActor in try? await callbackChannel.onConnectionChanged(deviceId: deviceId, connected: false, error: nil, errorCode: nil, nativeErrorCode: nil) } cleanUpConnection(deviceId: deviceId) return } @@ -318,7 +318,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral discoverServicesFutures.failAndRemoveAll(matching: deviceId, with: error) } - func discoverServices(deviceId: String, withDescriptors: Bool, platformConfig _: DiscoverServicesPlatformConfig?, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) { + func discoverServicesImpl(deviceId: String, withDescriptors: Bool, platformConfig _: DiscoverServicesPlatformConfig?, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) { guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { completion( Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)")) @@ -366,7 +366,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral discovery.startDiscovery() } - func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, completion: @escaping (Result) -> Void) { + func setNotifiableImpl(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, completion: @escaping (Result) -> Void) { UniversalBleLogger.shared.logDebug("SET_NOTIFY -> \(deviceId) \(service) \(characteristic) input=\(bleInputProperty)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) @@ -393,7 +393,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral characteristicNotifyFutures.append(CharacteristicNotifyFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion)) } - func readValue(deviceId: String, service: String, characteristic: String, completion: @escaping (Result) -> Void) { + func readValueImpl(deviceId: String, service: String, characteristic: String, completion: @escaping (Result) -> Void) { UniversalBleLogger.shared.logDebug("READ -> \(deviceId) \(service) \(characteristic)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) @@ -411,7 +411,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral characteristicReadFutures.append(CharacteristicReadFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion)) } - func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: BleOutputProperty, completion: @escaping (Result) -> Void) { + func writeValueImpl(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: BleOutputProperty, completion: @escaping (Result) -> Void) { UniversalBleLogger.shared.logDebug("WRITE -> \(deviceId) \(service) \(characteristic) len=\(value.data.count) property=\(bleOutputProperty)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) @@ -456,7 +456,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } } - func readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, completion: @escaping (Result) -> Void) { + func readDescriptorValueImpl(deviceId: String, service: String, characteristic: String, descriptor: String, completion: @escaping (Result) -> Void) { UniversalBleLogger.shared.logDebug("READ_DESCRIPTOR -> \(deviceId) \(service) \(characteristic) \(descriptor)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) @@ -476,7 +476,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral )) } - func writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData, completion: @escaping (Result) -> Void) { + func writeDescriptorValueImpl(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData, completion: @escaping (Result) -> Void) { UniversalBleLogger.shared.logDebug("WRITE_DESCRIPTOR -> \(deviceId) \(service) \(characteristic) \(descriptor) len=\(value.data.count)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) @@ -517,7 +517,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral )) } - func requestMtu(deviceId: String, expectedMtu _: Int64, completion: @escaping (Result) -> Void) { + func requestMtuImpl(deviceId: String, expectedMtu _: Int64, completion: @escaping (Result) -> Void) { UniversalBleLogger.shared.logDebug("REQUEST_MTU -> \(deviceId)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) @@ -529,7 +529,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral completion(Result.success(mtuResult)) } - func requestConnectionPriority( + func requestConnectionPriorityImpl( deviceId _: String, priority _: BleConnectionPriority, completion: @escaping (Result) -> Void @@ -537,7 +537,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral completion(.failure(createFlutterError(code: .notSupported, message: "requestConnectionPriority is not supported on Apple platforms"))) } - func readRssi(deviceId: String, completion: @escaping (Result) -> Void) { + func readRssiImpl(deviceId: String, completion: @escaping (Result) -> Void) { UniversalBleLogger.shared.logDebug("READ_RSSI -> \(deviceId)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) @@ -547,15 +547,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral rssiReadFutures.append(RssiReadFuture(deviceId: deviceId, result: completion)) } - func isPaired(deviceId _: String, completion: @escaping (Result) -> Void) { + func isPairedImpl(deviceId _: String, completion: @escaping (Result) -> Void) { completion(Result.failure(createFlutterError(code: .notSupported))) } - func pair(deviceId _: String, completion: @escaping (Result) -> Void) { + func pairImpl(deviceId _: String, completion: @escaping (Result) -> Void) { completion(Result.failure(createFlutterError(code: .notImplemented))) } - func unPair(deviceId: String, completion: @escaping (Result) -> Void) { + func unPairImpl(deviceId: String, completion: @escaping (Result) -> Void) { #if os(iOS) guard #available(iOS 18.0, *) else { completion(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit requires iOS 18 or later"))) @@ -579,7 +579,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } #endif - func getSystemDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) { + func getSystemDevicesImpl(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) { var servicesFilter = withServices if servicesFilter.isEmpty { UniversalBleLogger.shared.logInfo("No services filter was set for getting system connected devices. Using default services...") @@ -619,7 +619,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // Notify Dart if already connected; on a cold relaunch the Dart layer // re-subscribes on resume using the cached peripheral. if peripheral.state == .connected { - callbackChannel.onConnectionChanged(deviceId: deviceId, connected: true, error: nil, errorCode: nil, nativeErrorCode: nil) { _ in } + Task { @MainActor in try? await callbackChannel.onConnectionChanged(deviceId: deviceId, connected: true, error: nil, errorCode: nil, nativeErrorCode: nil) } } } } @@ -627,7 +627,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func centralManagerDidUpdateState(_ central: CBCentralManager) { let state = central.state.toAvailabilityState() - callbackChannel.onAvailabilityChanged(state: state) { _ in } + Task { @MainActor in try? await callbackChannel.onAvailabilityChanged(state: state) } // Complete Pending state handler availabilityStateUpdateHandlers.removeAll { handler in handler(.success(state)) @@ -682,29 +682,33 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } let timestampMicroseconds = Int64(Date().timeIntervalSince1970 * 1_000_000) - callbackChannel.onScanResult(result: UniversalBleScanResult( - deviceId: peripheral.uuid.uuidString, - name: displayName, - isPaired: nil, - rssi: RSSI as? Int64, - manufacturerDataList: manufacturerDataList, - serviceData: serviceData, - services: services?.map { $0.uuidStr }, - timestamp: timestampMicroseconds / 1000, - timestampMicroseconds: timestampMicroseconds, - connectable: connectable - )) { _ in } + Task { @MainActor in + try? await callbackChannel.onScanResult(result: UniversalBleScanResult( + deviceId: peripheral.uuid.uuidString, + name: displayName, + isPaired: nil, + rssi: RSSI as? Int64, + manufacturerDataList: manufacturerDataList, + serviceData: serviceData, + services: services?.map { $0.uuidStr }, + timestamp: timestampMicroseconds / 1000, + timestampMicroseconds: timestampMicroseconds, + connectable: connectable + )) + } } public func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { - callbackChannel.onConnectionChanged(deviceId: peripheral.uuid.uuidString, connected: true, error: nil, errorCode: nil, nativeErrorCode: nil) { _ in } + Task { @MainActor in try? await callbackChannel.onConnectionChanged(deviceId: peripheral.uuid.uuidString, connected: true, error: nil, errorCode: nil, nativeErrorCode: nil) } } private func handlePeripheralDisconnection(deviceId: String, error: Error?) { autoConnectDevices.remove(deviceId) - callbackChannel.onConnectionChanged( - deviceId: deviceId, connected: false, error: error?.localizedDescription, errorCode: error?.connectionErrorCode, nativeErrorCode: error?.nativeCode - ) { _ in } + Task { @MainActor in + try? await callbackChannel.onConnectionChanged( + deviceId: deviceId, connected: false, error: error?.localizedDescription, errorCode: error?.connectionErrorCode, nativeErrorCode: error?.nativeCode + ) + } cleanUpConnection(deviceId: deviceId) } @@ -733,9 +737,11 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } public func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - callbackChannel.onConnectionChanged( - deviceId: peripheral.uuid.uuidString, connected: false, error: error?.localizedDescription, errorCode: error?.connectionErrorCode, nativeErrorCode: error?.nativeCode - ) { _ in } + Task { @MainActor in + try? await callbackChannel.onConnectionChanged( + deviceId: peripheral.uuid.uuidString, connected: false, error: error?.localizedDescription, errorCode: error?.connectionErrorCode, nativeErrorCode: error?.nativeCode + ) + } cleanUpConnection(deviceId: peripheral.uuid.uuidString) } @@ -873,12 +879,14 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // Update callbackChannel if notifying if characteristic.isNotifying { if let characteristicValue = characteristic.value { - callbackChannel.onValueChanged( - deviceId: peripheral.uuid.uuidString, - characteristicId: characteristic.uuid.uuidStr, - value: FlutterStandardTypedData(bytes: characteristicValue), - timestamp: Int64(Date().timeIntervalSince1970 * 1000) - ) { _ in } + Task { @MainActor in + try? await callbackChannel.onValueChanged( + deviceId: peripheral.uuid.uuidString, + characteristicId: characteristic.uuid.uuidStr, + value: FlutterStandardTypedData(bytes: characteristicValue), + timestamp: Int64(Date().timeIntervalSince1970 * 1000) + ) + } } } @@ -919,6 +927,120 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } } + // MARK: - Pigeon 29 async bridges + // + // The generated protocol is `async` since Pigeon 29. The previous + // completion-based implementations are kept (renamed `…Impl`) and bridged + // here so the cooperate behaviour is unchanged. + + func getBluetoothAvailabilityState() async throws -> AvailabilityState { + try await withCheckedThrowingContinuation { continuation in + getBluetoothAvailabilityStateImpl { continuation.resume(with: $0) } + } + } + + func requestPermissions(withAndroidFineLocation fineLocation: Bool) async throws { + try await withCheckedThrowingContinuation { continuation in + requestPermissionsImpl(withAndroidFineLocation: fineLocation) { continuation.resume(with: $0) } + } + } + + func enableBluetooth() async throws -> Bool { + try await withCheckedThrowingContinuation { continuation in + enableBluetoothImpl { continuation.resume(with: $0) } + } + } + + func disableBluetooth() async throws -> Bool { + try await withCheckedThrowingContinuation { continuation in + disableBluetoothImpl { continuation.resume(with: $0) } + } + } + + func requestDevice(options: AppleAccessorySetupOptions) async throws -> UniversalDevicePickerResult { + try await withCheckedThrowingContinuation { continuation in + requestDeviceImpl(options: options) { continuation.resume(with: $0) } + } + } + + func discoverServices(deviceId: String, withDescriptors: Bool, platformConfig: DiscoverServicesPlatformConfig?) async throws -> [UniversalBleService] { + try await withCheckedThrowingContinuation { continuation in + discoverServicesImpl(deviceId: deviceId, withDescriptors: withDescriptors, platformConfig: platformConfig) { continuation.resume(with: $0) } + } + } + + func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty) async throws { + try await withCheckedThrowingContinuation { continuation in + setNotifiableImpl(deviceId: deviceId, service: service, characteristic: characteristic, bleInputProperty: bleInputProperty) { continuation.resume(with: $0) } + } + } + + func readValue(deviceId: String, service: String, characteristic: String) async throws -> FlutterStandardTypedData { + try await withCheckedThrowingContinuation { continuation in + readValueImpl(deviceId: deviceId, service: service, characteristic: characteristic) { continuation.resume(with: $0) } + } + } + + func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: BleOutputProperty) async throws { + try await withCheckedThrowingContinuation { continuation in + writeValueImpl(deviceId: deviceId, service: service, characteristic: characteristic, value: value, bleOutputProperty: bleOutputProperty) { continuation.resume(with: $0) } + } + } + + func readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String) async throws -> FlutterStandardTypedData { + try await withCheckedThrowingContinuation { continuation in + readDescriptorValueImpl(deviceId: deviceId, service: service, characteristic: characteristic, descriptor: descriptor) { continuation.resume(with: $0) } + } + } + + func writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData) async throws { + try await withCheckedThrowingContinuation { continuation in + writeDescriptorValueImpl(deviceId: deviceId, service: service, characteristic: characteristic, descriptor: descriptor, value: value) { continuation.resume(with: $0) } + } + } + + func requestMtu(deviceId: String, expectedMtu: Int64) async throws -> Int64 { + try await withCheckedThrowingContinuation { continuation in + requestMtuImpl(deviceId: deviceId, expectedMtu: expectedMtu) { continuation.resume(with: $0) } + } + } + + func requestConnectionPriority(deviceId: String, priority: BleConnectionPriority) async throws { + try await withCheckedThrowingContinuation { continuation in + requestConnectionPriorityImpl(deviceId: deviceId, priority: priority) { continuation.resume(with: $0) } + } + } + + func readRssi(deviceId: String) async throws -> Int64 { + try await withCheckedThrowingContinuation { continuation in + readRssiImpl(deviceId: deviceId) { continuation.resume(with: $0) } + } + } + + func isPaired(deviceId: String) async throws -> Bool { + try await withCheckedThrowingContinuation { continuation in + isPairedImpl(deviceId: deviceId) { continuation.resume(with: $0) } + } + } + + func pair(deviceId: String) async throws -> Bool { + try await withCheckedThrowingContinuation { continuation in + pairImpl(deviceId: deviceId) { continuation.resume(with: $0) } + } + } + + func unPair(deviceId: String) async throws { + try await withCheckedThrowingContinuation { continuation in + unPairImpl(deviceId: deviceId) { continuation.resume(with: $0) } + } + } + + func getSystemDevices(withServices: [String]) async throws -> [UniversalBleScanResult] { + try await withCheckedThrowingContinuation { continuation in + getSystemDevicesImpl(withServices: withServices) { continuation.resume(with: $0) } + } + } + } #if os(iOS) diff --git a/lib/src/universal_ble.g.dart b/lib/src/universal_ble.g.dart index 7c20d1aa..756c56d9 100644 --- a/lib/src/universal_ble.g.dart +++ b/lib/src/universal_ble.g.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// Autogenerated from Pigeon (v29.0.10), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: unused_import, unused_shown_name // ignore_for_file: type=lint @@ -113,7 +113,7 @@ enum BleLogLevel { warning, info, debug, - verbose, + verbose; } enum AvailabilityState { @@ -122,25 +122,25 @@ enum AvailabilityState { unsupported, unauthorized, poweredOff, - poweredOn, + poweredOn; } enum BleConnectionState { connected, disconnected, connecting, - disconnecting, + disconnecting; } enum BleInputProperty { disabled, notification, - indication, + indication; } enum BleOutputProperty { withResponse, - withoutResponse, + withoutResponse; } /// Connection priority hint passed to [requestConnectionPriority]. @@ -154,14 +154,14 @@ enum BleConnectionPriority { highPerformance, /// Power-optimized interval (~100-125 ms). Android constant: 2. - lowPower, + lowPower; } enum AndroidScanMode { balanced, lowLatency, lowPower, - opportunistic, + opportunistic; } /// Mirrors `android.bluetooth.le.ScanSettings#setCallbackType`. Pass any @@ -180,20 +180,20 @@ enum AndroidScanCallbackType { allMatches, firstMatch, matchLost, - allMatchesAutoBatch, + allMatchesAutoBatch; } /// Mirrors `android.bluetooth.le.ScanSettings#setMatchMode`. enum AndroidScanMatchMode { aggressive, - sticky, + sticky; } /// Mirrors `android.bluetooth.le.ScanSettings#setNumOfMatches`. enum AndroidScanNumOfMatches { one, few, - max, + max; } enum CharacteristicProperty { @@ -204,7 +204,7 @@ enum CharacteristicProperty { notify, indicate, authenticatedSignedWrites, - extendedProperties, + extendedProperties; } enum PeripheralReadinessState { @@ -212,14 +212,14 @@ enum PeripheralReadinessState { ready, bluetoothOff, unauthorized, - unsupported, + unsupported; } enum PeripheralAttributePermission { readable, writeable, readEncryptionRequired, - writeEncryptionRequired, + writeEncryptionRequired; } enum PeripheralAdvertisingState { @@ -227,7 +227,7 @@ enum PeripheralAdvertisingState { starting, advertising, stopping, - error, + error; } /// Unified error codes for all platforms @@ -292,7 +292,7 @@ enum UniversalBleErrorCode { alreadyUnpaired, scanFailed, stoppingScanInProgress, - webBluetoothGloballyDisabled, + webBluetoothGloballyDisabled; } /// Shared models & enums @@ -392,6 +392,11 @@ class UniversalBleScanResult { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'UniversalBleScanResult(deviceId: $deviceId, name: $name, isPaired: $isPaired, rssi: $rssi, manufacturerDataList: $manufacturerDataList, serviceData: $serviceData, services: $services, timestamp: $timestamp, timestampMicroseconds: $timestampMicroseconds, connectable: $connectable)'; + } } /// The device a platform's native picker returned. @@ -443,6 +448,11 @@ class UniversalDevicePickerResult { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'UniversalDevicePickerResult(deviceId: $deviceId, name: $name)'; + } } /// Central/GATT models @@ -492,6 +502,11 @@ class UniversalBleService { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'UniversalBleService(uuid: $uuid, characteristics: $characteristics)'; + } } class UniversalBleCharacteristic { @@ -546,6 +561,11 @@ class UniversalBleCharacteristic { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'UniversalBleCharacteristic(uuid: $uuid, properties: $properties, descriptors: $descriptors)'; + } } class UniversalBleDescriptor { @@ -587,6 +607,11 @@ class UniversalBleDescriptor { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'UniversalBleDescriptor(uuid: $uuid)'; + } } /// Link-layer connection parameters reported by Android [onConnectionUpdated]. @@ -657,6 +682,11 @@ class BleConnectionParametersChange { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'BleConnectionParametersChange(deviceId: $deviceId, interval: $interval, latency: $latency, supervisionTimeout: $supervisionTimeout, status: $status)'; + } } /// Scan models @@ -763,6 +793,11 @@ class AndroidOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'AndroidOptions(requestLocationPermission: $requestLocationPermission, scanMode: $scanMode, reportDelayMillis: $reportDelayMillis, callbackType: $callbackType, matchMode: $matchMode, numOfMatches: $numOfMatches, legacy: $legacy)'; + } } class UniversalScanConfig { @@ -814,6 +849,11 @@ class UniversalScanConfig { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'UniversalScanConfig(android: $android, skipDuplicates: $skipDuplicates)'; + } } class UniversalScanFilter { @@ -877,6 +917,11 @@ class UniversalScanFilter { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'UniversalScanFilter(withServices: $withServices, withNamePrefix: $withNamePrefix, withManufacturerData: $withManufacturerData, withServiceData: $withServiceData)'; + } } class ManufacturerDataFilter { @@ -935,6 +980,11 @@ class ManufacturerDataFilter { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'ManufacturerDataFilter(companyIdentifier: $companyIdentifier, payloadPrefix: $payloadPrefix, payloadMask: $payloadMask)'; + } } class UniversalManufacturerData { @@ -983,6 +1033,11 @@ class UniversalManufacturerData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'UniversalManufacturerData(companyIdentifier: $companyIdentifier, data: $data)'; + } } /// Apple options for `connect`. @@ -1055,6 +1110,11 @@ class AppleConnectionOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'AppleConnectionOptions(notifyOnConnection: $notifyOnConnection, notifyOnDisconnection: $notifyOnDisconnection, notifyOnNotification: $notifyOnNotification)'; + } } /// One accessory the AccessorySetupKit picker can offer. Each item is shown as @@ -1135,6 +1195,11 @@ class AppleAccessoryPickerItem { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'AppleAccessoryPickerItem(displayName: $displayName, imageAsset: $imageAsset, serviceUuid: $serviceUuid, nameSubstring: $nameSubstring, requiresImmediateRange: $requiresImmediateRange, supportsBluetoothPairing: $supportsBluetoothPairing)'; + } } /// iOS 18+ options for discovering and authorizing Bluetooth accessories with @@ -1180,6 +1245,11 @@ class AppleAccessorySetupOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'AppleAccessorySetupOptions(items: $items)'; + } } class AndroidConnectionOptions { @@ -1229,6 +1299,11 @@ class AndroidConnectionOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'AndroidConnectionOptions(closeGattOnDetach: $closeGattOnDetach)'; + } } class ConnectionPlatformConfig { @@ -1277,6 +1352,11 @@ class ConnectionPlatformConfig { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'ConnectionPlatformConfig(apple: $apple, android: $android)'; + } } class AndroidDiscoverServicesOptions { @@ -1325,6 +1405,11 @@ class AndroidDiscoverServicesOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'AndroidDiscoverServicesOptions(clearGattCache: $clearGattCache)'; + } } /// Platform-specific options for `discoverServices`. Ignored on platforms @@ -1369,6 +1454,11 @@ class DiscoverServicesPlatformConfig { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'DiscoverServicesPlatformConfig(android: $android)'; + } } class PeripheralAndroidOptions { @@ -1424,6 +1514,11 @@ class PeripheralAndroidOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PeripheralAndroidOptions(addManufacturerDataInScanResponse: $addManufacturerDataInScanResponse, addServicesInScanResponse: $addServicesInScanResponse)'; + } } class PeripheralPlatformConfig { @@ -1466,6 +1561,11 @@ class PeripheralPlatformConfig { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PeripheralPlatformConfig(android: $android)'; + } } /// Peripheral/GATT server models @@ -1521,6 +1621,11 @@ class PeripheralService { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PeripheralService(uuid: $uuid, primary: $primary, characteristics: $characteristics)'; + } } class PeripheralCharacteristic { @@ -1588,6 +1693,11 @@ class PeripheralCharacteristic { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PeripheralCharacteristic(uuid: $uuid, properties: $properties, permissions: $permissions, descriptors: $descriptors, value: $value)'; + } } class PeripheralDescriptor { @@ -1642,6 +1752,11 @@ class PeripheralDescriptor { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PeripheralDescriptor(uuid: $uuid, value: $value, permissions: $permissions)'; + } } class PeripheralReadRequestResult { @@ -1696,6 +1811,11 @@ class PeripheralReadRequestResult { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PeripheralReadRequestResult(value: $value, offset: $offset, status: $status)'; + } } class PeripheralWriteRequestResult { @@ -1750,6 +1870,11 @@ class PeripheralWriteRequestResult { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes int get hashCode => _deepHash([runtimeType, ..._toList()]); + + @override + String toString() { + return 'PeripheralWriteRequestResult(value: $value, offset: $offset, status: $status)'; + } } class _PigeonCodec extends StandardMessageCodec { @@ -1994,16 +2119,17 @@ class _PigeonCodec extends StandardMessageCodec { /// ------------------------------------------------------------ /// Flutter -> Native (central) class UniversalBlePlatformChannel { - /// Constructor for [UniversalBlePlatformChannel]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default + /// Constructor for [UniversalBlePlatformChannel]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - UniversalBlePlatformChannel( - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, + UniversalBlePlatformChannel({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; + final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; @@ -2726,16 +2852,17 @@ abstract class UniversalBleCallbackChannel { /// Flutter -> Native (peripheral) class UniversalBlePeripheralChannel { - /// Constructor for [UniversalBlePeripheralChannel]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default + /// Constructor for [UniversalBlePeripheralChannel]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - UniversalBlePeripheralChannel( - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, + UniversalBlePeripheralChannel({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; + final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; @@ -2967,16 +3094,17 @@ class UniversalBlePeripheralChannel { /// Flutter -> Native (Android only) class UniversalBleAndroidChannel { - /// Constructor for [UniversalBleAndroidChannel]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default + /// Constructor for [UniversalBleAndroidChannel]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - UniversalBleAndroidChannel( - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, + UniversalBleAndroidChannel({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; + final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 3cbcc1cc..ead7272f 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// Autogenerated from Pigeon (v29.0.10), do not edit directly. // See also: https://pub.dev/packages/pigeon #undef _HAS_EXCEPTIONS @@ -14,6 +14,7 @@ #include #include #include +#include #include namespace universal_ble { @@ -224,6 +225,99 @@ size_t PigeonInternalDeepHash(const ::flutter::EncodableValue& v) { return result; } +template +std::string PigeonInternalToString(const T& v); + +std::string PigeonInternalToString(const bool& v); + +template +std::string PigeonInternalToString(const std::vector& v); + +template +std::string PigeonInternalToString(const std::map& v); + +template +std::string PigeonInternalToString(const std::optional& v); + +template +std::string PigeonInternalToString(const std::unique_ptr& v); + +std::string PigeonInternalToString(const ::flutter::EncodableValue& v); + +template +std::string PigeonInternalToString(const T& v) { + std::stringstream ss; + if constexpr (std::is_enum_v) { + ss << static_cast(v); + } else { + ss << v; + } + return ss.str(); +} + +std::string PigeonInternalToString(const bool& v) { + return v ? "true" : "false"; +} + +template +std::string PigeonInternalToString(const std::vector& v) { + std::stringstream ss; + ss << "["; + for (size_t i = 0; i < v.size(); ++i) { + if (i > 0) { + ss << ", "; + } + ss << PigeonInternalToString(v[i]); + } + ss << "]"; + return ss.str(); +} + +template +std::string PigeonInternalToString(const std::map& v) { + std::stringstream ss; + ss << "{"; + bool first = true; + for (const auto& kv : v) { + if (!first) { + ss << ", "; + } + first = false; + ss << PigeonInternalToString(kv.first) << ": " << PigeonInternalToString(kv.second); + } + ss << "}"; + return ss.str(); +} + +template +std::string PigeonInternalToString(const std::optional& v) { + return v ? PigeonInternalToString(*v) : "null"; +} + +template +std::string PigeonInternalToString(const std::unique_ptr& v) { + return v ? PigeonInternalToString(*v) : "null"; +} + +std::string PigeonInternalToString(const ::flutter::EncodableValue& v) { + return std::visit( + [](const auto& val) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return std::string("null"); + } else if constexpr (std::is_same_v) { + return val ? std::string("true") : std::string("false"); + } else if constexpr (std::is_same_v) { + return "\"" + val + "\""; + } else if constexpr (std::is_same_v) { + return std::string("[custom]"); + } else { + return PigeonInternalToString(val); + } + }, + v); +} + } // namespace // UniversalBleScanResult @@ -459,6 +553,79 @@ size_t UniversalBleScanResult::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const UniversalBleScanResult& obj) { + os << "UniversalBleScanResult("; + os << "device_id: "; + os << PigeonInternalToString(obj.device_id_); + os << ", name: "; + if (obj.name_) { + os << PigeonInternalToString(*obj.name_); + } + else { + os << "null"; + } + os << ", is_paired: "; + if (obj.is_paired_) { + os << PigeonInternalToString(*obj.is_paired_); + } + else { + os << "null"; + } + os << ", rssi: "; + if (obj.rssi_) { + os << PigeonInternalToString(*obj.rssi_); + } + else { + os << "null"; + } + os << ", manufacturer_data_list: "; + if (obj.manufacturer_data_list_) { + os << PigeonInternalToString(*obj.manufacturer_data_list_); + } + else { + os << "null"; + } + os << ", service_data: "; + if (obj.service_data_) { + os << PigeonInternalToString(*obj.service_data_); + } + else { + os << "null"; + } + os << ", services: "; + if (obj.services_) { + os << PigeonInternalToString(*obj.services_); + } + else { + os << "null"; + } + os << ", timestamp: "; + if (obj.timestamp_) { + os << PigeonInternalToString(*obj.timestamp_); + } + else { + os << "null"; + } + os << ", timestamp_microseconds: "; + if (obj.timestamp_microseconds_) { + os << PigeonInternalToString(*obj.timestamp_microseconds_); + } + else { + os << "null"; + } + os << ", connectable: "; + if (obj.connectable_) { + os << PigeonInternalToString(*obj.connectable_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const UniversalBleScanResult& v) { return v.Hash(); } @@ -529,6 +696,23 @@ size_t UniversalDevicePickerResult::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const UniversalDevicePickerResult& obj) { + os << "UniversalDevicePickerResult("; + os << "device_id: "; + os << PigeonInternalToString(obj.device_id_); + os << ", name: "; + if (obj.name_) { + os << PigeonInternalToString(*obj.name_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const UniversalDevicePickerResult& v) { return v.Hash(); } @@ -599,6 +783,23 @@ size_t UniversalBleService::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const UniversalBleService& obj) { + os << "UniversalBleService("; + os << "uuid: "; + os << PigeonInternalToString(obj.uuid_); + os << ", characteristics: "; + if (obj.characteristics_) { + os << PigeonInternalToString(*obj.characteristics_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const UniversalBleService& v) { return v.Hash(); } @@ -673,6 +874,20 @@ size_t UniversalBleCharacteristic::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const UniversalBleCharacteristic& obj) { + os << "UniversalBleCharacteristic("; + os << "uuid: "; + os << PigeonInternalToString(obj.uuid_); + os << ", properties: "; + os << PigeonInternalToString(obj.properties_); + os << ", descriptors: "; + os << PigeonInternalToString(obj.descriptors_); + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const UniversalBleCharacteristic& v) { return v.Hash(); } @@ -718,6 +933,16 @@ size_t UniversalBleDescriptor::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const UniversalBleDescriptor& obj) { + os << "UniversalBleDescriptor("; + os << "uuid: "; + os << PigeonInternalToString(obj.uuid_); + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const UniversalBleDescriptor& v) { return v.Hash(); } @@ -820,6 +1045,24 @@ size_t BleConnectionParametersChange::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const BleConnectionParametersChange& obj) { + os << "BleConnectionParametersChange("; + os << "device_id: "; + os << PigeonInternalToString(obj.device_id_); + os << ", interval: "; + os << PigeonInternalToString(obj.interval_); + os << ", latency: "; + os << PigeonInternalToString(obj.latency_); + os << ", supervision_timeout: "; + os << PigeonInternalToString(obj.supervision_timeout_); + os << ", status: "; + os << PigeonInternalToString(obj.status_); + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const BleConnectionParametersChange& v) { return v.Hash(); } @@ -1001,6 +1244,63 @@ size_t AndroidOptions::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const AndroidOptions& obj) { + os << "AndroidOptions("; + os << "request_location_permission: "; + if (obj.request_location_permission_) { + os << PigeonInternalToString(*obj.request_location_permission_); + } + else { + os << "null"; + } + os << ", scan_mode: "; + if (obj.scan_mode_) { + os << PigeonInternalToString(*obj.scan_mode_); + } + else { + os << "null"; + } + os << ", report_delay_millis: "; + if (obj.report_delay_millis_) { + os << PigeonInternalToString(*obj.report_delay_millis_); + } + else { + os << "null"; + } + os << ", callback_type: "; + if (obj.callback_type_) { + os << PigeonInternalToString(*obj.callback_type_); + } + else { + os << "null"; + } + os << ", match_mode: "; + if (obj.match_mode_) { + os << PigeonInternalToString(*obj.match_mode_); + } + else { + os << "null"; + } + os << ", num_of_matches: "; + if (obj.num_of_matches_) { + os << PigeonInternalToString(*obj.num_of_matches_); + } + else { + os << "null"; + } + os << ", legacy: "; + if (obj.legacy_) { + os << PigeonInternalToString(*obj.legacy_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const AndroidOptions& v) { return v.Hash(); } @@ -1087,6 +1387,28 @@ size_t UniversalScanConfig::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const UniversalScanConfig& obj) { + os << "UniversalScanConfig("; + os << "android: "; + if (obj.android_) { + os << *obj.android_; + } + else { + os << "null"; + } + os << ", skip_duplicates: "; + if (obj.skip_duplicates_) { + os << PigeonInternalToString(*obj.skip_duplicates_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const UniversalScanConfig& v) { return v.Hash(); } @@ -1175,6 +1497,22 @@ size_t UniversalScanFilter::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const UniversalScanFilter& obj) { + os << "UniversalScanFilter("; + os << "with_services: "; + os << PigeonInternalToString(obj.with_services_); + os << ", with_name_prefix: "; + os << PigeonInternalToString(obj.with_name_prefix_); + os << ", with_manufacturer_data: "; + os << PigeonInternalToString(obj.with_manufacturer_data_); + os << ", with_service_data: "; + os << PigeonInternalToString(obj.with_service_data_); + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const UniversalScanFilter& v) { return v.Hash(); } @@ -1266,6 +1604,30 @@ size_t ManufacturerDataFilter::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const ManufacturerDataFilter& obj) { + os << "ManufacturerDataFilter("; + os << "company_identifier: "; + os << PigeonInternalToString(obj.company_identifier_); + os << ", payload_prefix: "; + if (obj.payload_prefix_) { + os << PigeonInternalToString(*obj.payload_prefix_); + } + else { + os << "null"; + } + os << ", payload_mask: "; + if (obj.payload_mask_) { + os << PigeonInternalToString(*obj.payload_mask_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const ManufacturerDataFilter& v) { return v.Hash(); } @@ -1326,6 +1688,18 @@ size_t UniversalManufacturerData::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const UniversalManufacturerData& obj) { + os << "UniversalManufacturerData("; + os << "company_identifier: "; + os << PigeonInternalToString(obj.company_identifier_); + os << ", data: "; + os << PigeonInternalToString(obj.data_); + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const UniversalManufacturerData& v) { return v.Hash(); } @@ -1423,6 +1797,35 @@ size_t AppleConnectionOptions::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const AppleConnectionOptions& obj) { + os << "AppleConnectionOptions("; + os << "notify_on_connection: "; + if (obj.notify_on_connection_) { + os << PigeonInternalToString(*obj.notify_on_connection_); + } + else { + os << "null"; + } + os << ", notify_on_disconnection: "; + if (obj.notify_on_disconnection_) { + os << PigeonInternalToString(*obj.notify_on_disconnection_); + } + else { + os << "null"; + } + os << ", notify_on_notification: "; + if (obj.notify_on_notification_) { + os << PigeonInternalToString(*obj.notify_on_notification_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const AppleConnectionOptions& v) { return v.Hash(); } @@ -1568,6 +1971,41 @@ size_t AppleAccessoryPickerItem::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const AppleAccessoryPickerItem& obj) { + os << "AppleAccessoryPickerItem("; + os << "display_name: "; + os << PigeonInternalToString(obj.display_name_); + os << ", image_asset: "; + os << PigeonInternalToString(obj.image_asset_); + os << ", service_uuid: "; + os << PigeonInternalToString(obj.service_uuid_); + os << ", name_substring: "; + if (obj.name_substring_) { + os << PigeonInternalToString(*obj.name_substring_); + } + else { + os << "null"; + } + os << ", requires_immediate_range: "; + if (obj.requires_immediate_range_) { + os << PigeonInternalToString(*obj.requires_immediate_range_); + } + else { + os << "null"; + } + os << ", supports_bluetooth_pairing: "; + if (obj.supports_bluetooth_pairing_) { + os << PigeonInternalToString(*obj.supports_bluetooth_pairing_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const AppleAccessoryPickerItem& v) { return v.Hash(); } @@ -1613,6 +2051,16 @@ size_t AppleAccessorySetupOptions::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const AppleAccessorySetupOptions& obj) { + os << "AppleAccessorySetupOptions("; + os << "items: "; + os << PigeonInternalToString(obj.items_); + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const AppleAccessorySetupOptions& v) { return v.Hash(); } @@ -1667,6 +2115,21 @@ size_t AndroidConnectionOptions::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const AndroidConnectionOptions& obj) { + os << "AndroidConnectionOptions("; + os << "close_gatt_on_detach: "; + if (obj.close_gatt_on_detach_) { + os << PigeonInternalToString(*obj.close_gatt_on_detach_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const AndroidConnectionOptions& v) { return v.Hash(); } @@ -1753,6 +2216,28 @@ size_t ConnectionPlatformConfig::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const ConnectionPlatformConfig& obj) { + os << "ConnectionPlatformConfig("; + os << "apple: "; + if (obj.apple_) { + os << *obj.apple_; + } + else { + os << "null"; + } + os << ", android: "; + if (obj.android_) { + os << *obj.android_; + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const ConnectionPlatformConfig& v) { return v.Hash(); } @@ -1807,6 +2292,21 @@ size_t AndroidDiscoverServicesOptions::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const AndroidDiscoverServicesOptions& obj) { + os << "AndroidDiscoverServicesOptions("; + os << "clear_gatt_cache: "; + if (obj.clear_gatt_cache_) { + os << PigeonInternalToString(*obj.clear_gatt_cache_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const AndroidDiscoverServicesOptions& v) { return v.Hash(); } @@ -1869,6 +2369,21 @@ size_t DiscoverServicesPlatformConfig::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const DiscoverServicesPlatformConfig& obj) { + os << "DiscoverServicesPlatformConfig("; + os << "android: "; + if (obj.android_) { + os << *obj.android_; + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const DiscoverServicesPlatformConfig& v) { return v.Hash(); } @@ -1945,6 +2460,28 @@ size_t PeripheralAndroidOptions::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const PeripheralAndroidOptions& obj) { + os << "PeripheralAndroidOptions("; + os << "add_manufacturer_data_in_scan_response: "; + if (obj.add_manufacturer_data_in_scan_response_) { + os << PigeonInternalToString(*obj.add_manufacturer_data_in_scan_response_); + } + else { + os << "null"; + } + os << ", add_services_in_scan_response: "; + if (obj.add_services_in_scan_response_) { + os << PigeonInternalToString(*obj.add_services_in_scan_response_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const PeripheralAndroidOptions& v) { return v.Hash(); } @@ -2007,6 +2544,21 @@ size_t PeripheralPlatformConfig::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const PeripheralPlatformConfig& obj) { + os << "PeripheralPlatformConfig("; + os << "android: "; + if (obj.android_) { + os << *obj.android_; + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const PeripheralPlatformConfig& v) { return v.Hash(); } @@ -2081,6 +2633,20 @@ size_t PeripheralService::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const PeripheralService& obj) { + os << "PeripheralService("; + os << "uuid: "; + os << PigeonInternalToString(obj.uuid_); + os << ", primary: "; + os << PigeonInternalToString(obj.primary_); + os << ", characteristics: "; + os << PigeonInternalToString(obj.characteristics_); + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const PeripheralService& v) { return v.Hash(); } @@ -2205,6 +2771,34 @@ size_t PeripheralCharacteristic::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const PeripheralCharacteristic& obj) { + os << "PeripheralCharacteristic("; + os << "uuid: "; + os << PigeonInternalToString(obj.uuid_); + os << ", properties: "; + os << PigeonInternalToString(obj.properties_); + os << ", permissions: "; + os << PigeonInternalToString(obj.permissions_); + os << ", descriptors: "; + if (obj.descriptors_) { + os << PigeonInternalToString(*obj.descriptors_); + } + else { + os << "null"; + } + os << ", value: "; + if (obj.value_) { + os << PigeonInternalToString(*obj.value_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const PeripheralCharacteristic& v) { return v.Hash(); } @@ -2296,6 +2890,30 @@ size_t PeripheralDescriptor::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const PeripheralDescriptor& obj) { + os << "PeripheralDescriptor("; + os << "uuid: "; + os << PigeonInternalToString(obj.uuid_); + os << ", value: "; + if (obj.value_) { + os << PigeonInternalToString(*obj.value_); + } + else { + os << "null"; + } + os << ", permissions: "; + if (obj.permissions_) { + os << PigeonInternalToString(*obj.permissions_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const PeripheralDescriptor& v) { return v.Hash(); } @@ -2387,6 +3005,30 @@ size_t PeripheralReadRequestResult::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const PeripheralReadRequestResult& obj) { + os << "PeripheralReadRequestResult("; + os << "value: "; + os << PigeonInternalToString(obj.value_); + os << ", offset: "; + if (obj.offset_) { + os << PigeonInternalToString(*obj.offset_); + } + else { + os << "null"; + } + os << ", status: "; + if (obj.status_) { + os << PigeonInternalToString(*obj.status_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const PeripheralReadRequestResult& v) { return v.Hash(); } @@ -2484,6 +3126,35 @@ size_t PeripheralWriteRequestResult::Hash() const { return result; } +std::ostream& operator<<( + std::ostream& os, + const PeripheralWriteRequestResult& obj) { + os << "PeripheralWriteRequestResult("; + os << "value: "; + if (obj.value_) { + os << PigeonInternalToString(*obj.value_); + } + else { + os << "null"; + } + os << ", offset: "; + if (obj.offset_) { + os << PigeonInternalToString(*obj.offset_); + } + else { + os << "null"; + } + os << ", status: "; + if (obj.status_) { + os << PigeonInternalToString(*obj.status_); + } + else { + os << "null"; + } + os << ")"; + return os; +} + size_t PigeonInternalDeepHash(const PeripheralWriteRequestResult& v) { return v.Hash(); } diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index 71980405..5c6c465c 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. +// Autogenerated from Pigeon (v29.0.10), do not edit directly. // See also: https://pub.dev/packages/pigeon #ifndef PIGEON_UNIVERSAL_BLE_G_H_ @@ -10,6 +10,7 @@ #include #include +#include #include namespace universal_ble { @@ -312,6 +313,8 @@ class UniversalBleScanResult { bool operator!=(const UniversalBleScanResult& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const UniversalBleScanResult& obj); private: static UniversalBleScanResult FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -360,6 +363,8 @@ class UniversalDevicePickerResult { bool operator!=(const UniversalDevicePickerResult& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const UniversalDevicePickerResult& obj); private: static UniversalDevicePickerResult FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -398,6 +403,8 @@ class UniversalBleService { bool operator!=(const UniversalBleService& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const UniversalBleService& obj); private: static UniversalBleService FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -434,6 +441,8 @@ class UniversalBleCharacteristic { bool operator!=(const UniversalBleCharacteristic& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const UniversalBleCharacteristic& obj); private: static UniversalBleCharacteristic FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -462,6 +471,8 @@ class UniversalBleDescriptor { bool operator!=(const UniversalBleDescriptor& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const UniversalBleDescriptor& obj); private: static UniversalBleDescriptor FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -510,6 +521,8 @@ class BleConnectionParametersChange { bool operator!=(const BleConnectionParametersChange& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const BleConnectionParametersChange& obj); private: static BleConnectionParametersChange FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -603,6 +616,8 @@ class AndroidOptions { bool operator!=(const AndroidOptions& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const AndroidOptions& obj); private: static AndroidOptions FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -655,6 +670,8 @@ class UniversalScanConfig { bool operator!=(const UniversalScanConfig& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const UniversalScanConfig& obj); private: static UniversalScanConfig FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -698,6 +715,8 @@ class UniversalScanFilter { bool operator!=(const UniversalScanFilter& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const UniversalScanFilter& obj); private: static UniversalScanFilter FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -746,6 +765,8 @@ class ManufacturerDataFilter { bool operator!=(const ManufacturerDataFilter& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const ManufacturerDataFilter& obj); private: static ManufacturerDataFilter FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -779,6 +800,8 @@ class UniversalManufacturerData { bool operator!=(const UniversalManufacturerData& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const UniversalManufacturerData& obj); private: static UniversalManufacturerData FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -841,6 +864,8 @@ class AppleConnectionOptions { bool operator!=(const AppleConnectionOptions& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const AppleConnectionOptions& obj); private: static AppleConnectionOptions FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -909,6 +934,8 @@ class AppleAccessoryPickerItem { bool operator!=(const AppleAccessoryPickerItem& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const AppleAccessoryPickerItem& obj); private: static AppleAccessoryPickerItem FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -944,6 +971,8 @@ class AppleAccessorySetupOptions { bool operator!=(const AppleAccessorySetupOptions& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const AppleAccessorySetupOptions& obj); private: static AppleAccessorySetupOptions FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -981,6 +1010,8 @@ class AndroidConnectionOptions { bool operator!=(const AndroidConnectionOptions& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const AndroidConnectionOptions& obj); private: static AndroidConnectionOptions FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1023,6 +1054,8 @@ class ConnectionPlatformConfig { bool operator!=(const ConnectionPlatformConfig& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const ConnectionPlatformConfig& obj); private: static ConnectionPlatformConfig FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1060,6 +1093,8 @@ class AndroidDiscoverServicesOptions { bool operator!=(const AndroidDiscoverServicesOptions& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const AndroidDiscoverServicesOptions& obj); private: static AndroidDiscoverServicesOptions FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1099,6 +1134,8 @@ class DiscoverServicesPlatformConfig { bool operator!=(const DiscoverServicesPlatformConfig& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const DiscoverServicesPlatformConfig& obj); private: static DiscoverServicesPlatformConfig FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1141,6 +1178,8 @@ class PeripheralAndroidOptions { bool operator!=(const PeripheralAndroidOptions& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const PeripheralAndroidOptions& obj); private: static PeripheralAndroidOptions FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1178,6 +1217,8 @@ class PeripheralPlatformConfig { bool operator!=(const PeripheralPlatformConfig& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const PeripheralPlatformConfig& obj); private: static PeripheralPlatformConfig FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1215,6 +1256,8 @@ class PeripheralService { bool operator!=(const PeripheralService& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const PeripheralService& obj); private: static PeripheralService FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1268,6 +1311,8 @@ class PeripheralCharacteristic { bool operator!=(const PeripheralCharacteristic& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const PeripheralCharacteristic& obj); private: static PeripheralCharacteristic FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1312,6 +1357,8 @@ class PeripheralDescriptor { bool operator!=(const PeripheralDescriptor& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const PeripheralDescriptor& obj); private: static PeripheralDescriptor FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1354,6 +1401,8 @@ class PeripheralReadRequestResult { bool operator!=(const PeripheralReadRequestResult& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const PeripheralReadRequestResult& obj); private: static PeripheralReadRequestResult FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; @@ -1397,6 +1446,8 @@ class PeripheralWriteRequestResult { bool operator!=(const PeripheralWriteRequestResult& other) const; /// Returns a hash code value for the object. This method is supported for the benefit of hash tables. size_t Hash() const; + /// Stream output operator for formatted string representation. + friend std::ostream& operator<<(std::ostream& os, const PeripheralWriteRequestResult& obj); private: static PeripheralWriteRequestResult FromEncodableList(const ::flutter::EncodableList& list); ::flutter::EncodableList ToEncodableList() const; From ed728fb6d6c5c49f6434c3edeb2388a599e63b54 Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Fri, 16 Oct 2026 10:12:37 +0200 Subject: [PATCH 2/5] Deduplicate Dart-callback plumbing and add delivery safeguards - Share one fire-and-forget helper per platform (Kotlin `fireAndForget`, Swift `DartCallbackSender.sendToDart`) instead of duplicated per-plugin helpers, and log delivery failures instead of swallowing them. - Bound peripheral read/write requests so a dropped Dart reply cannot pin the CoreBluetooth/GATT request open (10s), then answer the central with a failure. - Add a shared `MainDispatcherTest` base for JVM tests that deliver through Dispatchers.Main. - Add a CI job that regenerates the Pigeon outputs and fails if they drift from the pinned version. --- .github/workflows/pull_request.yml | 26 +++++++- .../universal_ble/UniversalBleHelper.kt | 19 ++++++ .../UniversalBlePeripheralPlugin.kt | 66 +++++++++---------- .../universal_ble/UniversalBlePlugin.kt | 35 +++------- .../universal_ble/DisconnectCloseTest.kt | 19 +----- .../universal_ble/MainDispatcherTest.kt | 24 +++++++ .../universal_ble/UniversalBleHelper.swift | 23 +++++++ .../UniversalBlePeripheralPlugin.swift | 64 +++++++++++++----- .../universal_ble/UniversalBlePlugin.swift | 22 +++---- 9 files changed, 190 insertions(+), 108 deletions(-) create mode 100644 android/src/test/kotlin/com/navideck/universal_ble/MainDispatcherTest.kt diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e7fdc21a..cf984fd6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -90,9 +90,33 @@ jobs: cmake --build build/windows-tests ctest --test-dir build/windows-tests --output-on-failure - pana: + pigeon: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Install dependencies + run: flutter pub get + + # Keeps the committed Pigeon outputs in lockstep with the pinned + # `pigeon` version. Without this, the constraint and the generated + # Kotlin/Swift/C++/Dart can drift (see the Pigeon 29 migration). + - name: Regenerate Pigeon outputs + run: | + dart run pigeon --input pigeon/universal_ble.dart + dart format lib/src/universal_ble.g.dart + + - name: Fail if generated code is out of date + run: git diff --exit-code + + pana: + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt index 878ac347..ff73fecc 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -27,6 +27,9 @@ import android.util.SparseArray import java.nio.ByteBuffer import java.nio.ByteOrder import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import androidx.core.util.size private const val TAG = "UniversalBlePlugin" @@ -527,3 +530,19 @@ class WriteDescriptorResultFuture( // increase, but an MTU already meeting the requested size needs no new exchange. internal fun canReuseNegotiatedMtu(mtu: Int, requested: Int, sdkInt: Int): Boolean = sdkInt >= 34 || mtu >= requested + +/** + * Fire-and-forget a Dart callback from a synchronous BLE callback. Delivery + * failures are logged rather than silently swallowed, since the caller has no + * other way to observe them. + */ +internal fun fireAndForget(channel: T?, block: suspend (T) -> Unit) { + if (channel == null) return + CoroutineScope(Dispatchers.Main).launch { + try { + block(channel) + } catch (e: Throwable) { + UniversalBleLogger.logError("Failed to deliver Dart callback: ${e.message}") + } + } +} diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt index 2e911748..db54f248 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt @@ -28,11 +28,17 @@ import android.util.Log import io.flutter.plugin.common.BinaryMessenger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withTimeout import kotlinx.coroutines.launch private const val TAG = "UniversalBlePeripheral" private const val REQUEST_CODE_ENABLE_BLUETOOTH = 2342717 +/// How long to wait for a Dart reply to a peripheral read/write request before +/// answering the central with a failure, so a dropped reply cannot pin the +/// request open indefinitely. +private const val PERIPHERAL_REQUEST_TIMEOUT_MS = 10_000L + @SuppressLint("MissingPermission") class UniversalBlePeripheralPlugin( private val applicationContext: Context, @@ -57,16 +63,16 @@ class UniversalBlePeripheralPlugin( this.activity = activity } - /** Fire-and-forget a Dart callback on the main dispatcher, ignoring failures. */ - private fun notify(block: suspend (UniversalBlePeripheralCallback) -> Unit) { - CoroutineScope(Dispatchers.Main).launch { - try { - block(callback) - } catch (_: Throwable) { - // Callback delivery failures are non-fatal. - } + /** + * Awaits a Dart reply to a peripheral request. A reply that never arrives is + * treated like a failure so the request is not held open forever. + */ + private suspend fun awaitReply(block: suspend () -> T?): T? = + try { + withTimeout(PERIPHERAL_REQUEST_TIMEOUT_MS) { block() } + } catch (_: Throwable) { + null } - } fun dispose() { kotlin.runCatching { bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback) } @@ -179,7 +185,7 @@ class UniversalBlePeripheralPlugin( ) { if (!hasRequiredPermissions()) { advertisingState = PeripheralAdvertisingState.ERROR - notify { it.onAdvertisingStateChange( + fireAndForget(callback) { it.onAdvertisingStateChange( PeripheralAdvertisingState.ERROR, "Bluetooth permission not granted", ) } @@ -189,7 +195,7 @@ class UniversalBlePeripheralPlugin( bluetoothManager.isBluetoothEnabled() } catch (e: SecurityException) { advertisingState = PeripheralAdvertisingState.ERROR - notify { it.onAdvertisingStateChange( + fireAndForget(callback) { it.onAdvertisingStateChange( PeripheralAdvertisingState.ERROR, "Bluetooth permission not granted", ) } @@ -197,7 +203,7 @@ class UniversalBlePeripheralPlugin( } if (!isEnabled) { advertisingState = PeripheralAdvertisingState.ERROR - notify { it.onAdvertisingStateChange( + fireAndForget(callback) { it.onAdvertisingStateChange( PeripheralAdvertisingState.ERROR, "Bluetooth is not enabled", ) } @@ -207,7 +213,7 @@ class UniversalBlePeripheralPlugin( isEnableBluetoothPromptPending = false ensurePeripheralInitialized() advertisingState = PeripheralAdvertisingState.STARTING - notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.STARTING, null) } + fireAndForget(callback) { it.onAdvertisingStateChange(PeripheralAdvertisingState.STARTING, null) } handler.post { val adapter = bluetoothManager.adapter @@ -264,12 +270,12 @@ class UniversalBlePeripheralPlugin( return } advertisingState = PeripheralAdvertisingState.STOPPING - notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.STOPPING, null) } + fireAndForget(callback) { it.onAdvertisingStateChange(PeripheralAdvertisingState.STOPPING, null) } handler.post { kotlin.runCatching { bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback) } restoreAdapterNameIfNeeded() advertisingState = PeripheralAdvertisingState.IDLE - notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.IDLE, null) } + fireAndForget(callback) { it.onAdvertisingStateChange(PeripheralAdvertisingState.IDLE, null) } } } @@ -320,7 +326,7 @@ class UniversalBlePeripheralPlugin( private fun onConnectionUpdate(device: BluetoothDevice, status: Int, newState: Int) { Log.e(TAG, "onConnectionStateChange: $status -> $newState") handler.post { - notify { it.onConnectionStateChange( + fireAndForget(callback) { it.onConnectionStateChange( device.address, newState == BluetoothProfile.STATE_CONNECTED, ) } @@ -337,7 +343,7 @@ class UniversalBlePeripheralPlugin( } subscribedCharUUID.forEach { charUUID -> handler.post { - notify { it.onCharacteristicSubscriptionChange( + fireAndForget(callback) { it.onCharacteristicSubscriptionChange( deviceAddress, charUUID, false, @@ -361,7 +367,7 @@ class UniversalBlePeripheralPlugin( else -> "Failed to start advertising: $errorCode" } advertisingState = PeripheralAdvertisingState.ERROR - notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.ERROR, errorMessage) } + fireAndForget(callback) { it.onAdvertisingStateChange(PeripheralAdvertisingState.ERROR, errorMessage) } } } @@ -369,7 +375,7 @@ class UniversalBlePeripheralPlugin( super.onStartSuccess(settingsInEffect) advertisingState = PeripheralAdvertisingState.ADVERTISING handler.post { - notify { it.onAdvertisingStateChange(PeripheralAdvertisingState.ADVERTISING, null) } + fireAndForget(callback) { it.onAdvertisingStateChange(PeripheralAdvertisingState.ADVERTISING, null) } } } } @@ -399,7 +405,7 @@ class UniversalBlePeripheralPlugin( super.onMtuChanged(device, mtu) device?.address?.let { address -> synchronized(mtuByDeviceId) { mtuByDeviceId[address] = mtu } - handler.post { notify { it.onMtuChange(address, mtu.toLong()) } } + handler.post { fireAndForget(callback) { it.onMtuChange(address, mtu.toLong()) } } } } @@ -413,15 +419,13 @@ class UniversalBlePeripheralPlugin( if (gattServer == null) return handler.post { CoroutineScope(Dispatchers.Main).launch { - val readResult = try { + val readResult = awaitReply { callback.onReadRequest( deviceIdArg = device.address, characteristicIdArg = characteristic.uuid.toString(), offsetArg = offset.toLong(), valueArg = characteristic.value, ) - } catch (_: Throwable) { - null } if (readResult == null) { gattServer?.sendResponse( @@ -454,15 +458,13 @@ class UniversalBlePeripheralPlugin( ) handler.post { CoroutineScope(Dispatchers.Main).launch { - val writeResult = try { + val writeResult = awaitReply { callback.onWriteRequest( deviceIdArg = device.address, characteristicIdArg = characteristic.uuid.toString(), offsetArg = offset.toLong(), valueArg = value, ) - } catch (_: Throwable) { - null } if (responseNeeded) { gattServer?.sendResponse( @@ -480,7 +482,7 @@ class UniversalBlePeripheralPlugin( override fun onServiceAdded(status: Int, service: BluetoothGattService) { super.onServiceAdded(status, service) val error = if (status != BluetoothGatt.GATT_SUCCESS) "Adding Service failed" else null - handler.post { notify { it.onServiceAdded(service.uuid.toString(), error) } } + handler.post { fireAndForget(callback) { it.onServiceAdded(service.uuid.toString(), error) } } } override fun onDescriptorReadRequest( @@ -492,7 +494,7 @@ class UniversalBlePeripheralPlugin( super.onDescriptorReadRequest(device, requestId, offset, descriptor) handler.post { CoroutineScope(Dispatchers.Main).launch { - val readResult = try { + val readResult = awaitReply { callback.onDescriptorReadRequest( deviceIdArg = device.address, characteristicIdArg = descriptor.characteristic.uuid.toString(), @@ -500,8 +502,6 @@ class UniversalBlePeripheralPlugin( offsetArg = offset.toLong(), valueArg = descriptor.getCacheValue(), ) - } catch (_: Throwable) { - null } if (readResult == null) { gattServer?.sendResponse( @@ -544,7 +544,7 @@ class UniversalBlePeripheralPlugin( val characteristicId = descriptor.characteristic.uuid.toString() device?.address?.let { address -> handler.post { - notify { it.onCharacteristicSubscriptionChange( + fireAndForget(callback) { it.onCharacteristicSubscriptionChange( address, characteristicId, isSubscribed, device.name, ) } } @@ -561,7 +561,7 @@ class UniversalBlePeripheralPlugin( if (responseNeeded) { handler.post { CoroutineScope(Dispatchers.Main).launch { - val writeResult = try { + val writeResult = awaitReply { callback.onDescriptorWriteRequest( deviceIdArg = device?.address ?: "", characteristicIdArg = descriptor.characteristic.uuid.toString(), @@ -569,8 +569,6 @@ class UniversalBlePeripheralPlugin( offsetArg = offset.toLong(), valueArg = value, ) - } catch (_: Throwable) { - null } gattServer?.sendResponse( device, diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index 5a3fe6d0..83d01c2f 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -35,9 +35,6 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import androidx.core.content.edit @@ -343,7 +340,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (currentState == BluetoothGatt.STATE_CONNECTED) { UniversalBleLogger.logError("$deviceId Already connected") mainThreadHandler?.post { - notify { it.onConnectionChanged(deviceId, true, null, null, null) } + fireAndForget(callbackChannel) { it.onConnectionChanged(deviceId, true, null, null, null) } } return } else if (currentState == BluetoothGatt.STATE_CONNECTING) { @@ -380,7 +377,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (gatt == null) { cleanUpConnection(deviceId) mainThreadHandler?.post { - notify { it.onConnectionChanged(deviceId, false, null, null, null) } + fireAndForget(callbackChannel) { it.onConnectionChanged(deviceId, false, null, null, null) } } } else { cleanConnection(gatt) @@ -1240,7 +1237,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), "onConnectionUpdated -> $deviceId interval=$interval latency=$latency timeout=$timeout status=$status" ) mainThreadHandler?.post { - notify { it.onConnectionParametersUpdated( + fireAndForget(callbackChannel) { it.onConnectionParametersUpdated( BleConnectionParametersChange( deviceId = deviceId, interval = interval.toLong(), @@ -1590,7 +1587,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), gatt.close() // A newer connect() owns this address now; its state is not ours to report. if (!gattRegistry.isKnown(deviceId)) { - notify { it.onConnectionChanged(deviceId, false, null, null, null) } + fireAndForget(callbackChannel) { it.onConnectionChanged(deviceId, false, null, null, null) } } } }, gattCloseTimeoutMs) @@ -1600,7 +1597,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), val future = pairResultFutures.remove(deviceId) future?.let { it(Result.success(bonded)) } mainThreadHandler?.post { - notify { it.onPairStateChange(deviceId, bonded, isFailure) } + fireAndForget(callbackChannel) { it.onPairStateChange(deviceId, bonded, isFailure) } } } @@ -1608,7 +1605,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), override fun onReceive(context: Context, intent: Intent) { if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) { mainThreadHandler?.post { - notify { it.onAvailabilityChanged( + fireAndForget(callbackChannel) { it.onAvailabilityChanged( bluetoothManager.adapter?.state?.toAvailabilityState() ?: AvailabilityState.UNKNOWN ) } @@ -1686,7 +1683,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), mainThreadHandler?.post { - notify { it.onScanResult( + fireAndForget(callbackChannel) { it.onScanResult( UniversalBleScanResult( name = name, deviceId = result.device.address, @@ -1721,7 +1718,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (newState == BluetoothGatt.STATE_CONNECTED) { connectedGatts.add(gatt) mainThreadHandler?.post { - notify { it.onConnectionChanged( + fireAndForget(callbackChannel) { it.onConnectionChanged( gatt.device.address, true, status.parseHciErrorCode(), status.toConnectionErrorCode(), status.toGattErrorCode() ) } } @@ -1740,7 +1737,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), // Send connection changed callback mainThreadHandler?.post { - notify { it.onConnectionChanged( + fireAndForget(callbackChannel) { it.onConnectionChanged( deviceId, false, status.parseHciErrorCode(), @@ -1770,7 +1767,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), "NOTIFY <- ${gatt.device.address} ${characteristic.uuid} len=${value.size}" ) mainThreadHandler?.post { - notify { it.onValueChanged( + fireAndForget(callbackChannel) { it.onValueChanged( deviceIdArg = gatt.device.address, characteristicIdArg = characteristic.uuid.toString(), valueArg = value, @@ -1974,18 +1971,6 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - /** Fire-and-forget a Dart callback on the main dispatcher, ignoring failures. */ - private fun notify(block: suspend (UniversalBleCallbackChannel) -> Unit) { - val channel = callbackChannel ?: return - CoroutineScope(Dispatchers.Main).launch { - try { - block(channel) - } catch (_: Throwable) { - // Callback delivery failures are non-fatal. - } - } - } - override suspend fun getBluetoothAvailabilityState(): AvailabilityState = awaitResult { getBluetoothAvailabilityStateInternal(it) } diff --git a/android/src/test/kotlin/com/navideck/universal_ble/DisconnectCloseTest.kt b/android/src/test/kotlin/com/navideck/universal_ble/DisconnectCloseTest.kt index ac143387..6d9b6a2f 100644 --- a/android/src/test/kotlin/com/navideck/universal_ble/DisconnectCloseTest.kt +++ b/android/src/test/kotlin/com/navideck/universal_ble/DisconnectCloseTest.kt @@ -3,15 +3,10 @@ package com.navideck.universal_ble import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.os.Handler -import kotlin.test.AfterTest -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertSame import kotlin.test.assertTrue -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain import org.mockito.ArgumentMatchers.any import org.mockito.Mockito @@ -22,21 +17,9 @@ import org.mockito.Mockito * without failing the new client's operations, evicting it from the cache or * reporting a disconnect the new client would take for its own. */ -internal class DisconnectCloseTest { +internal class DisconnectCloseTest : MainDispatcherTest() { private val deviceAddress = "AA:BB:CC:DD:EE:FF" - // Callbacks are delivered through `Dispatchers.Main`; the JVM test dispatcher - // stands in for Android's main looper. - @BeforeTest - fun setUpMainDispatcher() { - Dispatchers.setMain(Dispatchers.Unconfined) - } - - @AfterTest - fun tearDownMainDispatcher() { - Dispatchers.resetMain() - } - // (deviceId, connected, error, errorCode, nativeErrorCode) of every onConnectionChanged // delivered to Dart. private val connectionChanges = mutableListOf>() diff --git a/android/src/test/kotlin/com/navideck/universal_ble/MainDispatcherTest.kt b/android/src/test/kotlin/com/navideck/universal_ble/MainDispatcherTest.kt new file mode 100644 index 00000000..72492206 --- /dev/null +++ b/android/src/test/kotlin/com/navideck/universal_ble/MainDispatcherTest.kt @@ -0,0 +1,24 @@ +package com.navideck.universal_ble + +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain + +/** + * Installs a Main dispatcher for tests that exercise code delivering Dart + * callbacks through `Dispatchers.Main`, which is otherwise unavailable on the + * JVM. Extend this instead of duplicating the set/reset in each test class. + */ +internal abstract class MainDispatcherTest { + @BeforeTest + fun setUpMainDispatcher() { + Dispatchers.setMain(Dispatchers.Unconfined) + } + + @AfterTest + fun tearDownMainDispatcher() { + Dispatchers.resetMain() + } +} diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift index b407ba9a..bf3a4609 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift @@ -13,6 +13,29 @@ import Foundation import FlutterMacOS #endif +/// Supplies the Pigeon callback channel used to push events to Dart. +protocol DartCallbackSender { + associatedtype CallbackChannel + var callbackChannel: CallbackChannel { get } +} + +extension DartCallbackSender { + /// Fire-and-forget a Dart callback from a synchronous CoreBluetooth callback. + /// Delivery failures are logged rather than silently swallowed, since the + /// caller has no other way to observe them. + func sendToDart( + _ body: @escaping @MainActor (CallbackChannel) async throws -> Void + ) { + Task { @MainActor in + do { + try await body(callbackChannel) + } catch { + UniversalBleLogger.shared.logError("Failed to deliver Dart callback: \(error)") + } + } + } +} + extension CBCharacteristicProperties { var toCharacteristicProperty: [CharacteristicProperty] { var properties = [CharacteristicProperty]() diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift index a04cc581..e099a528 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift @@ -8,9 +8,9 @@ import CoreBluetooth import Foundation final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChannel, - CBPeripheralManagerDelegate + CBPeripheralManagerDelegate, DartCallbackSender { - private let callbackChannel: UniversalBlePeripheralCallback + let callbackChannel: UniversalBlePeripheralCallback private var advertisingState: PeripheralAdvertisingState = .idle private var peripheralManager: CBPeripheralManager? private let centralsLock = NSLock() @@ -21,6 +21,11 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne // another engine's advertising state. private let serviceRegistry = PeripheralServiceRegistry() + /// How long to wait for a Dart reply to a peripheral read/write request before + /// answering the central with a failure, so a dropped reply cannot pin the + /// CoreBluetooth request open indefinitely. + private static let peripheralRequestTimeout: TimeInterval = 10 + init(callbackChannel: UniversalBlePeripheralCallback) { self.callbackChannel = callbackChannel super.init() @@ -59,7 +64,7 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne private func completeStopIfNeeded() { guard advertisingState == .stopping else { return } advertisingState = .idle - Task { @MainActor in try? await callbackChannel.onAdvertisingStateChange(state: .idle, error: nil) } + sendToDart { try await $0.onAdvertisingStateChange(state: .idle, error: nil) } } func getReadinessState() throws -> PeripheralReadinessState { @@ -81,7 +86,7 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne func stopAdvertising() throws { advertisingState = .stopping - Task { @MainActor in try? await callbackChannel.onAdvertisingStateChange(state: .stopping, error: nil) } + sendToDart { try await $0.onAdvertisingStateChange(state: .stopping, error: nil) } guard let peripheralManager else { completeStopIfNeeded() return @@ -144,7 +149,7 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne advertisementData[CBAdvertisementDataManufacturerDataKey] = manufacturerField } advertisingState = .starting - Task { @MainActor in try? await callbackChannel.onAdvertisingStateChange(state: .starting, error: nil) } + sendToDart { try await $0.onAdvertisingStateChange(state: .starting, error: nil) } ensurePeripheralManager().startAdvertising(advertisementData) } @@ -180,9 +185,8 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne error: Error? ) { advertisingState = error == nil ? .advertising : .error - Task { @MainActor in - try? await callbackChannel.onAdvertisingStateChange( - state: advertisingState, + sendToDart { try await $0.onAdvertisingStateChange( + state: self.advertisingState, error: error?.localizedDescription ) } @@ -191,7 +195,7 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne nonisolated func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { if peripheral.state != .poweredOn { advertisingState = .idle - Task { @MainActor in try? await callbackChannel.onAdvertisingStateChange(state: .idle, error: nil) } + sendToDart { try await $0.onAdvertisingStateChange(state: .idle, error: nil) } } } @@ -200,8 +204,7 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne didAdd service: CBService, error: Error? ) { - Task { @MainActor in - try? await callbackChannel.onServiceAdded( + sendToDart { try await $0.onServiceAdded( serviceId: service.uuid.uuidString, error: error?.localizedDescription ) @@ -217,16 +220,14 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne central: central, characteristicId: characteristic.uuid.uuidString ) - Task { @MainActor in - try? await callbackChannel.onCharacteristicSubscriptionChange( + sendToDart { try await $0.onCharacteristicSubscriptionChange( deviceId: central.identifier.uuidString, characteristicId: characteristic.uuid.uuidString, isSubscribed: true, name: nil ) } - Task { @MainActor in - try? await callbackChannel.onMtuChange( + sendToDart { try await $0.onMtuChange( deviceId: central.identifier.uuidString, mtu: Int64(central.maximumUpdateValueLength) ) @@ -242,8 +243,7 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne centralId: central.identifier.uuidString, characteristicId: characteristic.uuid.uuidString ) - Task { @MainActor in - try? await callbackChannel.onCharacteristicSubscriptionChange( + sendToDart { try await $0.onCharacteristicSubscriptionChange( deviceId: central.identifier.uuidString, characteristicId: characteristic.uuid.uuidString, isSubscribed: false, @@ -257,6 +257,17 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne didReceiveRead request: CBATTRequest ) { Task { @MainActor in + var responded = false + // CoreBluetooth holds the request open until we respond; a Dart reply that + // never arrives must not pin it (and the connection) forever. + let timeout = DispatchWorkItem { + guard !responded else { return } + responded = true + peripheral.respond(to: request, withResult: .requestNotSupported) + } + DispatchQueue.main.asyncAfter(deadline: .now() + Self.peripheralRequestTimeout, execute: timeout) + defer { timeout.cancel() } + do { let result = try await callbackChannel.onReadRequest( deviceId: request.central.identifier.uuidString, @@ -264,6 +275,8 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne offset: Int64(request.offset), value: request.value?.toFlutterBytes() ) + guard !responded else { return } + responded = true let status = result?.status?.toCBATTErrorCode() ?? .success guard status == .success else { peripheral.respond(to: request, withResult: status) @@ -282,6 +295,9 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne request.value = sliced peripheral.respond(to: request, withResult: .success) } catch { + UniversalBleLogger.shared.logError("Peripheral read request failed: \(error)") + guard !responded else { return } + responded = true peripheral.respond(to: request, withResult: .requestNotSupported) } } @@ -293,6 +309,15 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne ) { requests.forEach { req in Task { @MainActor in + var responded = false + let timeout = DispatchWorkItem { + guard !responded else { return } + responded = true + peripheral.respond(to: req, withResult: .requestNotSupported) + } + DispatchQueue.main.asyncAfter(deadline: .now() + Self.peripheralRequestTimeout, execute: timeout) + defer { timeout.cancel() } + do { let response = try await callbackChannel.onWriteRequest( deviceId: req.central.identifier.uuidString, @@ -300,9 +325,14 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne offset: Int64(req.offset), value: req.value?.toFlutterBytes() ) + guard !responded else { return } + responded = true let status = response?.status?.toCBATTErrorCode() ?? .success peripheral.respond(to: req, withResult: status) } catch { + UniversalBleLogger.shared.logError("Peripheral write request failed: \(error)") + guard !responded else { return } + responded = true peripheral.respond(to: req, withResult: .requestNotSupported) } } diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift index a7d6250c..0897ceed 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift @@ -35,7 +35,7 @@ public class UniversalBlePlugin: NSObject, FlutterPlugin { } } -private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentralManagerDelegate, CBPeripheralDelegate { +private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentralManagerDelegate, CBPeripheralDelegate, DartCallbackSender { // Identifier CoreBluetooth uses to restore this central across relaunches. static let stateRestorationIdentifier = "com.universalble.central.restoration" @@ -280,7 +280,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func disconnect(deviceId: String) throws { autoConnectDevices.remove(deviceId) guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - Task { @MainActor in try? await callbackChannel.onConnectionChanged(deviceId: deviceId, connected: false, error: nil, errorCode: nil, nativeErrorCode: nil) } + sendToDart { try await $0.onConnectionChanged(deviceId: deviceId, connected: false, error: nil, errorCode: nil, nativeErrorCode: nil) } cleanUpConnection(deviceId: deviceId) return } @@ -619,7 +619,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // Notify Dart if already connected; on a cold relaunch the Dart layer // re-subscribes on resume using the cached peripheral. if peripheral.state == .connected { - Task { @MainActor in try? await callbackChannel.onConnectionChanged(deviceId: deviceId, connected: true, error: nil, errorCode: nil, nativeErrorCode: nil) } + sendToDart { try await $0.onConnectionChanged(deviceId: deviceId, connected: true, error: nil, errorCode: nil, nativeErrorCode: nil) } } } } @@ -627,7 +627,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func centralManagerDidUpdateState(_ central: CBCentralManager) { let state = central.state.toAvailabilityState() - Task { @MainActor in try? await callbackChannel.onAvailabilityChanged(state: state) } + sendToDart { try await $0.onAvailabilityChanged(state: state) } // Complete Pending state handler availabilityStateUpdateHandlers.removeAll { handler in handler(.success(state)) @@ -682,8 +682,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } let timestampMicroseconds = Int64(Date().timeIntervalSince1970 * 1_000_000) - Task { @MainActor in - try? await callbackChannel.onScanResult(result: UniversalBleScanResult( + sendToDart { try await $0.onScanResult(result: UniversalBleScanResult( deviceId: peripheral.uuid.uuidString, name: displayName, isPaired: nil, @@ -699,13 +698,12 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } public func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { - Task { @MainActor in try? await callbackChannel.onConnectionChanged(deviceId: peripheral.uuid.uuidString, connected: true, error: nil, errorCode: nil, nativeErrorCode: nil) } + sendToDart { try await $0.onConnectionChanged(deviceId: peripheral.uuid.uuidString, connected: true, error: nil, errorCode: nil, nativeErrorCode: nil) } } private func handlePeripheralDisconnection(deviceId: String, error: Error?) { autoConnectDevices.remove(deviceId) - Task { @MainActor in - try? await callbackChannel.onConnectionChanged( + sendToDart { try await $0.onConnectionChanged( deviceId: deviceId, connected: false, error: error?.localizedDescription, errorCode: error?.connectionErrorCode, nativeErrorCode: error?.nativeCode ) } @@ -737,8 +735,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } public func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - Task { @MainActor in - try? await callbackChannel.onConnectionChanged( + sendToDart { try await $0.onConnectionChanged( deviceId: peripheral.uuid.uuidString, connected: false, error: error?.localizedDescription, errorCode: error?.connectionErrorCode, nativeErrorCode: error?.nativeCode ) } @@ -879,8 +876,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // Update callbackChannel if notifying if characteristic.isNotifying { if let characteristicValue = characteristic.value { - Task { @MainActor in - try? await callbackChannel.onValueChanged( + sendToDart { try await $0.onValueChanged( deviceId: peripheral.uuid.uuidString, characteristicId: characteristic.uuid.uuidStr, value: FlutterStandardTypedData(bytes: characteristicValue), From 8df140a2c878e58c7805405f27860ed3597e5db8 Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Fri, 16 Oct 2026 10:25:07 +0200 Subject: [PATCH 3/5] Make Darwin pending requests continuation-based; drop the Impl/Internal indirection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Darwin: the pending-request registries (`…Future`, availability/permission handlers) now hold `CheckedContinuation` instead of completion closures. The generated async protocol methods are the single entry point; the old `…Impl` methods are gone (the continuation-taking logic is now an overload of the same name). Added a `CheckedContinuation.finish(_:)` helper to avoid the Swift 6 `Error`/`any Error` ambiguity of `resume(with:)`. - Android: keep the callback-driven GATT logic but remove the `…Internal` naming noise (same-name overloads taking a `(Result) -> Unit`), matching Darwin. - Cancellation wiring is intentionally not part of this change (it needs thread-safe pending-request removal and its own tests). --- .../universal_ble/UniversalBlePlugin.kt | 84 +++--- .../universal_ble/UniversalBleHelper.swift | 126 +++++---- .../universal_ble/UniversalBlePlugin.swift | 259 +++++++++--------- 3 files changed, 234 insertions(+), 235 deletions(-) diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index 83d01c2f..46db573a 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -150,7 +150,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), permissionHandler = null } - private fun getBluetoothAvailabilityStateInternal(callback: (Result) -> Unit) { + private fun getBluetoothAvailabilityState(callback: (Result) -> Unit) { callback( Result.success( bluetoothManager.adapter?.state?.toAvailabilityState() @@ -163,7 +163,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return permissionHandler?.hasPermissions(withAndroidFineLocation) ?: false } - private fun requestPermissionsInternal( + private fun requestPermissions( withAndroidFineLocation: Boolean, callback: (Result) -> Unit, ) { @@ -180,7 +180,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), permissionHandler?.requestPermissions(withAndroidFineLocation, callback) } - private fun enableBluetoothInternal(callback: (Result) -> Unit) { + private fun enableBluetooth(callback: (Result) -> Unit) { if (bluetoothManager.adapter.isEnabled) { callback(Result.success(true)) return @@ -201,7 +201,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), bluetoothEnableRequestFuture = callback } - private fun disableBluetoothInternal(callback: (Result) -> Unit) { + private fun disableBluetooth(callback: (Result) -> Unit) { if (!bluetoothManager.adapter.isEnabled) { callback(Result.success(true)) return @@ -311,7 +311,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return safeScanner.isScanning() } - private fun requestDeviceInternal( + private fun requestDevice( options: AppleAccessorySetupOptions, callback: (Result) -> Unit, ) { @@ -410,7 +410,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), return permissionHandler?.hasBluetoothAdvertisePermission() ?: false } - private fun requestBluetoothAdvertisePermissionInternal(callback: (Result) -> Unit) { + private fun requestBluetoothAdvertisePermission(callback: (Result) -> Unit) { val handler = permissionHandler if (handler == null) { callback( @@ -426,7 +426,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), handler.requestBluetoothAdvertisePermission(callback) } - private fun readRssiInternal(deviceId: String, callback: (Result) -> Unit) { + private fun readRssi(deviceId: String, callback: (Result) -> Unit) { try { val gatt = gattRegistry.get(deviceId) if (gatt.readRemoteRssi()) { @@ -469,7 +469,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - private fun discoverServicesInternal( + private fun discoverServices( deviceId: String, withDescriptors: Boolean, platformConfig: DiscoverServicesPlatformConfig?, @@ -555,7 +555,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } - private fun setNotifiableInternal( + private fun setNotifiable( deviceId: String, service: String, characteristic: String, @@ -662,7 +662,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - private fun readValueInternal( + private fun readValue( deviceId: String, service: String, characteristic: String, @@ -752,7 +752,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - private fun writeValueInternal( + private fun writeValue( deviceId: String, service: String, characteristic: String, @@ -910,7 +910,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - private fun readDescriptorValueInternal( + private fun readDescriptorValue( deviceId: String, service: String, characteristic: String, @@ -1025,7 +1025,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), onDescriptorRead(gatt, descriptor, status, descriptor.value ?: byteArrayOf()) } - private fun writeDescriptorValueInternal( + private fun writeDescriptorValue( deviceId: String, service: String, characteristic: String, @@ -1111,7 +1111,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - private fun requestMtuInternal(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) { + private fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) { UniversalBleLogger.logDebug("REQUEST_MTU -> $deviceId expected=$expectedMtu") val gatt = try { gattRegistry.get(deviceId) @@ -1180,7 +1180,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), for (future in pending) postMtuResult(future.result, result) } - private fun requestConnectionPriorityInternal( + private fun requestConnectionPriority( deviceId: String, priority: BleConnectionPriority, callback: (Result) -> Unit, @@ -1268,13 +1268,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), }) } - private fun isPairedInternal(deviceId: String, callback: (Result) -> Unit) { + private fun isPaired(deviceId: String, callback: (Result) -> Unit) { val remoteDevice: BluetoothDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) callback(Result.success(remoteDevice.isBonded())) } - private fun pairInternal(deviceId: String, callback: (Result) -> Unit) { + private fun pair(deviceId: String, callback: (Result) -> Unit) { try { val remoteDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) val pendingFuture = pairResultFutures.remove(deviceId) @@ -1322,7 +1322,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } - private fun unPairInternal(deviceId: String, callback: (Result) -> Unit) { + private fun unPair(deviceId: String, callback: (Result) -> Unit) { try { val remoteDevice: BluetoothDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) @@ -1333,7 +1333,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } - private fun getSystemDevicesInternal( + private fun getSystemDevices( withServices: List, callback: (Result>) -> Unit, ) { @@ -1956,10 +1956,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), ?: false } - // --- Pigeon 29 suspend bridges ------------------------------------------------- - // The generated Pigeon 29 interface is suspend-based. The implementations below - // keep the previous callback-based logic (renamed to *Internal) and bridge it - // to coroutines here. + // --- Pigeon 29 suspend adapters ------------------------------------------------- + // The generated Pigeon 29 interface is suspend-based. Each operation keeps the + // callback-driven GATT logic (the overload taking a `(Result) -> Unit`) and + // bridges it to coroutines here. private suspend fun awaitResult(block: ((Result) -> Unit) -> Unit): T = suspendCancellableCoroutine { continuation -> @@ -1972,19 +1972,19 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } override suspend fun getBluetoothAvailabilityState(): AvailabilityState = - awaitResult { getBluetoothAvailabilityStateInternal(it) } + awaitResult { getBluetoothAvailabilityState(it) } override suspend fun requestPermissions(withAndroidFineLocation: Boolean) = - awaitResult { requestPermissionsInternal(withAndroidFineLocation, it) } + awaitResult { requestPermissions(withAndroidFineLocation, it) } override suspend fun enableBluetooth(): Boolean = - awaitResult { enableBluetoothInternal(it) } + awaitResult { enableBluetooth(it) } override suspend fun disableBluetooth(): Boolean = - awaitResult { disableBluetoothInternal(it) } + awaitResult { disableBluetooth(it) } override suspend fun requestDevice(options: AppleAccessorySetupOptions): UniversalDevicePickerResult = - awaitResult { requestDeviceInternal(options, it) } + awaitResult { requestDevice(options, it) } override suspend fun setNotifiable( deviceId: String, @@ -1992,7 +1992,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), characteristic: String, bleInputProperty: BleInputProperty, ) = awaitResult { - setNotifiableInternal(deviceId, service, characteristic, bleInputProperty, it) + setNotifiable(deviceId, service, characteristic, bleInputProperty, it) } override suspend fun discoverServices( @@ -2000,13 +2000,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), withDescriptors: Boolean, platformConfig: DiscoverServicesPlatformConfig?, ): List = - awaitResult { discoverServicesInternal(deviceId, withDescriptors, platformConfig, it) } + awaitResult { discoverServices(deviceId, withDescriptors, platformConfig, it) } override suspend fun readValue( deviceId: String, service: String, characteristic: String, - ): ByteArray = awaitResult { readValueInternal(deviceId, service, characteristic, it) } + ): ByteArray = awaitResult { readValue(deviceId, service, characteristic, it) } override suspend fun readDescriptorValue( deviceId: String, @@ -2014,11 +2014,11 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), characteristic: String, descriptor: String, ): ByteArray = awaitResult { - readDescriptorValueInternal(deviceId, service, characteristic, descriptor, it) + readDescriptorValue(deviceId, service, characteristic, descriptor, it) } override suspend fun requestMtu(deviceId: String, expectedMtu: Long): Long = - awaitResult { requestMtuInternal(deviceId, expectedMtu, it) } + awaitResult { requestMtu(deviceId, expectedMtu, it) } override suspend fun writeValue( deviceId: String, @@ -2027,7 +2027,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), value: ByteArray, bleOutputProperty: BleOutputProperty, ) = awaitResult { - writeValueInternal(deviceId, service, characteristic, value, bleOutputProperty, it) + writeValue(deviceId, service, characteristic, value, bleOutputProperty, it) } override suspend fun writeDescriptorValue( @@ -2037,27 +2037,27 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), descriptor: String, value: ByteArray, ) = awaitResult { - writeDescriptorValueInternal(deviceId, service, characteristic, descriptor, value, it) + writeDescriptorValue(deviceId, service, characteristic, descriptor, value, it) } override suspend fun isPaired(deviceId: String): Boolean = - awaitResult { isPairedInternal(deviceId, it) } + awaitResult { isPaired(deviceId, it) } override suspend fun pair(deviceId: String): Boolean = - awaitResult { pairInternal(deviceId, it) } + awaitResult { pair(deviceId, it) } override suspend fun unPair(deviceId: String) = - awaitResult { unPairInternal(deviceId, it) } + awaitResult { unPair(deviceId, it) } override suspend fun getSystemDevices(withServices: List): List = - awaitResult { getSystemDevicesInternal(withServices, it) } + awaitResult { getSystemDevices(withServices, it) } override suspend fun readRssi(deviceId: String): Long = - awaitResult { readRssiInternal(deviceId, it) } + awaitResult { readRssi(deviceId, it) } override suspend fun requestConnectionPriority(deviceId: String, priority: BleConnectionPriority) = - awaitResult { requestConnectionPriorityInternal(deviceId, priority, it) } + awaitResult { requestConnectionPriority(deviceId, priority, it) } override suspend fun requestBluetoothAdvertisePermission(): Boolean = - awaitResult { requestBluetoothAdvertisePermissionInternal(it) } + awaitResult { requestBluetoothAdvertisePermission(it) } } diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift index bf3a4609..7da4aa74 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBleHelper.swift @@ -275,142 +275,140 @@ extension Array where Element: DeviceFuture { } } -class CharacteristicReadFuture: DeviceFuture { +/// A single-shot pending operation. Its continuation is resumed exactly once, +/// either by the matching CoreBluetooth callback or by a connection teardown. +protocol PendingResult { + associatedtype Value + var continuation: CheckedContinuation { get } +} + +extension CheckedContinuation { + /// Resumes with `result`. Spelled out rather than using `resume(with:)`, + /// which is ambiguous under Swift 6's `Error`/`any Error` distinction. + func finish(_ result: Result) { + switch result { + case .success(let value): + resume(returning: value) + case .failure(let error): + resume(throwing: error) + } + } +} + +extension PendingResult { + func complete(_ result: Result) { + continuation.finish(result) + } + + func fail(with error: Error) { + continuation.resume(throwing: error) + } +} + +class CharacteristicReadFuture: DeviceFuture, PendingResult { let deviceId: String let characteristicId: String let serviceId: String? - let result: (Result) -> Void + let continuation: CheckedContinuation - init(deviceId: String, characteristicId: String, serviceId: String?, result: @escaping (Result) -> Void) { + init(deviceId: String, characteristicId: String, serviceId: String?, continuation: CheckedContinuation) { self.deviceId = deviceId self.characteristicId = characteristicId self.serviceId = serviceId - self.result = result - } - - func fail(with error: Error) { - result(.failure(error)) + self.continuation = continuation } } -class CharacteristicWriteFuture: DeviceFuture { +class CharacteristicWriteFuture: DeviceFuture, PendingResult { let deviceId: String let characteristicId: String let serviceId: String? - let result: (Result) -> Void + let continuation: CheckedContinuation - init(deviceId: String, characteristicId: String, serviceId: String?, result: @escaping (Result) -> Void) { + init(deviceId: String, characteristicId: String, serviceId: String?, continuation: CheckedContinuation) { self.deviceId = deviceId self.characteristicId = characteristicId self.serviceId = serviceId - self.result = result - } - - func fail(with error: Error) { - result(.failure(error)) + self.continuation = continuation } } -class PendingWriteWithoutResponse: DeviceFuture { +class PendingWriteWithoutResponse: DeviceFuture, PendingResult { let deviceId: String let characteristic: CBCharacteristic let data: Data - let result: (Result) -> Void + let continuation: CheckedContinuation - init(deviceId: String, characteristic: CBCharacteristic, data: Data, result: @escaping (Result) -> Void) { + init(deviceId: String, characteristic: CBCharacteristic, data: Data, continuation: CheckedContinuation) { self.deviceId = deviceId self.characteristic = characteristic self.data = data - self.result = result - } - - func fail(with error: Error) { - result(.failure(error)) + self.continuation = continuation } } -class CharacteristicNotifyFuture: DeviceFuture { +class CharacteristicNotifyFuture: DeviceFuture, PendingResult { let deviceId: String let characteristicId: String let serviceId: String? - let result: (Result) -> Void + let continuation: CheckedContinuation - init(deviceId: String, characteristicId: String, serviceId: String?, result: @escaping (Result) -> Void) { + init(deviceId: String, characteristicId: String, serviceId: String?, continuation: CheckedContinuation) { self.deviceId = deviceId self.characteristicId = characteristicId self.serviceId = serviceId - self.result = result - } - - func fail(with error: Error) { - result(.failure(error)) + self.continuation = continuation } } -class DiscoverServicesFuture: DeviceFuture { +class DiscoverServicesFuture: DeviceFuture, PendingResult { let deviceId: String - let result: (Result<[UniversalBleService], Error>) -> Void + let continuation: CheckedContinuation<[UniversalBleService], Error> - init(deviceId: String, result: @escaping (Result<[UniversalBleService], Error>) -> Void) { + init(deviceId: String, continuation: CheckedContinuation<[UniversalBleService], Error>) { self.deviceId = deviceId - self.result = result - } - - func fail(with error: Error) { - result(.failure(error)) + self.continuation = continuation } } -class RssiReadFuture: DeviceFuture { +class RssiReadFuture: DeviceFuture, PendingResult { let deviceId: String - let result: (Result) -> Void + let continuation: CheckedContinuation - init(deviceId: String, result: @escaping (Result) -> Void) { + init(deviceId: String, continuation: CheckedContinuation) { self.deviceId = deviceId - self.result = result - } - - func fail(with error: Error) { - result(.failure(error)) + self.continuation = continuation } } -class DescriptorReadFuture: DeviceFuture { +class DescriptorReadFuture: DeviceFuture, PendingResult { let deviceId: String let descriptorId: String let characteristicId: String let serviceId: String? - let result: (Result) -> Void + let continuation: CheckedContinuation - init(deviceId: String, descriptorId: String, characteristicId: String, serviceId: String?, result: @escaping (Result) -> Void) { + init(deviceId: String, descriptorId: String, characteristicId: String, serviceId: String?, continuation: CheckedContinuation) { self.deviceId = deviceId self.descriptorId = descriptorId self.characteristicId = characteristicId self.serviceId = serviceId - self.result = result - } - - func fail(with error: Error) { - result(.failure(error)) + self.continuation = continuation } } -class DescriptorWriteFuture: DeviceFuture { +class DescriptorWriteFuture: DeviceFuture, PendingResult { let deviceId: String let descriptorId: String let characteristicId: String let serviceId: String? - let result: (Result) -> Void + let continuation: CheckedContinuation - init(deviceId: String, descriptorId: String, characteristicId: String, serviceId: String?, result: @escaping (Result) -> Void) { + init(deviceId: String, descriptorId: String, characteristicId: String, serviceId: String?, continuation: CheckedContinuation) { self.deviceId = deviceId self.descriptorId = descriptorId self.characteristicId = characteristicId self.serviceId = serviceId - self.result = result - } - - func fail(with error: Error) { - result(.failure(error)) + self.continuation = continuation } } diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift index 0897ceed..ac05b462 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift @@ -85,8 +85,8 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // macOS does not support CoreBluetooth state restoration. private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil) #endif - private var availabilityStateUpdateHandlers: [(Result) -> Void] = [] - private var requestPermissionStateUpdateHandlers: [(Result) -> Void] = [] + private var availabilityStateUpdateHandlers: [CheckedContinuation] = [] + private var requestPermissionStateUpdateHandlers: [CheckedContinuation] = [] private var activeServiceDiscoveries: [String: UniversalBleAsyncServiceDiscovery] = [:] private var characteristicReadFutures = [CharacteristicReadFuture]() private var characteristicWriteFutures = [CharacteristicWriteFuture]() @@ -125,17 +125,17 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } #endif - func getBluetoothAvailabilityStateImpl(completion: @escaping (Result) -> Void) { + func getBluetoothAvailabilityState(continuation: CheckedContinuation) { #if os(iOS) if Self.hasBluetoothCentralBackgroundMode, !Self.hasBluetoothPermission { - completion(.success(Self.availabilityStateFromAuthorization)) + continuation.finish(.success(Self.availabilityStateFromAuthorization)) return } #endif if manager.state != .unknown { - completion(.success(manager.state.toAvailabilityState())) + continuation.finish(.success(manager.state.toAvailabilityState())) } else { - availabilityStateUpdateHandlers.append(completion) + availabilityStateUpdateHandlers.append(continuation) _ = manager } } @@ -144,33 +144,33 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral return CBCentralManager.authorization == .allowedAlways } - func requestPermissionsImpl(withAndroidFineLocation _: Bool, completion: @escaping (Result) -> Void) { + func requestPermissions(withAndroidFineLocation _: Bool, continuation: CheckedContinuation) { if manager.state != .unknown { - completePermissionRequest(completion: completion) + completePermissionRequest(continuation: continuation) } else { - requestPermissionStateUpdateHandlers.append(completion) + requestPermissionStateUpdateHandlers.append(continuation) _ = manager } } - func completePermissionRequest(completion: @escaping (Result) -> Void) { + func completePermissionRequest(continuation: CheckedContinuation) { let state = manager.state switch state { case .unauthorized: - completion(.failure(createFlutterError(code: .bluetoothUnauthorized, message: "Not authorized to access Bluetooth"))) + continuation.finish(.failure(createFlutterError(code: .bluetoothUnauthorized, message: "Not authorized to access Bluetooth"))) case .unsupported: - completion(.failure(createFlutterError(code: .notSupported, message: "Bluetooth is not supported"))) + continuation.finish(.failure(createFlutterError(code: .notSupported, message: "Bluetooth is not supported"))) default: - completion(.success(())) + continuation.finish(.success(())) } } - func enableBluetoothImpl(completion: @escaping (Result) -> Void) { - completion(Result.failure(createFlutterError(code: .notSupported))) + func enableBluetooth(continuation: CheckedContinuation) { + continuation.finish(Result.failure(createFlutterError(code: .notSupported))) } - func disableBluetoothImpl(completion: @escaping (Result) -> Void) { - completion(Result.failure(createFlutterError(code: .notSupported))) + func disableBluetooth(continuation: CheckedContinuation) { + continuation.finish(Result.failure(createFlutterError(code: .notSupported))) } func startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) throws { @@ -212,15 +212,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral return isManageScanning } - func requestDeviceImpl(options: AppleAccessorySetupOptions, completion: @escaping (Result) -> Void) { + func requestDevice(options: AppleAccessorySetupOptions, continuation: CheckedContinuation) { #if os(iOS) guard #available(iOS 18.0, *) else { - completion(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit requires iOS 18 or later"))) + continuation.finish(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit requires iOS 18 or later"))) return } - getAccessorySetupManager().showPicker(options: options, completion: completion) + getAccessorySetupManager().showPicker(options: options) { continuation.finish($0) } #else - completion(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit is only supported on iOS 18+"))) + continuation.finish(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit is only supported on iOS 18+"))) #endif } @@ -318,16 +318,16 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral discoverServicesFutures.failAndRemoveAll(matching: deviceId, with: error) } - func discoverServicesImpl(deviceId: String, withDescriptors: Bool, platformConfig _: DiscoverServicesPlatformConfig?, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) { + func discoverServices(deviceId: String, withDescriptors: Bool, platformConfig _: DiscoverServicesPlatformConfig?, continuation: CheckedContinuation<[UniversalBleService], Error>) { guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - completion( + continuation.finish( Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)")) ) return } guard peripheral.state == .connected else { - completion( + continuation.finish( Result.failure(createFlutterError(code: .deviceDisconnected, message: "Device is not connected")) ) return @@ -338,16 +338,16 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // Check if discovery is already in progress if activeServiceDiscoveries[normalizedDeviceId] != nil { UniversalBleLogger.shared.logWarning("Services discovery already in progress for :\(normalizedDeviceId), waiting for completion.") - discoverServicesFutures.append(DiscoverServicesFuture(deviceId: normalizedDeviceId, result: completion)) + discoverServicesFutures.append(DiscoverServicesFuture(deviceId: normalizedDeviceId, continuation: continuation)) return } let wrappedCompletion: (Result<[UniversalBleService], Error>) -> Void = { [weak self] result in guard let self = self else { return } - completion(result) + continuation.finish(result) self.discoverServicesFutures.removeAll { future in if future.deviceId.caseInsensitiveCompare(normalizedDeviceId) == .orderedSame { - future.result(result) + future.complete(result) return true } return false @@ -366,104 +366,104 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral discovery.startDiscovery() } - func setNotifiableImpl(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, completion: @escaping (Result) -> Void) { + func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty, continuation: CheckedContinuation) { UniversalBleLogger.shared.logDebug("SET_NOTIFY -> \(deviceId) \(service) \(characteristic) input=\(bleInputProperty)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) + continuation.finish(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) return } guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else { - completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown characteristic:\(characteristic)"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown characteristic:\(characteristic)"))) return } if bleInputProperty == .notification && !gattCharacteristic.properties.contains(.notify) { - completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportNotify, message: "Characteristic does not support notify"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicDoesNotSupportNotify, message: "Characteristic does not support notify"))) return } if bleInputProperty == .indication && !gattCharacteristic.properties.contains(.indicate) { - completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportIndicate, message: "Characteristic does not support indicate"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicDoesNotSupportIndicate, message: "Characteristic does not support indicate"))) return } let shouldNotify = bleInputProperty != .disabled peripheral.setNotifyValue(shouldNotify, for: gattCharacteristic) - characteristicNotifyFutures.append(CharacteristicNotifyFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion)) + characteristicNotifyFutures.append(CharacteristicNotifyFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, continuation: continuation)) } - func readValueImpl(deviceId: String, service: String, characteristic: String, completion: @escaping (Result) -> Void) { + func readValue(deviceId: String, service: String, characteristic: String, continuation: CheckedContinuation) { UniversalBleLogger.shared.logDebug("READ -> \(deviceId) \(service) \(characteristic)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) + continuation.finish(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) return } guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else { - completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown characteristic:\(characteristic)"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown characteristic:\(characteristic)"))) return } if !gattCharacteristic.properties.contains(.read) { - completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportRead, message: "Characteristic does not support read"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicDoesNotSupportRead, message: "Characteristic does not support read"))) return } peripheral.readValue(for: gattCharacteristic) - characteristicReadFutures.append(CharacteristicReadFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion)) + characteristicReadFutures.append(CharacteristicReadFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, continuation: continuation)) } - func writeValueImpl(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: BleOutputProperty, completion: @escaping (Result) -> Void) { + func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: BleOutputProperty, continuation: CheckedContinuation) { UniversalBleLogger.shared.logDebug("WRITE -> \(deviceId) \(service) \(characteristic) len=\(value.data.count) property=\(bleOutputProperty)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) + continuation.finish(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) return } guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else { - completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown characteristic:\(characteristic)"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown characteristic:\(characteristic)"))) return } guard peripheral.state == .connected else { - completion(Result.failure(createFlutterError(code: .deviceDisconnected, message: "Device Disconnected"))) + continuation.finish(Result.failure(createFlutterError(code: .deviceDisconnected, message: "Device Disconnected"))) return } switch bleOutputProperty { case .withResponse: guard gattCharacteristic.properties.contains(.write) else { - completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportWrite, message: "Characteristic does not support write withResponse"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicDoesNotSupportWrite, message: "Characteristic does not support write withResponse"))) return } - characteristicWriteFutures.append(CharacteristicWriteFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion)) + characteristicWriteFutures.append(CharacteristicWriteFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, continuation: continuation)) 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"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicDoesNotSupportWriteWithoutResponse, message: "Characteristic does not support write withoutResponse"))) return } if peripheral.canSendWriteWithoutResponse { peripheral.writeValue(value.data, for: gattCharacteristic, type: .withoutResponse) - completion(Result.success(())) + continuation.finish(Result.success(())) } else { pendingWriteWithoutResponse.append( PendingWriteWithoutResponse( deviceId: deviceId, characteristic: gattCharacteristic, data: value.data, - result: completion + continuation: continuation ) ) } } } - func readDescriptorValueImpl(deviceId: String, service: String, characteristic: String, descriptor: String, completion: @escaping (Result) -> Void) { + func readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, continuation: CheckedContinuation) { UniversalBleLogger.shared.logDebug("READ_DESCRIPTOR -> \(deviceId) \(service) \(characteristic) \(descriptor)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) + continuation.finish(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) return } guard let gattDescriptor = peripheral.getDescriptor(descriptor, for: characteristic, of: service) else { - completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Descriptor not found:\(descriptor)"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Descriptor not found:\(descriptor)"))) return } peripheral.readValue(for: gattDescriptor) @@ -472,18 +472,18 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral descriptorId: gattDescriptor.uuid.uuidStr, characteristicId: gattDescriptor.characteristic?.uuid.uuidStr ?? characteristic, serviceId: gattDescriptor.characteristic?.service?.uuid.uuidStr ?? service, - result: completion + continuation: continuation )) } - func writeDescriptorValueImpl(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData, completion: @escaping (Result) -> Void) { + func writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData, continuation: CheckedContinuation) { UniversalBleLogger.shared.logDebug("WRITE_DESCRIPTOR -> \(deviceId) \(service) \(characteristic) \(descriptor) len=\(value.data.count)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) + continuation.finish(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) return } guard let gattDescriptor = peripheral.getDescriptor(descriptor, for: characteristic, of: service) else { - completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Descriptor not found:\(descriptor)"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Descriptor not found:\(descriptor)"))) return } @@ -493,7 +493,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral let fullCccdUUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") if gattDescriptor.uuid == cccdUUID || gattDescriptor.uuid == fullCccdUUID { guard let gattCharacteristic = gattDescriptor.characteristic else { - completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Characteristic not found for descriptor"))) + continuation.finish(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Characteristic not found for descriptor"))) return } let enable = value.data.contains { $0 != 0 } @@ -502,7 +502,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, - result: completion + continuation: continuation )) return } @@ -513,57 +513,57 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral descriptorId: gattDescriptor.uuid.uuidStr, characteristicId: gattDescriptor.characteristic?.uuid.uuidStr ?? characteristic, serviceId: gattDescriptor.characteristic?.service?.uuid.uuidStr ?? service, - result: completion + continuation: continuation )) } - func requestMtuImpl(deviceId: String, expectedMtu _: Int64, completion: @escaping (Result) -> Void) { + func requestMtu(deviceId: String, expectedMtu _: Int64, continuation: CheckedContinuation) { UniversalBleLogger.shared.logDebug("REQUEST_MTU -> \(deviceId)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) + continuation.finish(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) return } let mtu = peripheral.maximumWriteValueLength(for: CBCharacteristicWriteType.withoutResponse) let GATT_HEADER_LENGTH = 3 let mtuResult = Int64(mtu + GATT_HEADER_LENGTH) - completion(Result.success(mtuResult)) + continuation.finish(Result.success(mtuResult)) } - func requestConnectionPriorityImpl( + func requestConnectionPriority( deviceId _: String, priority _: BleConnectionPriority, - completion: @escaping (Result) -> Void + continuation: CheckedContinuation ) { - completion(.failure(createFlutterError(code: .notSupported, message: "requestConnectionPriority is not supported on Apple platforms"))) + continuation.finish(.failure(createFlutterError(code: .notSupported, message: "requestConnectionPriority is not supported on Apple platforms"))) } - func readRssiImpl(deviceId: String, completion: @escaping (Result) -> Void) { + func readRssi(deviceId: String, continuation: CheckedContinuation) { UniversalBleLogger.shared.logDebug("READ_RSSI -> \(deviceId)") guard let peripheral = peripheralCache.find(deviceId, manager: manager) else { - completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) + continuation.finish(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) return } peripheral.readRSSI() - rssiReadFutures.append(RssiReadFuture(deviceId: deviceId, result: completion)) + rssiReadFutures.append(RssiReadFuture(deviceId: deviceId, continuation: continuation)) } - func isPairedImpl(deviceId _: String, completion: @escaping (Result) -> Void) { - completion(Result.failure(createFlutterError(code: .notSupported))) + func isPaired(deviceId _: String, continuation: CheckedContinuation) { + continuation.finish(Result.failure(createFlutterError(code: .notSupported))) } - func pairImpl(deviceId _: String, completion: @escaping (Result) -> Void) { - completion(Result.failure(createFlutterError(code: .notImplemented))) + func pair(deviceId _: String, continuation: CheckedContinuation) { + continuation.finish(Result.failure(createFlutterError(code: .notImplemented))) } - func unPairImpl(deviceId: String, completion: @escaping (Result) -> Void) { + func unPair(deviceId: String, continuation: CheckedContinuation) { #if os(iOS) guard #available(iOS 18.0, *) else { - completion(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit requires iOS 18 or later"))) + continuation.finish(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit requires iOS 18 or later"))) return } - getAccessorySetupManager().removeAccessory(deviceId: deviceId, completion: completion) + getAccessorySetupManager().removeAccessory(deviceId: deviceId) { continuation.finish($0) } #else - completion(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit is only supported on iOS 18+"))) + continuation.finish(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit is only supported on iOS 18+"))) #endif } @@ -579,7 +579,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } #endif - func getSystemDevicesImpl(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) { + func getSystemDevices(withServices: [String], continuation: CheckedContinuation<[UniversalBleScanResult], Error>) { var servicesFilter = withServices if servicesFilter.isEmpty { UniversalBleLogger.shared.logInfo("No services filter was set for getting system connected devices. Using default services...") @@ -590,7 +590,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral let filterCBUUID = servicesFilter.map { CBUUID(string: $0) } let bleDevices = manager.retrieveConnectedPeripherals(withServices: filterCBUUID) bleDevices.forEach { peripheralCache.remember($0) } - completion(Result.success(bleDevices.map { peripheral in + continuation.finish(Result.success(bleDevices.map { peripheral in let id = peripheral.uuid.uuidString let name = peripheralCache.advertisedName(for: id) ?? peripheralCache.cached(id)?.name ?? peripheral.name ?? "" @@ -630,12 +630,12 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral sendToDart { try await $0.onAvailabilityChanged(state: state) } // Complete Pending state handler availabilityStateUpdateHandlers.removeAll { handler in - handler(.success(state)) + handler.resume(returning: state) return true } // Complete Pending permission request handler requestPermissionStateUpdateHandlers.removeAll { handler in - completePermissionRequest(completion: handler) + completePermissionRequest(continuation: handler) return true } } @@ -765,7 +765,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } let pending = pendingWriteWithoutResponse.remove(at: index) peripheral.writeValue(pending.data, for: pending.characteristic, type: .withoutResponse) - pending.result(Result.success(())) + pending.complete(Result.success(())) } } @@ -781,9 +781,9 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral let future = characteristicWriteFutures.remove(at: index) if let flutterError = error?.toFlutterError() { UniversalBleLogger.shared.logError("WRITE_FAILED <- \(peripheral.uuid.uuidString) \(characteristic.uuid.uuidStr): \(flutterError.message ?? "")") - future.result(Result.failure(flutterError)) + future.complete(Result.failure(flutterError)) } else { - future.result(Result.success(())) + future.complete(Result.success(())) } } @@ -799,20 +799,20 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral if let flutterError = error?.toFlutterError() { UniversalBleLogger.shared.logError("READ_DESCRIPTOR_FAILED <- \(peripheral.uuid.uuidString) \(descriptor.uuid.uuidStr): \(flutterError.message ?? "")") - future.result(Result.failure(flutterError)) + future.complete(Result.failure(flutterError)) } else if let valueData = descriptor.value as? Data { - future.result(Result.success(FlutterStandardTypedData(bytes: valueData))) + future.complete(Result.success(FlutterStandardTypedData(bytes: valueData))) } else if let numberVal = descriptor.value as? NSNumber { var val = numberVal.uint16Value let data = Data(bytes: &val, count: MemoryLayout.size) - future.result(Result.success(FlutterStandardTypedData(bytes: data))) + future.complete(Result.success(FlutterStandardTypedData(bytes: data))) } else if let stringVal = descriptor.value as? String { let data = Data(stringVal.utf8) - future.result(Result.success(FlutterStandardTypedData(bytes: data))) + future.complete(Result.success(FlutterStandardTypedData(bytes: data))) } else if let cbuuid = descriptor.value as? CBUUID { - future.result(Result.success(FlutterStandardTypedData(bytes: cbuuid.data))) + future.complete(Result.success(FlutterStandardTypedData(bytes: cbuuid.data))) } else { - future.result(Result.success(FlutterStandardTypedData(bytes: Data()))) + future.complete(Result.success(FlutterStandardTypedData(bytes: Data()))) } } @@ -828,9 +828,9 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral if let flutterError = error?.toFlutterError() { UniversalBleLogger.shared.logError("WRITE_DESCRIPTOR_FAILED <- \(peripheral.uuid.uuidString) \(descriptor.uuid.uuidStr): \(flutterError.message ?? "")") - future.result(Result.failure(flutterError)) + future.complete(Result.failure(flutterError)) } else { - future.result(Result.success({}())) + future.complete(Result.success({}())) } } @@ -845,9 +845,9 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral if let flutterError = error?.toFlutterError() { UniversalBleLogger.shared.logError("SET_NOTIFY_FAILED <- \(peripheral.uuid.uuidString) \(characteristic.uuid.uuidStr): \(flutterError.message ?? "")") - future.result(Result.failure(flutterError)) + future.complete(Result.failure(flutterError)) } else { - future.result(Result.success({}())) + future.complete(Result.success({}())) } } @@ -900,11 +900,11 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral if let flutterError = error?.toFlutterError() { UniversalBleLogger.shared.logError("READ_FAILED <- \(peripheral.uuid.uuidString) \(characteristic.uuid.uuidStr): \(flutterError.message ?? "")") - future.result(Result.failure(flutterError)) + future.complete(Result.failure(flutterError)) } else if let characteristicValue = characteristic.value { - future.result(Result.success(FlutterStandardTypedData(bytes: characteristicValue))) + future.complete(Result.success(FlutterStandardTypedData(bytes: characteristicValue))) } else { - future.result(Result.failure(createFlutterError(code: .readFailed, message: "No value"))) + future.complete(Result.failure(createFlutterError(code: .readFailed, message: "No value"))) } } @@ -917,123 +917,124 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral if let flutterError = error?.toFlutterError() { UniversalBleLogger.shared.logError("READ_RSSI_FAILED <- \(peripheral.uuid.uuidString): \(flutterError.message ?? "")") - future.result(Result.failure(flutterError)) + future.complete(Result.failure(flutterError)) } else { - future.result(Result.success(RSSI.int64Value)) + future.complete(Result.success(RSSI.int64Value)) } } - // MARK: - Pigeon 29 async bridges + // MARK: - Pigeon 29 async adapters // - // The generated protocol is `async` since Pigeon 29. The previous - // completion-based implementations are kept (renamed `…Impl`) and bridged - // here so the cooperate behaviour is unchanged. + // The generated protocol is `async` since Pigeon 29. Each operation below has + // an `async` adapter that bridges the protocol to a continuation-based + // implementation (the overload taking a `CheckedContinuation`), so the pending + // request registries hold continuations rather than completion closures. func getBluetoothAvailabilityState() async throws -> AvailabilityState { try await withCheckedThrowingContinuation { continuation in - getBluetoothAvailabilityStateImpl { continuation.resume(with: $0) } + getBluetoothAvailabilityState(continuation: continuation) } } func requestPermissions(withAndroidFineLocation fineLocation: Bool) async throws { try await withCheckedThrowingContinuation { continuation in - requestPermissionsImpl(withAndroidFineLocation: fineLocation) { continuation.resume(with: $0) } + requestPermissions(withAndroidFineLocation: fineLocation, continuation: continuation) } } func enableBluetooth() async throws -> Bool { try await withCheckedThrowingContinuation { continuation in - enableBluetoothImpl { continuation.resume(with: $0) } + enableBluetooth(continuation: continuation) } } func disableBluetooth() async throws -> Bool { try await withCheckedThrowingContinuation { continuation in - disableBluetoothImpl { continuation.resume(with: $0) } + disableBluetooth(continuation: continuation) } } func requestDevice(options: AppleAccessorySetupOptions) async throws -> UniversalDevicePickerResult { try await withCheckedThrowingContinuation { continuation in - requestDeviceImpl(options: options) { continuation.resume(with: $0) } + requestDevice(options: options, continuation: continuation) } } func discoverServices(deviceId: String, withDescriptors: Bool, platformConfig: DiscoverServicesPlatformConfig?) async throws -> [UniversalBleService] { try await withCheckedThrowingContinuation { continuation in - discoverServicesImpl(deviceId: deviceId, withDescriptors: withDescriptors, platformConfig: platformConfig) { continuation.resume(with: $0) } + discoverServices(deviceId: deviceId, withDescriptors: withDescriptors, platformConfig: platformConfig, continuation: continuation) } } func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: BleInputProperty) async throws { try await withCheckedThrowingContinuation { continuation in - setNotifiableImpl(deviceId: deviceId, service: service, characteristic: characteristic, bleInputProperty: bleInputProperty) { continuation.resume(with: $0) } + setNotifiable(deviceId: deviceId, service: service, characteristic: characteristic, bleInputProperty: bleInputProperty, continuation: continuation) } } func readValue(deviceId: String, service: String, characteristic: String) async throws -> FlutterStandardTypedData { try await withCheckedThrowingContinuation { continuation in - readValueImpl(deviceId: deviceId, service: service, characteristic: characteristic) { continuation.resume(with: $0) } + readValue(deviceId: deviceId, service: service, characteristic: characteristic, continuation: continuation) } } func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: BleOutputProperty) async throws { try await withCheckedThrowingContinuation { continuation in - writeValueImpl(deviceId: deviceId, service: service, characteristic: characteristic, value: value, bleOutputProperty: bleOutputProperty) { continuation.resume(with: $0) } + writeValue(deviceId: deviceId, service: service, characteristic: characteristic, value: value, bleOutputProperty: bleOutputProperty, continuation: continuation) } } func readDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String) async throws -> FlutterStandardTypedData { try await withCheckedThrowingContinuation { continuation in - readDescriptorValueImpl(deviceId: deviceId, service: service, characteristic: characteristic, descriptor: descriptor) { continuation.resume(with: $0) } + readDescriptorValue(deviceId: deviceId, service: service, characteristic: characteristic, descriptor: descriptor, continuation: continuation) } } func writeDescriptorValue(deviceId: String, service: String, characteristic: String, descriptor: String, value: FlutterStandardTypedData) async throws { try await withCheckedThrowingContinuation { continuation in - writeDescriptorValueImpl(deviceId: deviceId, service: service, characteristic: characteristic, descriptor: descriptor, value: value) { continuation.resume(with: $0) } + writeDescriptorValue(deviceId: deviceId, service: service, characteristic: characteristic, descriptor: descriptor, value: value, continuation: continuation) } } func requestMtu(deviceId: String, expectedMtu: Int64) async throws -> Int64 { try await withCheckedThrowingContinuation { continuation in - requestMtuImpl(deviceId: deviceId, expectedMtu: expectedMtu) { continuation.resume(with: $0) } + requestMtu(deviceId: deviceId, expectedMtu: expectedMtu, continuation: continuation) } } func requestConnectionPriority(deviceId: String, priority: BleConnectionPriority) async throws { try await withCheckedThrowingContinuation { continuation in - requestConnectionPriorityImpl(deviceId: deviceId, priority: priority) { continuation.resume(with: $0) } + requestConnectionPriority(deviceId: deviceId, priority: priority, continuation: continuation) } } func readRssi(deviceId: String) async throws -> Int64 { try await withCheckedThrowingContinuation { continuation in - readRssiImpl(deviceId: deviceId) { continuation.resume(with: $0) } + readRssi(deviceId: deviceId, continuation: continuation) } } func isPaired(deviceId: String) async throws -> Bool { try await withCheckedThrowingContinuation { continuation in - isPairedImpl(deviceId: deviceId) { continuation.resume(with: $0) } + isPaired(deviceId: deviceId, continuation: continuation) } } func pair(deviceId: String) async throws -> Bool { try await withCheckedThrowingContinuation { continuation in - pairImpl(deviceId: deviceId) { continuation.resume(with: $0) } + pair(deviceId: deviceId, continuation: continuation) } } func unPair(deviceId: String) async throws { try await withCheckedThrowingContinuation { continuation in - unPairImpl(deviceId: deviceId) { continuation.resume(with: $0) } + unPair(deviceId: deviceId, continuation: continuation) } } func getSystemDevices(withServices: [String]) async throws -> [UniversalBleScanResult] { try await withCheckedThrowingContinuation { continuation in - getSystemDevicesImpl(withServices: withServices) { continuation.resume(with: $0) } + getSystemDevices(withServices: withServices, continuation: continuation) } } @@ -1058,15 +1059,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } } - func showPicker(options: AppleAccessorySetupOptions, completion: @escaping (Result) -> Void) { + func showPicker(options: AppleAccessorySetupOptions, continuation: CheckedContinuation) { guard pickerCompletion == nil else { - completion(.failure(createFlutterError(code: .invalidAction, message: "The accessory picker is already open"))) + continuation.finish(.failure(createFlutterError(code: .invalidAction, message: "The accessory picker is already open"))) return } var items: [ASPickerDisplayItem] = [] for item in options.items { guard let image = UIImage(named: item.imageAsset, in: Bundle.main, compatibleWith: nil) else { - completion(.failure(createFlutterError(code: .illegalArgument, message: "Image asset not found: \(item.imageAsset)"))) + continuation.finish(.failure(createFlutterError(code: .illegalArgument, message: "Image asset not found: \(item.imageAsset)"))) return } let descriptor = ASDiscoveryDescriptor() @@ -1081,7 +1082,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral items.append(ASPickerDisplayItem(name: item.displayName, productImage: image, descriptor: descriptor)) } guard !items.isEmpty else { - completion(.failure(createFlutterError(code: .illegalArgument, message: "At least one picker item is required"))) + continuation.finish(.failure(createFlutterError(code: .illegalArgument, message: "At least one picker item is required"))) return } @@ -1097,27 +1098,27 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } } - func removeAccessory(deviceId: String, completion: @escaping (Result) -> Void) { + func removeAccessory(deviceId: String, continuation: CheckedContinuation) { guard let identifier = UUID(uuidString: deviceId) else { - completion(.failure(createFlutterError(code: .illegalArgument, message: "Invalid deviceId: \(deviceId)"))) + continuation.finish(.failure(createFlutterError(code: .illegalArgument, message: "Invalid deviceId: \(deviceId)"))) return } whenActivated({ [weak self] in guard let self else { return } guard let accessory = session.accessories.first(where: { $0.bluetoothIdentifier == identifier }) else { - completion(.failure(createFlutterError(code: .deviceNotFound, message: "Accessory is not managed by AccessorySetupKit: \(deviceId)"))) + continuation.finish(.failure(createFlutterError(code: .deviceNotFound, message: "Accessory is not managed by AccessorySetupKit: \(deviceId)"))) return } session.removeAccessory(accessory) { error in if let error { - completion(.failure(error.toFlutterError())) + continuation.finish(.failure(error.toFlutterError())) } else { - completion(.success(())) + continuation.finish(.success(())) } } }, onCancel: { - completion(.failure(createFlutterError(code: .failed, message: "Accessory setup session was invalidated"))) + continuation.finish(.failure(createFlutterError(code: .failed, message: "Accessory setup session was invalidated"))) }) } @@ -1196,7 +1197,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral pickerCompletion = nil selectedIdentifier = nil selectedName = nil - completion(result) + continuation.finish(result) } } #endif From 56085ac1160dce940c7f5a78f1baf5861a6b5210 Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Fri, 16 Oct 2026 10:31:40 +0200 Subject: [PATCH 4/5] Document deferred Pigeon 29 follow-ups as TODOs Note the two pieces intentionally left out of the migration: coroutine/async cancellation wiring (both platforms) and converting the Android pending-request registries to CancellableContinuation as done on Darwin. --- .../com/navideck/universal_ble/UniversalBleHelper.kt | 3 +++ .../com/navideck/universal_ble/UniversalBlePlugin.kt | 7 +++++++ .../Sources/universal_ble/UniversalBlePlugin.swift | 4 ++++ 3 files changed, 14 insertions(+) diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt index ff73fecc..f99ea951 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -471,6 +471,9 @@ fun Int.parseHciErrorCode(): String? { } } +// TODO: Hold a CancellableContinuation here (as Darwin does) instead of a +// `(Result) -> Unit` callback, and remove the awaitResult bridge in +// UniversalBlePlugin once the nested callback helpers are converted. // Future result classes class DiscoverServicesFuture( val deviceId: String, diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index 46db573a..bb757f58 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -1960,6 +1960,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), // The generated Pigeon 29 interface is suspend-based. Each operation keeps the // callback-driven GATT logic (the overload taking a `(Result) -> Unit`) and // bridges it to coroutines here. + // + // TODO: Wire coroutine cancellation (invokeOnCancellation) so a cancelled + // caller removes its pending request instead of waiting for the GATT callback. + // TODO: Store a CancellableContinuation in each pending-request type (as Darwin + // does) and drop this bridge, once the nested callback helpers + // (discoverServicesOffAlreadyConnectedDevice, PermissionHandler, postMtuResult) + // are converted. private suspend fun awaitResult(block: ((Result) -> Unit) -> Unit): T = suspendCancellableCoroutine { continuation -> diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift index ac05b462..241060f3 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift @@ -929,6 +929,10 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // an `async` adapter that bridges the protocol to a continuation-based // implementation (the overload taking a `CheckedContinuation`), so the pending // request registries hold continuations rather than completion closures. + // + // TODO: Wire task cancellation (withTaskCancellationHandler) so a cancelled + // Dart call removes its pending future instead of leaving the continuation + // suspended until the CoreBluetooth callback arrives. func getBluetoothAvailabilityState() async throws -> AvailabilityState { try await withCheckedThrowingContinuation { continuation in From b319dd8e145b0e63ff9feaa7acaff6e7aff2a71f Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Fri, 16 Oct 2026 10:51:06 +0200 Subject: [PATCH 5/5] Fix iOS AccessorySetupKit continuation refactor and harden callback bridging The Pigeon 29 migration converted the iOS-only AccessorySetupKit picker to CheckedContinuation but left several references in the old closure shape, so the iOS target did not compile: - pickerCompletion now stores the continuation instead of a closure - showPicker assigns the continuation parameter (not the removed completion) - completePicker resumes the stored continuation - requestDevice/unPair pass continuation: instead of a trailing closure Also: - capture the advertising state locally before deferred Dart delivery so a later state change cannot alter the value sent to Dart - stop swallowing genuine CancellationException in the Android peripheral awaitReply timeout bridge --- .../universal_ble/UniversalBlePeripheralPlugin.kt | 10 ++++++++++ .../universal_ble/UniversalBlePeripheralPlugin.swift | 5 +++-- .../Sources/universal_ble/UniversalBlePlugin.swift | 10 +++++----- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt index db54f248..d36cb98b 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePeripheralPlugin.kt @@ -28,8 +28,10 @@ import android.util.Log import io.flutter.plugin.common.BinaryMessenger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException private const val TAG = "UniversalBlePeripheral" private const val REQUEST_CODE_ENABLE_BLUETOOTH = 2342717 @@ -70,7 +72,15 @@ class UniversalBlePeripheralPlugin( private suspend fun awaitReply(block: suspend () -> T?): T? = try { withTimeout(PERIPHERAL_REQUEST_TIMEOUT_MS) { block() } + } catch (_: TimeoutCancellationException) { + // The Dart reply did not arrive in time: answer the central with a + // failure rather than holding the request (and the connection) open. + null + } catch (e: CancellationException) { + // A genuine scope/job cancellation must not be swallowed as a reply. + throw e } catch (_: Throwable) { + // e.g. the Dart side replied with an error. null } diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift index e099a528..84cd5ee7 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePeripheralPlugin.swift @@ -184,9 +184,10 @@ final class UniversalBlePeripheralPlugin: NSObject, UniversalBlePeripheralChanne _: CBPeripheralManager, error: Error? ) { - advertisingState = error == nil ? .advertising : .error + let state: PeripheralAdvertisingState = error == nil ? .advertising : .error + advertisingState = state sendToDart { try await $0.onAdvertisingStateChange( - state: self.advertisingState, + state: state, error: error?.localizedDescription ) } diff --git a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift index 241060f3..b97f77e0 100644 --- a/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift +++ b/darwin/universal_ble/Sources/universal_ble/UniversalBlePlugin.swift @@ -218,7 +218,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral continuation.finish(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit requires iOS 18 or later"))) return } - getAccessorySetupManager().showPicker(options: options) { continuation.finish($0) } + getAccessorySetupManager().showPicker(options: options, continuation: continuation) #else continuation.finish(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit is only supported on iOS 18+"))) #endif @@ -561,7 +561,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral continuation.finish(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit requires iOS 18 or later"))) return } - getAccessorySetupManager().removeAccessory(deviceId: deviceId) { continuation.finish($0) } + getAccessorySetupManager().removeAccessory(deviceId: deviceId, continuation: continuation) #else continuation.finish(.failure(createFlutterError(code: .notSupported, message: "AccessorySetupKit is only supported on iOS 18+"))) #endif @@ -1053,7 +1053,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral /// after that, so the owner drops this manager and builds a fresh one. private(set) var isInvalidated = false private var activationActions: [(run: () -> Void, cancel: () -> Void)] = [] - private var pickerCompletion: ((Result) -> Void)? + private var pickerCompletion: CheckedContinuation? private var selectedIdentifier: UUID? private var selectedName: String? @@ -1090,7 +1090,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral return } - pickerCompletion = completion + pickerCompletion = continuation selectedIdentifier = nil selectedName = nil whenActivated { [weak self] in @@ -1197,7 +1197,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } private func completePicker(_ result: Result) { - guard let completion = pickerCompletion else { return } + guard let continuation = pickerCompletion else { return } pickerCompletion = nil selectedIdentifier = nil selectedName = nil