diff --git a/CHANGELOG.md b/CHANGELOG.md index 260d441..0ae3b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.0.0 +* Unified error codes for all platforms + ## 0.21.1 * Fix device name resolution on Windows * Add `exclusionFilters` to filter out devices from scan results diff --git a/README.md b/README.md index f939988..5bec1c8 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE - [Requesting MTU](#requesting-mtu) - [Command Queue](#command-queue) - [Timeout](#timeout) +- [Error Handling](#error-handling) - [UUID Format Agnostic](#uuid-format-agnostic) ## API Support @@ -456,6 +457,56 @@ UniversalBle.timeout = null; You can also specify the `timeout` parameter when sending a command. This will override the global timeout. +## Error Handling + +Universal BLE provides a unified and type-safe error handling system across all platforms. All errors are represented using the `UniversalBleException` base class with typed error codes from the `UniversalBleErrorCode` enum. + +### Exception Types + +- **`UniversalBleException`**: Base exception class for all BLE errors +- **`ConnectionException`**: Thrown for connection-related errors +- **`PairingException`**: Thrown for pairing-related errors +- **`WebBluetoothGloballyDisabled`**: Thrown when Web Bluetooth is globally disabled + +### Error Codes + +All errors are categorized using the `UniversalBleErrorCode` enum, which includes codes for: +- Connection errors (timeout, failed, rejected, etc.) +- Pairing errors (failed, cancelled, not allowed, etc.) +- Operation errors (not supported, timeout, cancelled, etc.) +- Permission errors (not allowed, unauthorized, access denied, etc.) +- Device errors (not found, disconnected, etc.) +- Service/Characteristic errors (not found, invalid UUID, etc.) +- And many more... + +### Usage + +```dart +try { + await bleDevice.connect(); +} on ConnectionException catch (e) { + // Handle connection-specific errors + switch (e.code) { + case UniversalBleErrorCode.connectionTimeout: + // Handle timeout + break; + case UniversalBleErrorCode.connectionFailed: + // Handle connection failure + break; + case UniversalBleErrorCode.deviceDisconnected: + // Handle disconnection + break; + default: + // Handle other connection errors + } +} on UniversalBleException catch (e) { + // Handle other BLE errors + print('Error code: ${e.code}, Message: ${e.message}'); +} +``` + +The error parser automatically converts platform-specific error formats (strings, numeric codes, PlatformExceptions) into the unified `UniversalBleErrorCode` enum, ensuring consistent error handling across all platforms. + ## UUID Format Agnostic Universal BLE is agnostic to the UUID format of services and characteristics regardless of the platform the app runs on. When passing a UUID, you can pass it in any format (long/short) or character case (upper/lower case) you want. Universal BLE will take care of necessary conversions, across all platforms, so that you don't need to worry about underlying platform differences. 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 6d6a34f..ac103cc 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 (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v26.0.5), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -7,33 +7,67 @@ package com.navideck.universal_ble import android.util.Log import io.flutter.plugin.common.BasicMessageChannel import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +private object UniversalBlePigeonUtils { -private fun wrapResult(result: Any?): List { - return listOf(result) -} + fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") } -private fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) + fun wrapResult(result: Any?): List { + return listOf(result) } -} -private fun createConnectionError(channelName: String): FlutterError { - return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "")} + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).contains(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + +} /** * Error class for passing custom error details to Flutter via a thrown PlatformException. @@ -47,6 +81,77 @@ class FlutterError ( val details: Any? = null ) : Throwable() +/** Unified error codes for all platforms */ +enum class UniversalBleErrorCode(val raw: Int) { + UNKNOWN_ERROR(0), + FAILED(1), + NOT_SUPPORTED(2), + NOT_IMPLEMENTED(3), + CHANNEL_ERROR(4), + BLUETOOTH_NOT_AVAILABLE(5), + BLUETOOTH_NOT_ENABLED(6), + BLUETOOTH_NOT_ALLOWED(7), + BLUETOOTH_UNAUTHORIZED(8), + DEVICE_DISCONNECTED(9), + CONNECTION_TIMEOUT(10), + CONNECTION_FAILED(11), + CONNECTION_REJECTED(12), + CONNECTION_LIMIT_EXCEEDED(13), + CONNECTION_ALREADY_EXISTS(14), + CONNECTION_TERMINATED(15), + CONNECTION_IN_PROGRESS(16), + ILLEGAL_ARGUMENT(17), + DEVICE_NOT_FOUND(18), + SERVICE_NOT_FOUND(19), + CHARACTERISTIC_NOT_FOUND(20), + INVALID_SERVICE_UUID(21), + INVALID_CHARACTERISTIC_UUID(22), + INVALID_OFFSET(23), + INVALID_ATTRIBUTE_LENGTH(24), + INVALID_PDU(25), + INVALID_HANDLE(26), + READ_FAILED(27), + READ_NOT_PERMITTED(28), + WRITE_FAILED(29), + WRITE_NOT_PERMITTED(30), + WRITE_REQUEST_BUSY(31), + INVALID_ACTION(32), + OPERATION_NOT_SUPPORTED(33), + OPERATION_TIMEOUT(34), + OPERATION_CANCELLED(35), + OPERATION_IN_PROGRESS(36), + CHARACTERISTIC_DOES_NOT_SUPPORT_READ(37), + CHARACTERISTIC_DOES_NOT_SUPPORT_WRITE(38), + CHARACTERISTIC_DOES_NOT_SUPPORT_WRITE_WITHOUT_RESPONSE(39), + CHARACTERISTIC_DOES_NOT_SUPPORT_NOTIFY(40), + CHARACTERISTIC_DOES_NOT_SUPPORT_INDICATE(41), + NOT_PAIRED(42), + NOT_PAIRABLE(43), + ALREADY_PAIRED(44), + PAIRING_FAILED(45), + PAIRING_CANCELLED(46), + PAIRING_TIMEOUT(47), + PAIRING_NOT_ALLOWED(48), + AUTHENTICATION_FAILURE(49), + INSUFFICIENT_AUTHENTICATION(50), + INSUFFICIENT_AUTHORIZATION(51), + INSUFFICIENT_ENCRYPTION(52), + INSUFFICIENT_KEY_SIZE(53), + PROTECTION_LEVEL_NOT_MET(54), + ACCESS_DENIED(55), + UNPAIRING_FAILED(56), + ALREADY_UNPAIRED(57), + SCAN_FAILED(58), + STOPPING_SCAN_IN_PROGRESS(59), + WEB_BLUETOOTH_GLOBALLY_DISABLED(60); + + companion object { + fun ofRaw(raw: Int): UniversalBleErrorCode? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class UniversalBleScanResult ( val deviceId: String, @@ -78,6 +183,16 @@ data class UniversalBleScanResult ( services, ) } + override fun equals(other: Any?): Boolean { + if (other !is UniversalBleScanResult) { + return false + } + if (this === other) { + return true + } + return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ @@ -99,6 +214,16 @@ data class UniversalBleService ( characteristics, ) } + override fun equals(other: Any?): Boolean { + if (other !is UniversalBleService) { + return false + } + if (this === other) { + return true + } + return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ @@ -120,6 +245,16 @@ data class UniversalBleCharacteristic ( properties, ) } + override fun equals(other: Any?): Boolean { + if (other !is UniversalBleCharacteristic) { + return false + } + if (this === other) { + return true + } + return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() } /** @@ -148,6 +283,16 @@ data class UniversalScanFilter ( withManufacturerData, ) } + override fun equals(other: Any?): Boolean { + if (other !is UniversalScanFilter) { + return false + } + if (this === other) { + return true + } + return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ @@ -172,6 +317,16 @@ data class UniversalManufacturerDataFilter ( mask, ) } + override fun equals(other: Any?): Boolean { + if (other !is UniversalManufacturerDataFilter) { + return false + } + if (this === other) { + return true + } + return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() } /** Generated class from Pigeon that represents data sent in messages. */ @@ -193,36 +348,51 @@ data class UniversalManufacturerData ( data, ) } + override fun equals(other: Any?): Boolean { + if (other !is UniversalManufacturerData) { + return false + } + if (this === other) { + return true + } + return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() } private open class UniversalBlePigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { 129.toByte() -> { - return (readValue(buffer) as? List)?.let { - UniversalBleScanResult.fromList(it) + return (readValue(buffer) as Long?)?.let { + UniversalBleErrorCode.ofRaw(it.toInt()) } } 130.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalBleService.fromList(it) + UniversalBleScanResult.fromList(it) } } 131.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalBleCharacteristic.fromList(it) + UniversalBleService.fromList(it) } } 132.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalScanFilter.fromList(it) + UniversalBleCharacteristic.fromList(it) } } 133.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalManufacturerDataFilter.fromList(it) + UniversalScanFilter.fromList(it) } } 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalManufacturerDataFilter.fromList(it) + } + } + 135.toByte() -> { return (readValue(buffer) as? List)?.let { UniversalManufacturerData.fromList(it) } @@ -232,30 +402,34 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { } override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { - is UniversalBleScanResult -> { + is UniversalBleErrorCode -> { stream.write(129) - writeValue(stream, value.toList()) + writeValue(stream, value.raw.toLong()) } - is UniversalBleService -> { + is UniversalBleScanResult -> { stream.write(130) writeValue(stream, value.toList()) } - is UniversalBleCharacteristic -> { + is UniversalBleService -> { stream.write(131) writeValue(stream, value.toList()) } - is UniversalScanFilter -> { + is UniversalBleCharacteristic -> { stream.write(132) writeValue(stream, value.toList()) } - is UniversalManufacturerDataFilter -> { + is UniversalScanFilter -> { stream.write(133) writeValue(stream, value.toList()) } - is UniversalManufacturerData -> { + is UniversalManufacturerDataFilter -> { stream.write(134) writeValue(stream, value.toList()) } + is UniversalManufacturerData -> { + stream.write(135) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -302,10 +476,10 @@ interface UniversalBlePlatformChannel { api.getBluetoothAvailabilityState{ result: Result -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(wrapResult(data)) + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) } } } @@ -320,10 +494,10 @@ interface UniversalBlePlatformChannel { api.enableBluetooth{ result: Result -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(wrapResult(data)) + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) } } } @@ -338,10 +512,10 @@ interface UniversalBlePlatformChannel { api.disableBluetooth{ result: Result -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(wrapResult(data)) + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) } } } @@ -359,7 +533,7 @@ interface UniversalBlePlatformChannel { api.startScan(filterArg) listOf(null) } catch (exception: Throwable) { - wrapError(exception) + UniversalBlePigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -375,7 +549,7 @@ interface UniversalBlePlatformChannel { api.stopScan() listOf(null) } catch (exception: Throwable) { - wrapError(exception) + UniversalBlePigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -393,7 +567,7 @@ interface UniversalBlePlatformChannel { api.connect(deviceIdArg) listOf(null) } catch (exception: Throwable) { - wrapError(exception) + UniversalBlePigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -411,7 +585,7 @@ interface UniversalBlePlatformChannel { api.disconnect(deviceIdArg) listOf(null) } catch (exception: Throwable) { - wrapError(exception) + UniversalBlePigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -431,9 +605,9 @@ interface UniversalBlePlatformChannel { api.setNotifiable(deviceIdArg, serviceArg, characteristicArg, bleInputPropertyArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { - reply.reply(wrapResult(null)) + reply.reply(UniversalBlePigeonUtils.wrapResult(null)) } } } @@ -450,10 +624,10 @@ interface UniversalBlePlatformChannel { api.discoverServices(deviceIdArg) { result: Result> -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(wrapResult(data)) + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) } } } @@ -472,10 +646,10 @@ interface UniversalBlePlatformChannel { api.readValue(deviceIdArg, serviceArg, characteristicArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(wrapResult(data)) + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) } } } @@ -493,10 +667,10 @@ interface UniversalBlePlatformChannel { api.requestMtu(deviceIdArg, expectedMtuArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(wrapResult(data)) + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) } } } @@ -517,9 +691,9 @@ interface UniversalBlePlatformChannel { api.writeValue(deviceIdArg, serviceArg, characteristicArg, valueArg, bleOutputPropertyArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { - reply.reply(wrapResult(null)) + reply.reply(UniversalBlePigeonUtils.wrapResult(null)) } } } @@ -536,10 +710,10 @@ interface UniversalBlePlatformChannel { api.isPaired(deviceIdArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(wrapResult(data)) + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) } } } @@ -556,10 +730,10 @@ interface UniversalBlePlatformChannel { api.pair(deviceIdArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(wrapResult(data)) + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) } } } @@ -577,7 +751,7 @@ interface UniversalBlePlatformChannel { api.unPair(deviceIdArg) listOf(null) } catch (exception: Throwable) { - wrapError(exception) + UniversalBlePigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -594,10 +768,10 @@ interface UniversalBlePlatformChannel { api.getSystemDevices(withServicesArg) { result: Result> -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(wrapError(error)) + reply.reply(UniversalBlePigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(wrapResult(data)) + reply.reply(UniversalBlePigeonUtils.wrapResult(data)) } } } @@ -614,7 +788,7 @@ interface UniversalBlePlatformChannel { val wrapped: List = try { listOf(api.getConnectionState(deviceIdArg)) } catch (exception: Throwable) { - wrapError(exception) + UniversalBlePigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -650,7 +824,7 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger, callback(Result.success(Unit)) } } else { - callback(Result.failure(createConnectionError(channelName))) + callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) } } } @@ -667,7 +841,7 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger, callback(Result.success(Unit)) } } else { - callback(Result.failure(createConnectionError(channelName))) + callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) } } } @@ -684,7 +858,7 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger, callback(Result.success(Unit)) } } else { - callback(Result.failure(createConnectionError(channelName))) + callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) } } } @@ -701,7 +875,7 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger, callback(Result.success(Unit)) } } else { - callback(Result.failure(createConnectionError(channelName))) + callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) } } } @@ -718,7 +892,7 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger, callback(Result.success(Unit)) } } else { - callback(Result.failure(createConnectionError(channelName))) + callback(Result.failure(UniversalBlePigeonUtils.createConnectionError(channelName))) } } } diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleFilterUtil.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleFilterUtil.kt index a8a6da8..7361d8b 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleFilterUtil.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleFilterUtil.kt @@ -2,11 +2,8 @@ package com.navideck.universal_ble import android.annotation.SuppressLint import android.bluetooth.le.ScanFilter -import android.bluetooth.le.ScanResult import android.os.ParcelUuid import android.util.Log -import java.nio.ByteBuffer -import java.nio.ByteOrder import java.util.UUID import kotlin.experimental.and @@ -46,7 +43,7 @@ class UniversalBleFilterUtil { } private fun isNameMatchingFilters(scanFilter: UniversalScanFilter, name: String?): Boolean { - val namePrefixFilter = scanFilter.withNamePrefix.filterNotNull() + val namePrefixFilter = scanFilter.withNamePrefix if (namePrefixFilter.isEmpty()) { return true } @@ -74,7 +71,7 @@ class UniversalBleFilterUtil { scanFilter: UniversalScanFilter, manufacturerDataList: List, ): Boolean { - val filters = scanFilter.withManufacturerData.filterNotNull() + val filters = scanFilter.withManufacturerData if (filters.isEmpty()) return true if (manufacturerDataList.isEmpty()) return false return manufacturerDataList.any { msd -> @@ -122,8 +119,8 @@ fun UniversalScanFilter.toScanFilters(serviceUuids: List): List): List): List.toUUIDList(): List { fun String.toBluetoothGatt(): BluetoothGatt { return this.findGatt() - ?: throw FlutterError("IllegalArgument", "Unknown deviceId: $this", null) + ?: throw createFlutterError( + UniversalBleErrorCode.DEVICE_NOT_FOUND, + "Unknown deviceId: $this", + ) } fun String.isKnownGatt(): Boolean { @@ -122,53 +126,6 @@ fun Int.parseScanErrorMessage(): String { } } -fun Int.parseBluetoothStatusCodeError(): String? { - if (this == BluetoothStatusCodes.SUCCESS) return null - return when (this) { - BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED -> "ERROR_BLUETOOTH_NOT_ENABLED" - BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED -> "ERROR_BLUETOOTH_NOT_ALLOWED" - BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED -> "ERROR_DEVICE_NOT_BONDED" - BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED -> "ERROR_GATT_WRITE_NOT_ALLOWED" - BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY -> "ERROR_GATT_WRITE_REQUEST_BUSY" - BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION -> "ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION" - BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND -> "ERROR_PROFILE_SERVICE_NOT_BOUND" - BluetoothStatusCodes.ERROR_UNKNOWN -> "ERROR_UNKNOWN" - BluetoothStatusCodes.FEATURE_NOT_CONFIGURED -> "FEATURE_NOT_CONFIGURED" - BluetoothStatusCodes.FEATURE_NOT_SUPPORTED -> "FEATURE_NOT_SUPPORTED" - BluetoothStatusCodes.FEATURE_SUPPORTED -> "FEATURE_SUPPORTED" - else -> "ErrorCode: $this" - } -} - -fun Int.parseGattErrorCode(): String? { - return when (this) { - BluetoothGatt.GATT_SUCCESS -> null - BluetoothGatt.GATT_READ_NOT_PERMITTED -> "GATT_READ_NOT_PERMITTED" - BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> "GATT_WRITE_NOT_PERMITTED" - BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION -> "GATT_INSUFFICIENT_AUTHENTICATION" - BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED -> "GATT_REQUEST_NOT_SUPPORTED" - BluetoothGatt.GATT_INVALID_OFFSET -> "GATT_INVALID_OFFSET" - BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION -> "GATT_INSUFFICIENT_AUTHORIZATION" - BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH -> "GATT_INVALID_ATTRIBUTE_LENGTH" - BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION -> "GATT_INSUFFICIENT_ENCRYPTION" - BluetoothGatt.GATT_CONNECTION_CONGESTED -> "GATT_CONNECTION_CONGESTED" - BluetoothGatt.GATT_FAILURE -> "GATT_FAILURE" - BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED -> "ERROR_GATT_WRITE_NOT_ALLOWED" - BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY -> "ERROR_GATT_WRITE_REQUEST_BUSY" - BluetoothStatusCodes.FEATURE_NOT_CONFIGURED -> "FEATURE_NOT_CONFIGURED" - 0x01 -> "GATT_INVALID_HANDLE" - 0x04 -> "GATT_INVALID_PDU" - 0x09 -> "GATT_PREPARE_QUEUE_FULL" - 0x0a -> "GATT_ATTR_NOT_FOUND" - 0x0b -> "GATT_ATTR_NOT_LONG" - 0x0c -> "GATT_INSUFFICIENT_KEY_SIZE" - 0x0e -> "GATT_UNLIKELY" - 0x10 -> "GATT_UNSUPPORTED_GROUP" - 0x11 -> "GATT_INSUFFICIENT_RESOURCES" - else -> "Unknown Error: $this" - } -} - val ScanResult.manufacturerDataList: List get() { return scanRecord?.manufacturerSpecificData?.toList()?.map { (key, value) -> @@ -177,7 +134,7 @@ val ScanResult.manufacturerDataList: List } fun SparseArray.toList(): List> { - return (0 until size()).map { index -> + return (0 until size).map { index -> keyAt(index) to valueAt(index) } } @@ -189,16 +146,6 @@ fun BluetoothGatt.getCharacteristic( return getService(UUID.fromString(service))?.getCharacteristic(UUID.fromString(characteristic)) } -fun subscriptionFailedError(error: String? = null): Result { - return Result.failure( - FlutterError( - "Failed", - "Failed to update subscription state", - error - ) - ) -} - fun BluetoothDevice.removeBond() { try { javaClass.getMethod("removeBond").invoke(this) @@ -207,7 +154,6 @@ fun BluetoothDevice.removeBond() { } } - fun BluetoothGattCharacteristic.getPropertiesList(): ArrayList { val propertiesList = arrayListOf() if (properties and BluetoothGattCharacteristic.PROPERTY_BROADCAST > 0) { @@ -241,16 +187,54 @@ fun BluetoothGattCharacteristic.getPropertiesList(): ArrayList { fun Short.toByteArray(byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN): ByteArray = ByteBuffer.allocate(2 /*Short.SIZE_BYTES*/).order(byteOrder).putShort(this).array() -// Errors -fun unknownCharacteristicError(char: String) = - FlutterError("IllegalArgument", "Unknown error", null) +fun createFlutterError( + code: UniversalBleErrorCode, + message: String? = null, + details: String? = null, +) = FlutterError(code.raw.toString(), message, details ?: code.toString()) -val DeviceDisconnectedError: FlutterError = FlutterError( - "DeviceDisconnected", - "Device Disconnected", - null -) +fun gattStatusToUniversalBleErrorCode(code: Int): UniversalBleErrorCode { + return when (code) { + BluetoothGatt.GATT_READ_NOT_PERMITTED -> UniversalBleErrorCode.READ_NOT_PERMITTED + BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> UniversalBleErrorCode.WRITE_NOT_PERMITTED + BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION -> UniversalBleErrorCode.INSUFFICIENT_AUTHENTICATION + BluetoothGatt.GATT_INSUFFICIENT_AUTHORIZATION -> UniversalBleErrorCode.INSUFFICIENT_AUTHORIZATION + BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION -> UniversalBleErrorCode.INSUFFICIENT_ENCRYPTION + BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED -> UniversalBleErrorCode.OPERATION_NOT_SUPPORTED + BluetoothGatt.GATT_INVALID_OFFSET -> UniversalBleErrorCode.INVALID_OFFSET + BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH -> UniversalBleErrorCode.INVALID_ATTRIBUTE_LENGTH + BluetoothGatt.GATT_CONNECTION_CONGESTED -> UniversalBleErrorCode.CONNECTION_FAILED + BluetoothGatt.GATT_FAILURE -> UniversalBleErrorCode.FAILED + 0x01 -> UniversalBleErrorCode.INVALID_HANDLE + 0x04 -> UniversalBleErrorCode.INVALID_PDU + 0x09 -> UniversalBleErrorCode.OPERATION_IN_PROGRESS + 0x0a -> UniversalBleErrorCode.SERVICE_NOT_FOUND + 0x0b -> UniversalBleErrorCode.INVALID_ATTRIBUTE_LENGTH + 0x0c -> UniversalBleErrorCode.INSUFFICIENT_KEY_SIZE + 0x0e -> UniversalBleErrorCode.FAILED + 0x10 -> UniversalBleErrorCode.OPERATION_NOT_SUPPORTED + 0x11 -> UniversalBleErrorCode.FAILED + else -> UniversalBleErrorCode.UNKNOWN_ERROR + } +} + +fun Int.parseBluetoothStatusCodeError(): UniversalBleErrorCode? { + if (this == BluetoothStatusCodes.SUCCESS) return null + return when (this) { + BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED -> UniversalBleErrorCode.BLUETOOTH_NOT_ENABLED + BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED -> UniversalBleErrorCode.BLUETOOTH_NOT_ALLOWED + BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED -> UniversalBleErrorCode.NOT_PAIRED + BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED -> UniversalBleErrorCode.WRITE_NOT_PERMITTED + BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY -> UniversalBleErrorCode.WRITE_REQUEST_BUSY + BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION -> UniversalBleErrorCode.CONNECTION_FAILED + BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND -> UniversalBleErrorCode.SERVICE_NOT_FOUND + BluetoothStatusCodes.ERROR_UNKNOWN -> UniversalBleErrorCode.UNKNOWN_ERROR + BluetoothStatusCodes.FEATURE_NOT_CONFIGURED -> UniversalBleErrorCode.NOT_IMPLEMENTED + BluetoothStatusCodes.FEATURE_NOT_SUPPORTED -> UniversalBleErrorCode.NOT_SUPPORTED + else -> null + } +} fun Int.parseHciErrorCode(): String? { return when (this) { 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 ad61ee8..7dd39ae 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -32,6 +32,7 @@ import io.flutter.plugin.common.PluginRegistry import java.util.UUID import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import androidx.core.content.edit private const val TAG = "UniversalBlePlugin" @@ -102,7 +103,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (bluetoothEnableRequestFuture != null) { callback( Result.failure( - FlutterError("Failed", "Bluetooth enable request in progress", null) + createFlutterError( + UniversalBleErrorCode.OPERATION_IN_PROGRESS, + "Bluetooth enable request in progress" + ) ) ) return @@ -120,7 +124,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (bluetoothDisableRequestFuture != null) { callback( Result.failure( - FlutterError("Failed", "Bluetooth disable request in progress", null) + createFlutterError( + UniversalBleErrorCode.OPERATION_IN_PROGRESS, + "Bluetooth disable request in progress" + ) ) ) return @@ -131,9 +138,9 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } override fun startScan(filter: UniversalScanFilter?) { - if (!isBluetoothAvailable()) throw FlutterError( - "BluetoothNotEnabled", - "Bluetooth not enabled", + if (!isBluetoothAvailable()) throw createFlutterError( + UniversalBleErrorCode.BLUETOOTH_NOT_ENABLED, + "Bluetooth not enabled" ) val builder = ScanSettings.Builder() @@ -143,10 +150,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } val settings = builder.build() - val usesCustomFilters = filter?.usesCustomFilters() ?: false; + val usesCustomFilters = filter?.usesCustomFilters() ?: false try { - val filterServices = filter?.withServices?.filterNotNull()?.toUUIDList() ?: emptyList() + val filterServices = filter?.withServices?.toUUIDList() ?: emptyList() var scanFilters = emptyList() // Set custom scan filter only if required @@ -156,25 +163,25 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), universalBleFilterUtil.serviceFilterUUIDS = filterServices } else { universalBleFilterUtil.scanFilter = null - scanFilters = filter?.toScanFilters(filterServices) ?: emptyList() + scanFilters = filter?.toScanFilters(filterServices) ?: emptyList() } safeScanner.startScan( scanFilters, settings, scanCallback ) } catch (e: Exception) { - throw FlutterError( - "illegalIllegalArgument", + throw createFlutterError( + UniversalBleErrorCode.FAILED, "Failed to start Scan", - e.toString() + details = e.toString() ) } } override fun stopScan() { - if (!isBluetoothAvailable()) throw FlutterError( - "BluetoothNotEnabled", - "Bluetooth not enabled", + if (!isBluetoothAvailable()) throw createFlutterError( + UniversalBleErrorCode.BLUETOOTH_NOT_ENABLED, + "Bluetooth not enabled" ) // check if already scanning safeScanner.stopScan(scanCallback) @@ -192,7 +199,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } return } else if (currentState == BluetoothGatt.STATE_CONNECTING) { - throw FlutterError("Connecting", "Connection already in progress", null) + throw createFlutterError( + UniversalBleErrorCode.CONNECTION_IN_PROGRESS, + "Connection already in progress" + ) } } @@ -240,7 +250,12 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), discoverServicesFutureList.add(DiscoverServicesFuture(deviceId, callback)) } else { callback( - Result.failure(FlutterError("Failed", "Failed to discover services", null)) + Result.failure( + createFlutterError( + UniversalBleErrorCode.FAILED, + "Failed to discover services" + ) + ) ) } } catch (e: FlutterError) { @@ -253,7 +268,12 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), discoverServicesFutureList.filter { it.deviceId == gatt.device.address }.forEach { discoverServicesFutureList.remove(it) it.result( - Result.failure(FlutterError("Failed", "Failed to discover services", null)) + Result.failure( + createFlutterError( + UniversalBleErrorCode.FAILED, + "Failed to discover services" + ) + ) ) } return @@ -290,7 +310,14 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), gatt.getCharacteristic(service, characteristic) if (gattCharacteristic == null) { - callback(subscriptionFailedError("characteristic not found")) + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND, + "characteristic not found" + ) + ) + ) return } @@ -298,7 +325,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), gattCharacteristic.getDescriptor(ccdCharacteristic) val bleInputPropertyEnum: BleInputProperty = - BleInputProperty.values().first { it.value == bleInputProperty } + BleInputProperty.entries.first { it.value == bleInputProperty } val (value, enable) = when (bleInputPropertyEnum) { BleInputProperty.Notification -> BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE to true @@ -310,14 +337,30 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), // Some devices do not need CCCD to update @Suppress("DEPRECATION") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val status = gatt.writeDescriptor(descriptor, value) if (gatt.writeDescriptor(descriptor, value) != BluetoothStatusCodes.SUCCESS) { - callback(subscriptionFailedError("Failed to update descriptor")) + callback( + Result.failure( + createFlutterError( + status.parseBluetoothStatusCodeError() + ?: UniversalBleErrorCode.FAILED, + "Failed to update descriptor" + ) + ) + ) return } } else { descriptor.value = value if (!gatt.writeDescriptor(descriptor)) { - callback(subscriptionFailedError("Failed to update descriptor")) + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.FAILED, + "Failed to update descriptor" + ) + ) + ) return } } @@ -339,12 +382,27 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), callback(Result.success(Unit)) } } else { - callback(subscriptionFailedError()) + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.FAILED, + "Failed to update subscription state" + ) + ) + ) } } catch (e: FlutterError) { callback(Result.failure(e)) } catch (e: Exception) { - callback(subscriptionFailedError(e.toString())) + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.FAILED, + "Failed to update subscription state", + e.toString() + ) + ) + ) } } @@ -359,13 +417,23 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), val gattCharacteristic = gatt.getCharacteristic(service, characteristic) if (gattCharacteristic == null) { callback( - Result.failure(FlutterError("IllegalArgument", "Unknown characteristic", null)) + Result.failure( + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND, + "Unknown characteristic" + ) + ) ) return } if (!gatt.readCharacteristic(gattCharacteristic)) { callback( - Result.failure(unknownCharacteristicError(characteristic)) + Result.failure( + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND, + "$characteristic not found", + ) + ) ) return } @@ -383,8 +451,8 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } catch (e: Exception) { callback( Result.failure( - FlutterError( - "Failed", + createFlutterError( + UniversalBleErrorCode.READ_FAILED, "Failed to read value", e.toString() ) @@ -410,10 +478,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } else { it.result( Result.failure( - FlutterError( - status.toString(), - "Failed to read: (${status.parseGattErrorCode()})", - null, + createFlutterError( + gattStatusToUniversalBleErrorCode(status), + "Failed to read", + status.toString() ) ) ) @@ -434,33 +502,38 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), val gatt = deviceId.toBluetoothGatt() val gattCharacteristic = gatt.getCharacteristic(service, characteristic) if (gattCharacteristic == null) { - callback(Result.failure(unknownCharacteristicError(characteristic))) + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_NOT_FOUND, + "$characteristic not found", + ) + ) + ) return } var writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - if (bleOutputProperty == BleOutputProperty.withResponse.value) { + if (bleOutputProperty == BleOutputProperty.WithResponse.value) { if (gattCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE == 0) { callback( Result.failure( - FlutterError( - "IllegalArgument", - "Characteristic does not support write withResponse", - null + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_DOES_NOT_SUPPORT_WRITE, + "Characteristic does not support write withResponse" ) ) ) return } writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT - } else if (bleOutputProperty == BleOutputProperty.withoutResponse.value) { + } else if (bleOutputProperty == BleOutputProperty.WithoutResponse.value) { if (gattCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE == 0) { callback( Result.failure( - FlutterError( - "IllegalArgument", - "Characteristic does not support write withoutResponse", - null + createFlutterError( + UniversalBleErrorCode.CHARACTERISTIC_DOES_NOT_SUPPORT_WRITE_WITHOUT_RESPONSE, + "Characteristic does not support write withoutResponse" ) ) ) @@ -495,10 +568,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), writeResultFutureList.remove(writeFuture) callback( Result.failure( - FlutterError( - "WriteError", - "Failed to write: ${result.parseGattErrorCode()}", - null + createFlutterError( + gattStatusToUniversalBleErrorCode(result), + "Failed to write", + result.toString() ) ) ) @@ -524,10 +597,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } else { it.result( Result.failure( - FlutterError( - status.toString(), - "Failed to write: (${status.parseGattErrorCode()})", - null, + createFlutterError( + gattStatusToUniversalBleErrorCode(status), + "Failed to write", + status.toString() ) ) ) @@ -553,7 +626,14 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (status == BluetoothGatt.GATT_SUCCESS) { it.result(Result.success(mtu.toLong())) } else { - it.result(Result.failure(FlutterError("Failed to change MTU", null, null))) + it.result( + Result.failure( + createFlutterError( + UniversalBleErrorCode.FAILED, + "Failed to change MTU" + ) + ) + ) } } } @@ -580,10 +660,9 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (pendingFuture != null) { callback( Result.failure( - FlutterError( - "InProgress", - "Pairing already in progress", - null + createFlutterError( + UniversalBleErrorCode.OPERATION_IN_PROGRESS, + "Pairing already in progress" ) ) ) @@ -594,12 +673,19 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (remoteDevice.createBond()) { pairResultFutures[deviceId] = callback } else { - callback(Result.failure(FlutterError("Failed", "Failed to pair", null))) + callback( + Result.failure( + createFlutterError( + UniversalBleErrorCode.PAIRING_FAILED, + "Failed to pair" + ) + ) + ) } } catch (e: Exception) { callback( Result.failure( - FlutterError("Failed", e.toString(), null) + createFlutterError(UniversalBleErrorCode.FAILED, e.toString()) ) ) } @@ -664,7 +750,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), try { val timeout = (devices.size * 2).toLong() latch.await(timeout, TimeUnit.SECONDS) - } catch (e: InterruptedException) { + } catch (_: InterruptedException) { Thread.currentThread().interrupt() } @@ -749,10 +835,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), private fun cleanConnection(gatt: BluetoothGatt) { gatt.removeCache() gatt.disconnect() - + val deviceDisconnectedError: FlutterError = createFlutterError( + UniversalBleErrorCode.DEVICE_DISCONNECTED, + "Device Disconnected", + ) readResultFutureList.removeAll { if (it.deviceId == gatt.device.address) { - it.result(Result.failure(DeviceDisconnectedError)) + it.result(Result.failure(deviceDisconnectedError)) true } else { false @@ -760,7 +849,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } writeResultFutureList.removeAll { if (it.deviceId == gatt.device.address) { - it.result(Result.failure(DeviceDisconnectedError)) + it.result(Result.failure(deviceDisconnectedError)) true } else { false @@ -768,7 +857,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } subscriptionResultFutureList.removeAll { if (it.deviceId == gatt.device.address) { - it.result(Result.failure(DeviceDisconnectedError)) + it.result(Result.failure(deviceDisconnectedError)) true } else { false @@ -776,7 +865,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } mtuResultFutureList.removeAll { if (it.deviceId == gatt.device.address) { - it.result(Result.failure(DeviceDisconnectedError)) + it.result(Result.failure(deviceDisconnectedError)) true } else { false @@ -784,7 +873,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } discoverServicesFutureList.removeAll { if (it.deviceId == gatt.device.address) { - it.result(Result.failure(DeviceDisconnectedError)) + it.result(Result.failure(deviceDisconnectedError)) true } else { false @@ -830,7 +919,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), Log.v(TAG, "${device.address} BOND_BONDING") } - BluetoothDevice.BOND_BONDED -> { + BOND_BONDED -> { onBondStateUpdate(device.address, true) } @@ -944,7 +1033,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (descriptor?.uuid.toString() == ccdCharacteristic.toString()) { val char: String? = descriptor?.characteristic?.uuid?.toString() val service: String? = descriptor?.characteristic?.service?.uuid?.toString() - val deviceId: String? = gatt?.device?.address; + val deviceId: String? = gatt?.device?.address if (deviceId != null && char != null && service != null) { updateSubscriptionState(deviceId, char, service, status) } @@ -963,14 +1052,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), it.serviceId == service }.forEach { subscriptionResultFutureList.remove(it) - val error: String? = status.parseGattErrorCode() - if (error != null) { + if (status != BluetoothGatt.GATT_SUCCESS) { it.result( Result.failure( - FlutterError( - status.toString(), - "Failed to update subscription state: $error", - null, + createFlutterError( + gattStatusToUniversalBleErrorCode(status), + "Failed to update subscription state", + status.toString() ) ) ) @@ -989,7 +1077,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), "com.navideck.universal_ble.services", Context.MODE_PRIVATE ) - cachedServicesSharedPref.edit().putStringSet(deviceId, services.toSet()).apply() + cachedServicesSharedPref.edit { putStringSet(deviceId, services.toSet()) } cachedServicesMap[deviceId] = services } diff --git a/build_pigeon.sh b/build_pigeon.sh new file mode 100755 index 0000000..3dbe7ba --- /dev/null +++ b/build_pigeon.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +echo "Building pigeon..." +dart run pigeon --input pigeon/universal_ble.dart +echo "Pigeon built successfully" + +echo "Formatting generated files..." +dart format lib/src/universal_ble_pigeon/universal_ble.g.dart +echo "Generated files formatted successfully" + +echo "Done" \ No newline at end of file diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index eb5424e..4ead34e 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v26.0.5), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -15,9 +15,9 @@ import Foundation final class PigeonError: Error { let code: String let message: String? - let details: Any? + let details: Sendable? - init(code: String, message: String?, details: Any?) { + init(code: String, message: String?, details: Sendable?) { self.code = code self.message = message self.details = details @@ -26,7 +26,7 @@ final class PigeonError: Error { var localizedDescription: String { return "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" - } + } } private func wrapResult(_ result: Any?) -> [Any?] { @@ -68,8 +68,137 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } +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 + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsUniversalBle(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsUniversalBle(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashUniversalBle(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashUniversalBle(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashUniversalBle(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +/// Unified error codes for all platforms +enum UniversalBleErrorCode: Int { + case unknownError = 0 + case failed = 1 + case notSupported = 2 + case notImplemented = 3 + case channelError = 4 + case bluetoothNotAvailable = 5 + case bluetoothNotEnabled = 6 + case bluetoothNotAllowed = 7 + case bluetoothUnauthorized = 8 + case deviceDisconnected = 9 + case connectionTimeout = 10 + case connectionFailed = 11 + case connectionRejected = 12 + case connectionLimitExceeded = 13 + case connectionAlreadyExists = 14 + case connectionTerminated = 15 + case connectionInProgress = 16 + case illegalArgument = 17 + case deviceNotFound = 18 + case serviceNotFound = 19 + case characteristicNotFound = 20 + case invalidServiceUuid = 21 + case invalidCharacteristicUuid = 22 + case invalidOffset = 23 + case invalidAttributeLength = 24 + case invalidPdu = 25 + case invalidHandle = 26 + case readFailed = 27 + case readNotPermitted = 28 + case writeFailed = 29 + case writeNotPermitted = 30 + case writeRequestBusy = 31 + case invalidAction = 32 + case operationNotSupported = 33 + case operationTimeout = 34 + case operationCancelled = 35 + case operationInProgress = 36 + case characteristicDoesNotSupportRead = 37 + case characteristicDoesNotSupportWrite = 38 + case characteristicDoesNotSupportWriteWithoutResponse = 39 + case characteristicDoesNotSupportNotify = 40 + case characteristicDoesNotSupportIndicate = 41 + case notPaired = 42 + case notPairable = 43 + case alreadyPaired = 44 + case pairingFailed = 45 + case pairingCancelled = 46 + case pairingTimeout = 47 + case pairingNotAllowed = 48 + case authenticationFailure = 49 + case insufficientAuthentication = 50 + case insufficientAuthorization = 51 + case insufficientEncryption = 52 + case insufficientKeySize = 53 + case protectionLevelNotMet = 54 + case accessDenied = 55 + case unpairingFailed = 56 + case alreadyUnpaired = 57 + case scanFailed = 58 + case stoppingScanInProgress = 59 + case webBluetoothGloballyDisabled = 60 +} + /// Generated class from Pigeon that represents data sent in messages. -struct UniversalBleScanResult { +struct UniversalBleScanResult: Hashable { var deviceId: String var name: String? = nil var isPaired: Bool? = nil @@ -78,7 +207,6 @@ struct UniversalBleScanResult { var services: [String]? = nil - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBleScanResult? { let deviceId = pigeonVar_list[0] as! String @@ -107,15 +235,19 @@ struct UniversalBleScanResult { services, ] } + static func == (lhs: UniversalBleScanResult, rhs: UniversalBleScanResult) -> Bool { + return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashUniversalBle(value: toList(), hasher: &hasher) + } } /// Generated class from Pigeon that represents data sent in messages. -struct UniversalBleService { +struct UniversalBleService: Hashable { var uuid: String var characteristics: [UniversalBleCharacteristic]? = nil - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBleService? { let uuid = pigeonVar_list[0] as! String @@ -132,15 +264,19 @@ struct UniversalBleService { characteristics, ] } + static func == (lhs: UniversalBleService, rhs: UniversalBleService) -> Bool { + return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashUniversalBle(value: toList(), hasher: &hasher) + } } /// Generated class from Pigeon that represents data sent in messages. -struct UniversalBleCharacteristic { +struct UniversalBleCharacteristic: Hashable { var uuid: String var properties: [Int64] - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> UniversalBleCharacteristic? { let uuid = pigeonVar_list[0] as! String @@ -157,18 +293,22 @@ struct UniversalBleCharacteristic { properties, ] } + static func == (lhs: UniversalBleCharacteristic, rhs: UniversalBleCharacteristic) -> Bool { + return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashUniversalBle(value: toList(), hasher: &hasher) + } } /// Scan Filters /// /// Generated class from Pigeon that represents data sent in messages. -struct UniversalScanFilter { +struct UniversalScanFilter: Hashable { var withServices: [String] var withNamePrefix: [String] var withManufacturerData: [UniversalManufacturerDataFilter] - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> UniversalScanFilter? { let withServices = pigeonVar_list[0] as! [String] @@ -188,16 +328,20 @@ struct UniversalScanFilter { withManufacturerData, ] } + static func == (lhs: UniversalScanFilter, rhs: UniversalScanFilter) -> Bool { + return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashUniversalBle(value: toList(), hasher: &hasher) + } } /// Generated class from Pigeon that represents data sent in messages. -struct UniversalManufacturerDataFilter { +struct UniversalManufacturerDataFilter: Hashable { var companyIdentifier: Int64 var data: FlutterStandardTypedData? = nil var mask: FlutterStandardTypedData? = nil - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> UniversalManufacturerDataFilter? { let companyIdentifier = pigeonVar_list[0] as! Int64 @@ -217,15 +361,19 @@ struct UniversalManufacturerDataFilter { mask, ] } + static func == (lhs: UniversalManufacturerDataFilter, rhs: UniversalManufacturerDataFilter) -> Bool { + return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashUniversalBle(value: toList(), hasher: &hasher) + } } /// Generated class from Pigeon that represents data sent in messages. -struct UniversalManufacturerData { +struct UniversalManufacturerData: Hashable { var companyIdentifier: Int64 var data: FlutterStandardTypedData - // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ pigeonVar_list: [Any?]) -> UniversalManufacturerData? { let companyIdentifier = pigeonVar_list[0] as! Int64 @@ -242,22 +390,33 @@ struct UniversalManufacturerData { data, ] } + static func == (lhs: UniversalManufacturerData, rhs: UniversalManufacturerData) -> Bool { + return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashUniversalBle(value: toList(), hasher: &hasher) + } } private class UniversalBlePigeonCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 129: - return UniversalBleScanResult.fromList(self.readValue() as! [Any?]) + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return UniversalBleErrorCode(rawValue: enumResultAsInt) + } + return nil case 130: - return UniversalBleService.fromList(self.readValue() as! [Any?]) + return UniversalBleScanResult.fromList(self.readValue() as! [Any?]) case 131: - return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?]) + return UniversalBleService.fromList(self.readValue() as! [Any?]) case 132: - return UniversalScanFilter.fromList(self.readValue() as! [Any?]) + return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?]) case 133: - return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + return UniversalScanFilter.fromList(self.readValue() as! [Any?]) case 134: + return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + case 135: return UniversalManufacturerData.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) @@ -267,24 +426,27 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader { private class UniversalBlePigeonCodecWriter: FlutterStandardWriter { override func writeValue(_ value: Any) { - if let value = value as? UniversalBleScanResult { + if let value = value as? UniversalBleErrorCode { super.writeByte(129) - super.writeValue(value.toList()) - } else if let value = value as? UniversalBleService { + super.writeValue(value.rawValue) + } else if let value = value as? UniversalBleScanResult { super.writeByte(130) super.writeValue(value.toList()) - } else if let value = value as? UniversalBleCharacteristic { + } else if let value = value as? UniversalBleService { super.writeByte(131) super.writeValue(value.toList()) - } else if let value = value as? UniversalScanFilter { + } else if let value = value as? UniversalBleCharacteristic { super.writeByte(132) super.writeValue(value.toList()) - } else if let value = value as? UniversalManufacturerDataFilter { + } else if let value = value as? UniversalScanFilter { super.writeByte(133) super.writeValue(value.toList()) - } else if let value = value as? UniversalManufacturerData { + } else if let value = value as? UniversalManufacturerDataFilter { super.writeByte(134) super.writeValue(value.toList()) + } else if let value = value as? UniversalManufacturerData { + super.writeByte(135) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -650,7 +812,7 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { let details: String? = nilOrValue(listResponse[2]) completion(.failure(PigeonError(code: code, message: message, details: details))) } else { - completion(.success(Void())) + completion(.success(())) } } } @@ -668,7 +830,7 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { let details: String? = nilOrValue(listResponse[2]) completion(.failure(PigeonError(code: code, message: message, details: details))) } else { - completion(.success(Void())) + completion(.success(())) } } } @@ -686,7 +848,7 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { let details: String? = nilOrValue(listResponse[2]) completion(.failure(PigeonError(code: code, message: message, details: details))) } else { - completion(.success(Void())) + completion(.success(())) } } } @@ -704,7 +866,7 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { let details: String? = nilOrValue(listResponse[2]) completion(.failure(PigeonError(code: code, message: message, details: details))) } else { - completion(.success(Void())) + completion(.success(())) } } } @@ -722,7 +884,7 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { let details: String? = nilOrValue(listResponse[2]) completion(.failure(PigeonError(code: code, message: message, details: details))) } else { - completion(.success(Void())) + completion(.success(())) } } } diff --git a/darwin/Classes/UniversalBleHelper.swift b/darwin/Classes/UniversalBleHelper.swift index 6eb35f4..ade3600 100644 --- a/darwin/Classes/UniversalBleHelper.swift +++ b/darwin/Classes/UniversalBleHelper.swift @@ -103,12 +103,69 @@ extension CBManagerState { } } +/// Maps string error codes to UniversalBleErrorCode enum +func mapErrorCodeToEnum(_ code: String) -> UniversalBleErrorCode { + switch code.lowercased() { + case "notsupported", "not_supported": + return .notSupported + case "notimplemented", "not_implemented": + return .notImplemented + case "channel-error", "channelerror": + return .channelError + case "failed": + return .failed + case "devicedisconnected", "device_disconnected": + return .deviceDisconnected + case "illegalargument", "illegal_argument": + return .illegalArgument + case "invalidaction", "invalid_action": + return .invalidAction + case "readfailed", "read_failed": + return .readFailed + case "devicenotfound", "device_not_found": + return .deviceNotFound + case "servicenotfound", "service_not_found": + return .serviceNotFound + case "characteristicnotfound", "characteristic_not_found": + return .characteristicNotFound + case "invalidserviceuuid", "invalid_service_uuid": + return .invalidServiceUuid + case "characteristicdoesnotsupportread": + return .characteristicDoesNotSupportRead + case "characteristicdoesnotsupportwrite": + return .characteristicDoesNotSupportWrite + case "characteristicdoesnotsupportwritewithoutresponse": + return .characteristicDoesNotSupportWriteWithoutResponse + case "characteristicdoesnotsupportnotify": + return .characteristicDoesNotSupportNotify + case "characteristicdoesnotsupportindicate": + return .characteristicDoesNotSupportIndicate + default: + return .unknownError + } +} + +/// Creates a PigeonError with the error code enum in details +func createFlutterError( + code: UniversalBleErrorCode, + message: String? = nil, + details: String? = nil +) -> PigeonError { + // Pass the enum's rawValue (Int) in code as string, and enum name in details + return PigeonError( + code: code.rawValue.description, + message: message, + details: details ?? code.rawValue + ) +} + extension Error { - func toPigeonError() -> PigeonError { + func toFlutterError() -> PigeonError { let nsError = self as NSError let errorCode: String = .init(nsError.code) let errorDescription: String = nsError.localizedDescription - return PigeonError(code: errorCode, message: errorDescription, details: nil) + let mappedCode = mapErrorCodeToEnum(errorCode) + return createFlutterError(code: mappedCode, message: errorDescription, details: errorCode) } } diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index 0a54537..7ffced3 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -55,11 +55,11 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } func enableBluetooth(completion: @escaping (Result) -> Void) { - completion(Result.failure(PigeonError(code: "NotSupported", message: nil, details: nil))) + completion(Result.failure(createFlutterError(code: .notSupported))) } func disableBluetooth(completion: @escaping (Result) -> Void) { - completion(Result.failure(PigeonError(code: "NotSupported", message: nil, details: nil))) + completion(Result.failure(createFlutterError(code: .notSupported))) } func startScan(filter: UniversalScanFilter?) throws { @@ -122,7 +122,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral characteristicReadFutures.removeAll { future in if future.deviceId == deviceId { future.result( - Result.failure(PigeonError(code: "DeviceDisconnected", message: "Device Disconnected", details: nil)) + Result.failure(createFlutterError(code: .deviceDisconnected, message: "Device Disconnected")) ) return true } @@ -131,7 +131,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral characteristicWriteFutures.removeAll { future in if future.deviceId == deviceId { future.result( - Result.failure(PigeonError(code: "DeviceDisconnected", message: "Device Disconnected", details: nil)) + Result.failure(createFlutterError(code: .deviceDisconnected, message: "Device Disconnected")) ) return true } @@ -140,7 +140,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral characteristicNotifyFutures.removeAll { future in if future.deviceId == deviceId { future.result( - Result.failure(PigeonError(code: "DeviceDisconnected", message: "Device Disconnected", details: nil)) + Result.failure(createFlutterError(code: .deviceDisconnected, message: "Device Disconnected")) ) return true } @@ -149,7 +149,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral discoverServicesFutures.removeAll { future in if future.deviceId == deviceId { future.result( - Result.failure(PigeonError(code: "DeviceDisconnected", message: "Device Disconnected", details: nil)) + Result.failure(createFlutterError(code: .deviceDisconnected, message: "Device Disconnected")) ) return true } @@ -161,7 +161,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func discoverServices(deviceId: String, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) { guard let peripheral = deviceId.findPeripheral(manager: manager) else { completion( - Result.failure(PigeonError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil)) + Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)")) ) return } @@ -198,22 +198,22 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64, completion: @escaping (Result) -> Void) { guard let peripheral = deviceId.findPeripheral(manager: manager) else { - completion(Result.failure(PigeonError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil))) + completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) return } guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else { - completion(Result.failure(PigeonError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil))) + completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown characteristic:\(characteristic)"))) return } if bleInputProperty == BleInputProperty.notification.rawValue && !gattCharacteristic.properties.contains(.notify) { - completion(Result.failure(PigeonError(code: "InvalidAction", message: "Characteristic does not support notify", details: nil))) + completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportNotify, message: "Characteristic does not support notify"))) return } if bleInputProperty == BleInputProperty.indication.rawValue && !gattCharacteristic.properties.contains(.indicate) { - completion(Result.failure(PigeonError(code: "InvalidAction", message: "Characteristic does not support indicate", details: nil))) + completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportIndicate, message: "Characteristic does not support indicate"))) return } @@ -224,15 +224,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func readValue(deviceId: String, service: String, characteristic: String, completion: @escaping (Result) -> Void) { guard let peripheral = deviceId.findPeripheral(manager: manager) else { - completion(Result.failure(PigeonError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil))) + completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) return } guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else { - completion(Result.failure(PigeonError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil))) + completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown characteristic:\(characteristic)"))) return } if !gattCharacteristic.properties.contains(.read) { - completion(Result.failure(PigeonError(code: "InvalidAction", message: "Characteristic does not support read", details: nil))) + completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportRead, message: "Characteristic does not support read"))) return } peripheral.readValue(for: gattCharacteristic) @@ -241,11 +241,11 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: Int64, completion: @escaping (Result) -> Void) { guard let peripheral = deviceId.findPeripheral(manager: manager) else { - completion(Result.failure(PigeonError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil))) + completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) return } guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else { - completion(Result.failure(PigeonError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil))) + completion(Result.failure(createFlutterError(code: .characteristicNotFound, message: "Unknown characteristic:\(characteristic)"))) return } @@ -253,12 +253,12 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral if type == CBCharacteristicWriteType.withResponse { if !gattCharacteristic.properties.contains(.write) { - completion(Result.failure(PigeonError(code: "InvalidAction", message: "Characteristic does not support write withResponse", details: nil))) + completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportWrite, message: "Characteristic does not support write withResponse"))) return } } else if type == CBCharacteristicWriteType.withoutResponse { if !gattCharacteristic.properties.contains(.writeWithoutResponse) { - completion(Result.failure(PigeonError(code: "InvalidAction", message: "Characteristic does not support write withoutResponse", details: nil))) + completion(Result.failure(createFlutterError(code: .characteristicDoesNotSupportWriteWithoutResponse, message: "Characteristic does not support write withoutResponse"))) return } } @@ -275,7 +275,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func requestMtu(deviceId: String, expectedMtu _: Int64, completion: @escaping (Result) -> Void) { guard let peripheral = deviceId.findPeripheral(manager: manager) else { - completion(Result.failure(PigeonError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil))) + completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) return } let mtu = peripheral.maximumWriteValueLength(for: CBCharacteristicWriteType.withoutResponse) @@ -285,15 +285,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } func isPaired(deviceId _: String, completion: @escaping (Result) -> Void) { - completion(Result.failure(PigeonError(code: "NotSupported", message: nil, details: nil))) + completion(Result.failure(createFlutterError(code: .notSupported))) } func pair(deviceId _: String, completion: @escaping (Result) -> Void) { - completion(Result.failure(PigeonError(code: "Implemented in Dart", message: nil, details: nil))) + completion(Result.failure(createFlutterError(code: .notImplemented))) } func unPair(deviceId _: String) throws { - throw PigeonError(code: "NotSupported", message: nil, details: nil) + throw createFlutterError(code: .notSupported) } func getSystemDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) { @@ -427,8 +427,8 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { characteristicWriteFutures.removeAll { future in if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { - if let pigeonError = error?.toPigeonError() { - future.result(Result.failure(pigeonError)) + if let flutterError = error?.toFlutterError() { + future.result(Result.failure(flutterError)) } else { future.result(Result.success({}())) } @@ -441,8 +441,8 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { characteristicNotifyFutures.removeAll { future in if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { - if let pigeonError = error?.toPigeonError() { - future.result(Result.failure(pigeonError)) + if let flutterError = error?.toFlutterError() { + future.result(Result.failure(flutterError)) } else { future.result(Result.success({}())) } @@ -467,13 +467,13 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // Update futures for readValue characteristicReadFutures.removeAll { future in if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { - if let pigeonError = error?.toPigeonError() { - future.result(Result.failure(pigeonError)) + if let flutterError = error?.toFlutterError() { + future.result(Result.failure(flutterError)) } else { if let characteristicValue = characteristic.value { future.result(Result.success(FlutterStandardTypedData(bytes: characteristicValue))) } else { - future.result(Result.failure(PigeonError(code: "ReadFailed", message: "No value", details: nil))) + future.result(Result.failure(createFlutterError(code: .readFailed, message: "No value"))) } } return true @@ -484,15 +484,15 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } extension CBPeripheral { - func saveCache(){ - discoveredPeripherals[self.uuid.uuidString] = self - } + func saveCache() { + discoveredPeripherals[uuid.uuidString] = self + } } extension String { func getPeripheral(manager: CBCentralManager) throws -> CBPeripheral { guard let peripheral = findPeripheral(manager: manager) else { - throw PigeonError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil) + throw createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)") } return peripheral } @@ -515,7 +515,7 @@ extension [String] { func toCBUUID() throws -> [CBUUID] { return try compactMap { serviceUUID in guard UUID(uuidString: serviceUUID) != nil else { - throw PigeonError(code: "IllegalArgument", message: "Invalid service UUID:\(serviceUUID)", details: nil) + throw createFlutterError(code: .invalidServiceUuid, message: "Invalid service UUID:\(serviceUUID)") } return CBUUID(string: serviceUUID) } diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index d97f17e..e51a31d 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 3bd6f1b..b349027 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -31,11 +31,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 +PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 428666c..beb7003 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -470,7 +470,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -598,7 +598,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -647,7 +647,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15cada4..e3773d4 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> BleUuidParser.compareStrings(s.uuid, service), - orElse: () => throw ServiceNotFoundException( - 'Service "$service" not available', + orElse: () => throw UniversalBleException( + code: UniversalBleErrorCode.serviceNotFound, + message: 'Service "$service" not available', ), ); } diff --git a/lib/src/extensions/ble_service_extension.dart b/lib/src/extensions/ble_service_extension.dart index ed9ca0a..b1206ec 100644 --- a/lib/src/extensions/ble_service_extension.dart +++ b/lib/src/extensions/ble_service_extension.dart @@ -1,3 +1,4 @@ +import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; import 'package:universal_ble/universal_ble.dart'; /// Extension methods for [BleService] objects. @@ -8,12 +9,16 @@ extension BleServiceExtension on BleService { /// with the given UUID is not available. BleCharacteristic getCharacteristic(String characteristicId) { if (characteristics.isEmpty) { - throw CharacteristicNotFoundException('No characteristics found'); + throw UniversalBleException( + code: UniversalBleErrorCode.characteristicNotFound, + message: 'No characteristics found', + ); } return characteristics.firstWhere( (c) => BleUuidParser.compareStrings(c.uuid, characteristicId), - orElse: () => throw CharacteristicNotFoundException( - 'Characteristic "$characteristicId" not available', + orElse: () => throw UniversalBleException( + code: UniversalBleErrorCode.characteristicNotFound, + message: 'Characteristic "$characteristicId" not available', ), ); } diff --git a/lib/src/universal_ble_exceptions.dart b/lib/src/universal_ble_exceptions.dart index 9a360d7..16c9715 100644 --- a/lib/src/universal_ble_exceptions.dart +++ b/lib/src/universal_ble_exceptions.dart @@ -1,58 +1,85 @@ import 'package:flutter/services.dart'; +import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; +import 'package:universal_ble/src/utils/universal_ble_error_parser.dart'; -class ConnectionException implements Exception { - late String message; +/// Base exception class for all BLE errors with typed error codes +class UniversalBleException implements Exception { + final UniversalBleErrorCode code; + final String message; + final dynamic details; - ConnectionException([dynamic error]) { - message = _errorParser(error); + UniversalBleException({ + required this.code, + required this.message, + this.details, + }); + + @override + String toString() => message; + + factory UniversalBleException.fromError(dynamic error) { + String message = error.toString(); + dynamic details = error; + if (error is PlatformException) { + message = error.message ?? error.details?.toString() ?? error.code; + details = error.details; + } + return UniversalBleException( + code: UniversalBleErrorParser.getCode(error), + message: message, + details: details, + ); } - - @override - String toString() => message; } -class PairingException implements Exception { - late String message; +/// Exception thrown when connection-related errors occur +class ConnectionException extends UniversalBleException { + ConnectionException._({ + required super.code, + required super.message, + super.details, + }); - PairingException([dynamic error]) { - message = _errorParser(error); - } - - @override - String toString() => message; + ConnectionException([dynamic error]) + : this._( + code: UniversalBleErrorParser.getCode(error), + message: _errorParser(error), + details: error, + ); } -class WebBluetoothGloballyDisabled implements Exception { - String message; - WebBluetoothGloballyDisabled(this.message); +/// Exception thrown when pairing-related errors occur +class PairingException extends UniversalBleException { + PairingException._({ + required super.code, + required super.message, + super.details, + }); - @override - String toString() => message; + /// Legacy constructor for backward compatibility + PairingException([dynamic error]) + : this._( + code: UniversalBleErrorParser.getCode(error), + message: _errorParser(error), + details: error, + ); } -class NotFoundException implements Exception {} - -class ServiceNotFoundException implements NotFoundException { - String message; - ServiceNotFoundException(this.message); - - @override - String toString() => message; -} - -class CharacteristicNotFoundException implements NotFoundException { - String message; - CharacteristicNotFoundException(this.message); - - @override - String toString() => message; +/// Exception thrown when Web Bluetooth API is globally disabled +class WebBluetoothGloballyDisabled extends UniversalBleException { + WebBluetoothGloballyDisabled({ + super.code = UniversalBleErrorCode.webBluetoothGloballyDisabled, + required super.message, + super.details, + }); } +/// Legacy error parser for backward compatibility String _errorParser(dynamic error) { if (error == null) { return "Failed"; } else if (error is PlatformException) { - return error.message ?? error.details ?? error.code; + return error.message ?? error.details ?? error.code ?? 'Unknown error'; } else if (error is String) { return error; } else { diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index e204849..ebca5e2 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -3,9 +3,12 @@ import 'dart:async'; import 'package:bluez/bluez.dart'; import 'package:flutter/services.dart'; import 'package:universal_ble/src/models/model_exports.dart'; +import 'package:universal_ble/src/utils/universal_ble_error_parser.dart'; import 'package:universal_ble/src/utils/universal_ble_filter_util.dart'; +import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; import 'package:universal_ble/src/universal_ble_platform_interface.dart'; import 'package:universal_ble/src/utils/universal_logger.dart'; +import 'package:universal_ble/src/universal_ble_exceptions.dart'; class UniversalBleLinux extends UniversalBlePlatform { UniversalBleLinux._(); @@ -180,7 +183,10 @@ class UniversalBleLinux extends UniversalBlePlatform { await Future.delayed(const Duration(seconds: 1)); if (device.gattServices.isEmpty && !device.servicesResolved) { - throw "Failed to resolve services"; + throw UniversalBleException( + code: UniversalBleErrorCode.failed, + message: "Failed to resolve services", + ); } List services = []; @@ -217,7 +223,10 @@ class UniversalBleLinux extends UniversalBlePlatform { orElse: () => null); if (c == null) { - throw Exception('Unknown characteristic:$characteristic'); + throw UniversalBleException( + code: UniversalBleErrorCode.characteristicNotFound, + message: 'Unknown characteristic:$characteristic', + ); } return c; } @@ -279,9 +288,8 @@ class UniversalBleLinux extends UniversalBlePlatform { final data = await c.readValue(); return Uint8List.fromList(data); } on BlueZFailedException catch (e) { - throw PlatformException( - code: e.errorCode ?? "ReadFailed", - message: e.message, + throw e.toUniversalBleException( + defaultCode: UniversalBleErrorCode.readFailed, ); } } @@ -307,9 +315,8 @@ class UniversalBleLinux extends UniversalBlePlatform { ); } } on BlueZFailedException catch (e) { - throw PlatformException( - code: e.errorCode ?? "WriteFailed", - message: e.message, + throw e.toUniversalBleException( + defaultCode: UniversalBleErrorCode.writeFailed, ); } } @@ -317,7 +324,12 @@ class UniversalBleLinux extends UniversalBlePlatform { @override Future requestMtu(String deviceId, int expectedMtu) async { final device = _findDeviceById(deviceId); - if (!device.connected) throw Exception('Device not connected'); + if (!device.connected) { + throw UniversalBleException( + code: UniversalBleErrorCode.deviceDisconnected, + message: 'Device not connected', + ); + } for (BlueZGattService service in device.gattServices) { for (BlueZGattCharacteristic characteristic in service.characteristics) { int? mtu = characteristic.mtu; @@ -325,7 +337,10 @@ class UniversalBleLinux extends UniversalBlePlatform { if (mtu != null) return mtu - 3; } } - throw Exception('MTU not available'); + throw UniversalBleException( + code: UniversalBleErrorCode.operationNotSupported, + message: 'MTU not available', + ); } @override @@ -393,7 +408,10 @@ class UniversalBleLinux extends UniversalBlePlatform { (device) => device?.address == deviceId, orElse: () => null); if (device == null) { - throw Exception('Unknown deviceId:$deviceId'); + throw UniversalBleException( + code: UniversalBleErrorCode.deviceNotFound, + message: 'Unknown deviceId:$deviceId', + ); } return device; } @@ -459,7 +477,10 @@ class UniversalBleLinux extends UniversalBlePlatform { } if (client.adapters.isEmpty) { - throw Exception('Bluetooth adapter unavailable'); + throw UniversalBleException( + code: UniversalBleErrorCode.bluetoothNotAvailable, + message: 'Bluetooth adapter unavailable', + ); } } @@ -584,6 +605,21 @@ extension on BlueZGattCharacteristicFlag { } extension on BlueZFailedException { + UniversalBleException toUniversalBleException({ + required UniversalBleErrorCode defaultCode, + }) { + // Map BlueZ error code to UniversalBleErrorCode + UniversalBleErrorCode code = UniversalBleErrorParser.getCode(errorCode); + if (code == UniversalBleErrorCode.unknownError) { + code = defaultCode; + } + throw UniversalBleException( + code: code, + message: message, + details: errorCode, + ); + } + /// Extract error code from message and parse into decimal /// example: 'Operation failed with ATT error: 0x90' => 144 String? get errorCode { diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index ae3484f..5e451d7 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v26.0.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -15,7 +15,8 @@ PlatformException _createConnectionError(String channelName) { ); } -List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { +List wrapResponse( + {Object? result, PlatformException? error, bool empty = false}) { if (empty) { return []; } @@ -25,6 +26,86 @@ List wrapResponse({Object? result, PlatformException? error, bool empty return [error.code, error.message, error.details]; } +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && + a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + +/// Unified error codes for all platforms +enum UniversalBleErrorCode { + unknownError, + failed, + notSupported, + notImplemented, + channelError, + bluetoothNotAvailable, + bluetoothNotEnabled, + bluetoothNotAllowed, + bluetoothUnauthorized, + deviceDisconnected, + connectionTimeout, + connectionFailed, + connectionRejected, + connectionLimitExceeded, + connectionAlreadyExists, + connectionTerminated, + connectionInProgress, + illegalArgument, + deviceNotFound, + serviceNotFound, + characteristicNotFound, + invalidServiceUuid, + invalidCharacteristicUuid, + invalidOffset, + invalidAttributeLength, + invalidPdu, + invalidHandle, + readFailed, + readNotPermitted, + writeFailed, + writeNotPermitted, + writeRequestBusy, + invalidAction, + operationNotSupported, + operationTimeout, + operationCancelled, + operationInProgress, + characteristicDoesNotSupportRead, + characteristicDoesNotSupportWrite, + characteristicDoesNotSupportWriteWithoutResponse, + characteristicDoesNotSupportNotify, + characteristicDoesNotSupportIndicate, + notPaired, + notPairable, + alreadyPaired, + pairingFailed, + pairingCancelled, + pairingTimeout, + pairingNotAllowed, + authenticationFailure, + insufficientAuthentication, + insufficientAuthorization, + insufficientEncryption, + insufficientKeySize, + protectionLevelNotMet, + accessDenied, + unpairingFailed, + alreadyUnpaired, + scanFailed, + stoppingScanInProgress, + webBluetoothGloballyDisabled, +} + class UniversalBleScanResult { UniversalBleScanResult({ required this.deviceId, @@ -47,7 +128,7 @@ class UniversalBleScanResult { List? services; - Object encode() { + List _toList() { return [ deviceId, name, @@ -58,6 +139,10 @@ class UniversalBleScanResult { ]; } + Object encode() { + return _toList(); + } + static UniversalBleScanResult decode(Object result) { result as List; return UniversalBleScanResult( @@ -65,10 +150,27 @@ class UniversalBleScanResult { name: result[1] as String?, isPaired: result[2] as bool?, rssi: result[3] as int?, - manufacturerDataList: (result[4] as List?)?.cast(), + manufacturerDataList: + (result[4] as List?)?.cast(), services: (result[5] as List?)?.cast(), ); } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! UniversalBleScanResult || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); } class UniversalBleService { @@ -81,20 +183,41 @@ class UniversalBleService { List? characteristics; - Object encode() { + List _toList() { return [ uuid, characteristics, ]; } + Object encode() { + return _toList(); + } + static UniversalBleService decode(Object result) { result as List; return UniversalBleService( uuid: result[0]! as String, - characteristics: (result[1] as List?)?.cast(), + characteristics: + (result[1] as List?)?.cast(), ); } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! UniversalBleService || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); } class UniversalBleCharacteristic { @@ -107,13 +230,17 @@ class UniversalBleCharacteristic { List properties; - Object encode() { + List _toList() { return [ uuid, properties, ]; } + Object encode() { + return _toList(); + } + static UniversalBleCharacteristic decode(Object result) { result as List; return UniversalBleCharacteristic( @@ -121,6 +248,23 @@ class UniversalBleCharacteristic { properties: (result[1] as List?)!.cast(), ); } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! UniversalBleCharacteristic || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); } /// Scan Filters @@ -137,7 +281,7 @@ class UniversalScanFilter { List withManufacturerData; - Object encode() { + List _toList() { return [ withServices, withNamePrefix, @@ -145,14 +289,35 @@ class UniversalScanFilter { ]; } + Object encode() { + return _toList(); + } + static UniversalScanFilter decode(Object result) { result as List; return UniversalScanFilter( withServices: (result[0] as List?)!.cast(), withNamePrefix: (result[1] as List?)!.cast(), - withManufacturerData: (result[2] as List?)!.cast(), + withManufacturerData: (result[2] as List?)! + .cast(), ); } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! UniversalScanFilter || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); } class UniversalManufacturerDataFilter { @@ -168,7 +333,7 @@ class UniversalManufacturerDataFilter { Uint8List? mask; - Object encode() { + List _toList() { return [ companyIdentifier, data, @@ -176,6 +341,10 @@ class UniversalManufacturerDataFilter { ]; } + Object encode() { + return _toList(); + } + static UniversalManufacturerDataFilter decode(Object result) { result as List; return UniversalManufacturerDataFilter( @@ -184,6 +353,23 @@ class UniversalManufacturerDataFilter { mask: result[2] as Uint8List?, ); } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! UniversalManufacturerDataFilter || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); } class UniversalManufacturerData { @@ -196,13 +382,17 @@ class UniversalManufacturerData { Uint8List data; - Object encode() { + List _toList() { return [ companyIdentifier, data, ]; } + Object encode() { + return _toList(); + } + static UniversalManufacturerData decode(Object result) { result as List; return UniversalManufacturerData( @@ -210,8 +400,24 @@ class UniversalManufacturerData { data: result[1]! as Uint8List, ); } -} + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! UniversalManufacturerData || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @@ -220,24 +426,27 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is UniversalBleScanResult) { + } else if (value is UniversalBleErrorCode) { buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else if (value is UniversalBleService) { + writeValue(buffer, value.index); + } else if (value is UniversalBleScanResult) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is UniversalBleCharacteristic) { + } else if (value is UniversalBleService) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is UniversalScanFilter) { + } else if (value is UniversalBleCharacteristic) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is UniversalManufacturerDataFilter) { + } else if (value is UniversalScanFilter) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is UniversalManufacturerData) { + } else if (value is UniversalManufacturerDataFilter) { buffer.putUint8(134); writeValue(buffer, value.encode()); + } else if (value is UniversalManufacturerData) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -246,17 +455,20 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : UniversalBleErrorCode.values[value]; + case 130: return UniversalBleScanResult.decode(readValue(buffer)!); - case 130: + case 131: return UniversalBleService.decode(readValue(buffer)!); - case 131: + case 132: return UniversalBleCharacteristic.decode(readValue(buffer)!); - case 132: + case 133: return UniversalScanFilter.decode(readValue(buffer)!); - case 133: + case 134: return UniversalManufacturerDataFilter.decode(readValue(buffer)!); - case 134: + case 135: return UniversalManufacturerData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -269,9 +481,11 @@ class UniversalBlePlatformChannel { /// 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 = ''}) + UniversalBlePlatformChannel( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -279,14 +493,17 @@ class UniversalBlePlatformChannel { final String pigeonVar_messageChannelSuffix; Future getBluetoothAvailabilityState() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -306,14 +523,17 @@ class UniversalBlePlatformChannel { } Future enableBluetooth() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -333,14 +553,17 @@ class UniversalBlePlatformChannel { } Future disableBluetooth() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disableBluetooth$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -360,14 +583,18 @@ class UniversalBlePlatformChannel { } Future startScan(UniversalScanFilter? filter) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([filter]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([filter]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -382,14 +609,17 @@ class UniversalBlePlatformChannel { } Future stopScan() async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -404,14 +634,18 @@ class UniversalBlePlatformChannel { } Future connect(String deviceId) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([deviceId]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -426,14 +660,18 @@ class UniversalBlePlatformChannel { } Future disconnect(String deviceId) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([deviceId]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -447,15 +685,20 @@ class UniversalBlePlatformChannel { } } - Future setNotifiable(String deviceId, String service, String characteristic, int bleInputProperty) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + Future setNotifiable(String deviceId, String service, + String characteristic, int bleInputProperty) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel + .send([deviceId, service, characteristic, bleInputProperty]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId, service, characteristic, bleInputProperty]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -470,14 +713,18 @@ class UniversalBlePlatformChannel { } Future> discoverServices(String deviceId) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([deviceId]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -492,19 +739,25 @@ class UniversalBlePlatformChannel { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)!.cast(); + return (pigeonVar_replyList[0] as List?)! + .cast(); } } - Future readValue(String deviceId, String service, String characteristic) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + Future readValue( + String deviceId, String service, String characteristic) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([deviceId, service, characteristic]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId, service, characteristic]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -524,14 +777,18 @@ class UniversalBlePlatformChannel { } Future requestMtu(String deviceId, int expectedMtu) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([deviceId, expectedMtu]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId, expectedMtu]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -550,15 +807,20 @@ class UniversalBlePlatformChannel { } } - Future writeValue(String deviceId, String service, String characteristic, Uint8List value, int bleOutputProperty) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + Future writeValue(String deviceId, String service, + String characteristic, Uint8List value, int bleOutputProperty) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [deviceId, service, characteristic, value, bleOutputProperty]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId, service, characteristic, value, bleOutputProperty]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -573,14 +835,18 @@ class UniversalBlePlatformChannel { } Future isPaired(String deviceId) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([deviceId]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -600,14 +866,18 @@ class UniversalBlePlatformChannel { } Future pair(String deviceId) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([deviceId]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -627,14 +897,18 @@ class UniversalBlePlatformChannel { } Future unPair(String deviceId) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([deviceId]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -648,15 +922,20 @@ class UniversalBlePlatformChannel { } } - Future> getSystemDevices(List withServices) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + Future> getSystemDevices( + List withServices) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([withServices]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([withServices]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -671,19 +950,24 @@ class UniversalBlePlatformChannel { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)!.cast(); + return (pigeonVar_replyList[0] as List?)! + .cast(); } } Future getConnectionState(String deviceId) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectionState$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectionState$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([deviceId]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([deviceId]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -713,22 +997,31 @@ abstract class UniversalBleCallbackChannel { void onScanResult(UniversalBleScanResult result); - void onValueChanged(String deviceId, String characteristicId, Uint8List value); + void onValueChanged( + String deviceId, String characteristicId, Uint8List value); void onConnectionChanged(String deviceId, bool connected, String? error); - static void setUp(UniversalBleCallbackChannel? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + UniversalBleCallbackChannel? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged$messageChannelSuffix', pigeonChannelCodec, + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged was null.'); final List args = (message as List?)!; final int? arg_state = (args[0] as int?); assert(arg_state != null, @@ -738,22 +1031,26 @@ abstract class UniversalBleCallbackChannel { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange$messageChannelSuffix', pigeonChannelCodec, + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange was null.'); final List args = (message as List?)!; final String? arg_deviceId = (args[0] as String?); assert(arg_deviceId != null, @@ -767,24 +1064,29 @@ abstract class UniversalBleCallbackChannel { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult$messageChannelSuffix', pigeonChannelCodec, + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult was null.'); final List args = (message as List?)!; - final UniversalBleScanResult? arg_result = (args[0] as UniversalBleScanResult?); + final UniversalBleScanResult? arg_result = + (args[0] as UniversalBleScanResult?); assert(arg_result != null, 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult was null, expected non-null UniversalBleScanResult.'); try { @@ -792,22 +1094,26 @@ abstract class UniversalBleCallbackChannel { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged$messageChannelSuffix', pigeonChannelCodec, + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null.'); final List args = (message as List?)!; final String? arg_deviceId = (args[0] as String?); assert(arg_deviceId != null, @@ -819,26 +1125,31 @@ abstract class UniversalBleCallbackChannel { assert(arg_value != null, 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null, expected non-null Uint8List.'); try { - api.onValueChanged(arg_deviceId!, arg_characteristicId!, arg_value!); + api.onValueChanged( + arg_deviceId!, arg_characteristicId!, arg_value!); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } } { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged$messageChannelSuffix', pigeonChannelCodec, + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged was null.'); + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged was null.'); final List args = (message as List?)!; final String? arg_deviceId = (args[0] as String?); assert(arg_deviceId != null, @@ -852,8 +1163,9 @@ abstract class UniversalBleCallbackChannel { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } diff --git a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart index 6dc1666..0892e20 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -1,5 +1,4 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; import 'package:universal_ble/src/utils/universal_ble_filter_util.dart'; import 'package:universal_ble/universal_ble.dart'; @@ -18,7 +17,9 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { @override Future getBluetoothAvailabilityState() async { - int state = await _channel.getBluetoothAvailabilityState(); + int state = await _executeWithErrorHandling( + () => _channel.getBluetoothAvailabilityState(), + ); return AvailabilityState.parse(state); } @@ -27,7 +28,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { if (!BleCapabilities.supportsBluetoothEnableApi) { throw UnsupportedError("Not supported"); } - return _channel.enableBluetooth(); + return _executeWithErrorHandling(() => _channel.enableBluetooth()); } @override @@ -35,7 +36,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { if (!BleCapabilities.supportsBluetoothEnableApi) { throw UnsupportedError("Not supported"); } - return _channel.disableBluetooth(); + return _executeWithErrorHandling(() => _channel.disableBluetooth()); } @override @@ -45,31 +46,37 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { }) async { await _ensureInitialized(); _bleFilter.scanFilter = scanFilter; - await _channel.startScan( - scanFilter.toUniversalScanFilter(), + await _executeWithErrorHandling( + () => _channel.startScan( + scanFilter.toUniversalScanFilter(), + ), ); } @override - Future stopScan() => _channel.stopScan(); + Future stopScan() => + _executeWithErrorHandling(() => _channel.stopScan()); @override Future getConnectionState(String deviceId) async { - int state = await _channel.getConnectionState(deviceId); + int state = await _executeWithErrorHandling( + () => _channel.getConnectionState(deviceId)); return BleConnectionState.parse(state); } @override Future connect(String deviceId, {Duration? connectionTimeout}) => - _channel.connect(deviceId); + _executeWithErrorHandling(() => _channel.connect(deviceId)); @override - Future disconnect(String deviceId) => _channel.disconnect(deviceId); + Future disconnect(String deviceId) => + _executeWithErrorHandling(() => _channel.disconnect(deviceId)); @override Future> discoverServices(String deviceId) async { List universalBleServices = - await _channel.discoverServices(deviceId); + await _executeWithErrorHandling( + () => _channel.discoverServices(deviceId)); return List.from(universalBleServices .where((e) => e != null) .map((e) => e!.toBleService(deviceId)) @@ -79,12 +86,12 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { @override Future setNotifiable(String deviceId, String service, String characteristic, BleInputProperty bleInputProperty) { - return _channel.setNotifiable( - deviceId, - service, - characteristic, - bleInputProperty.index, - ); + return _executeWithErrorHandling(() => _channel.setNotifiable( + deviceId, + service, + characteristic, + bleInputProperty.index, + )); } @override @@ -94,7 +101,8 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { String characteristic, { final Duration? timeout, }) { - return _channel.readValue(deviceId, service, characteristic); + return _executeWithErrorHandling( + () => _channel.readValue(deviceId, service, characteristic)); } @override @@ -104,33 +112,38 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { String characteristic, Uint8List value, BleOutputProperty bleOutputProperty) { - return _channel.writeValue( - deviceId, - service, - characteristic, - value, - bleOutputProperty.index, - ); + return _executeWithErrorHandling(() => _channel.writeValue( + deviceId, + service, + characteristic, + value, + bleOutputProperty.index, + )); } @override Future requestMtu(String deviceId, int expectedMtu) => - _channel.requestMtu(deviceId, expectedMtu); + _executeWithErrorHandling( + () => _channel.requestMtu(deviceId, expectedMtu)); @override - Future isPaired(String deviceId) => _channel.isPaired(deviceId); + Future isPaired(String deviceId) => + _executeWithErrorHandling(() => _channel.isPaired(deviceId)); @override - Future pair(String deviceId) => _channel.pair(deviceId); + Future pair(String deviceId) => + _executeWithErrorHandling(() => _channel.pair(deviceId)); @override - Future unpair(String deviceId) => _channel.unPair(deviceId); + Future unpair(String deviceId) => + _executeWithErrorHandling(() => _channel.unPair(deviceId)); @override Future> getSystemDevices( List? withServices, ) async { - var devices = await _channel.getSystemDevices(withServices ?? []); + var devices = await _executeWithErrorHandling( + () => _channel.getSystemDevices(withServices ?? [])); return List.from( devices.map((e) => e.toBleDevice(isSystemDevice: true)).toList(), ); @@ -152,6 +165,16 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { )); } + /// Executes a platform call with error handling + /// Converts any errors to UniversalBleException + Future _executeWithErrorHandling(Future Function() future) async { + try { + return await future(); + } catch (error) { + throw UniversalBleException.fromError(error); + } + } + Future _ensureInitialized() async { // Check bluetooth availability on Apple // so that it will ask permission only when required, and throw error on failed @@ -160,13 +183,13 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { AvailabilityState state = await getBluetoothAvailabilityState(); switch (state) { case AvailabilityState.unauthorized: - throw PlatformException( - code: "Unauthorized", + throw UniversalBleException( + code: UniversalBleErrorCode.bluetoothUnauthorized, message: "Not authorized to access Bluetooth", ); case AvailabilityState.unsupported: - throw PlatformException( - code: "Unsupported", + throw UniversalBleException( + code: UniversalBleErrorCode.notSupported, message: "Bluetooth is not supported", ); default: diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 4d02037..0fa55f2 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -4,6 +4,7 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart'; import 'package:universal_ble/src/utils/universal_logger.dart'; +import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; import 'package:universal_ble/universal_ble.dart'; class UniversalBleWeb extends UniversalBlePlatform { @@ -35,7 +36,12 @@ class UniversalBleWeb extends UniversalBlePlatform { Duration? connectionTimeout = const Duration(seconds: 10), }) async { var device = _getDeviceById(deviceId); - if (device == null) throw "$deviceId Not Found"; + if (device == null) { + throw UniversalBleException( + code: UniversalBleErrorCode.deviceNotFound, + message: "$deviceId Not Found", + ); + } await device.connect(timeout: connectionTimeout); // Subscribe to Connection Stream @@ -92,7 +98,7 @@ class UniversalBleWeb extends UniversalBlePlatform { } catch (e) { String error = e.toString().replaceAll("DeviceNotFoundError:", "").trim(); if (error.toLowerCase().contains("api globally disabled")) { - throw WebBluetoothGloballyDisabled(error); + throw WebBluetoothGloballyDisabled(message: error); } rethrow; } @@ -154,8 +160,10 @@ class UniversalBleWeb extends UniversalBlePlatform { ); if (bleCharacteristic == null) { - throw Exception( - 'Characteristic $characteristic for service $service not found', + throw UniversalBleException( + code: UniversalBleErrorCode.characteristicNotFound, + message: + 'Characteristic $characteristic for service $service not found', ); } @@ -195,8 +203,10 @@ class UniversalBleWeb extends UniversalBlePlatform { ); if (bleCharacteristic == null) { - throw Exception( - 'Characteristic $characteristic for service $service not found', + throw UniversalBleException( + code: UniversalBleErrorCode.characteristicNotFound, + message: + 'Characteristic $characteristic for service $service not found', ); } @@ -221,8 +231,11 @@ class UniversalBleWeb extends UniversalBlePlatform { characteristicId: characteristic, ); if (bleCharacteristic == null) { - throw Exception( - 'Characteristic $characteristic for service $service not found'); + throw UniversalBleException( + code: UniversalBleErrorCode.characteristicNotFound, + message: + 'Characteristic $characteristic for service $service not found', + ); } var data = timeout != null ? bleCharacteristic.readValue(timeout: timeout) @@ -233,29 +246,44 @@ class UniversalBleWeb extends UniversalBlePlatform { /// `Unimplemented` @override Future requestMtu(String deviceId, int expectedMtu) { - throw UnimplementedError(); + throw UniversalBleException( + code: UniversalBleErrorCode.notImplemented, + message: "requestMtu is not implemented on Web platform", + ); } @override Future isPaired(String deviceId) { - throw UnimplementedError(); + throw UniversalBleException( + code: UniversalBleErrorCode.notImplemented, + message: "isPaired is not implemented on Web platform", + ); } @override Future pair(String deviceId) { - throw UnimplementedError(); + throw UniversalBleException( + code: UniversalBleErrorCode.notImplemented, + message: "pair is not implemented on Web platform", + ); } @override Future unpair(String deviceId) { - throw UnimplementedError(); + throw UniversalBleException( + code: UniversalBleErrorCode.notImplemented, + message: "unpair is not implemented on Web platform", + ); } @override Future> getSystemDevices( List? withServices, ) { - throw UnimplementedError(); + throw UniversalBleException( + code: UniversalBleErrorCode.notImplemented, + message: "getSystemDevices is not implemented on Web platform", + ); } /// Helpers @@ -335,12 +363,18 @@ class UniversalBleWeb extends UniversalBlePlatform { @override Future enableBluetooth() { - throw UnimplementedError(); + throw UniversalBleException( + code: UniversalBleErrorCode.notImplemented, + message: "enableBluetooth is not implemented on Web platform", + ); } @override Future disableBluetooth() { - throw UnimplementedError(); + throw UniversalBleException( + code: UniversalBleErrorCode.notImplemented, + message: "disableBluetooth is not implemented on Web platform", + ); } RequestOptionsBuilder _getRequestOptionBuilder( diff --git a/lib/src/utils/universal_ble_error_parser.dart b/lib/src/utils/universal_ble_error_parser.dart new file mode 100644 index 0000000..76064d9 --- /dev/null +++ b/lib/src/utils/universal_ble_error_parser.dart @@ -0,0 +1,417 @@ +import 'package:flutter/services.dart'; +import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; + +/// Utility class to parse error codes from dynamic errors +class UniversalBleErrorParser { + static UniversalBleErrorCode getCode(dynamic error) { + if (error is UniversalBleErrorCode) return error; + + if (error is PlatformException) { + int? errorCodeInt = int.tryParse((error).code); + if (errorCodeInt != null) { + return UniversalBleErrorCode.values[errorCodeInt]; + } + } + + if (error is num) { + return _parseNumericErrorCode((error).toInt()); + } + + if (error is String) { + int? errorCodeInt = int.tryParse(error); + if (errorCodeInt != null) { + return _parseNumericErrorCode(errorCodeInt); + } else { + return _parseStringErrorCode(error) ?? + UniversalBleErrorCode.unknownError; + } + } + + return UniversalBleErrorCode.unknownError; + } + + static UniversalBleErrorCode? _parseStringErrorCode(String code) { + switch (code.toLowerCase()) { + case 'notsupported': + case 'not_supported': + return UniversalBleErrorCode.notSupported; + case 'notimplemented': + case 'not_implemented': + return UniversalBleErrorCode.notImplemented; + case 'channel-error': + case 'channelerror': + return UniversalBleErrorCode.channelError; + case 'failed': + return UniversalBleErrorCode.failed; + case 'bluetoothnotavailable': + case 'bluetooth_not_available': + return UniversalBleErrorCode.bluetoothNotAvailable; + case 'bluetoothnotenabled': + case 'bluetooth_not_enabled': + return UniversalBleErrorCode.bluetoothNotEnabled; + case 'bluetoothnotallowed': + case 'bluetooth_not_allowed': + return UniversalBleErrorCode.bluetoothNotAllowed; + case 'bluetoothunauthorized': + case 'bluetooth_unauthorized': + return UniversalBleErrorCode.bluetoothUnauthorized; + case 'devicedisconnected': + case 'device_disconnected': + return UniversalBleErrorCode.deviceDisconnected; + case 'connectiontimeout': + case 'connection_timeout': + return UniversalBleErrorCode.connectionTimeout; + case 'connectionfailed': + case 'connection_failed': + return UniversalBleErrorCode.connectionFailed; + case 'connectionrejected': + case 'connection_rejected': + return UniversalBleErrorCode.connectionRejected; + case 'connecting': + case 'connectioninprogress': + case 'connection_in_progress': + return UniversalBleErrorCode.connectionInProgress; + case 'connectionterminated': + case 'connection_terminated': + return UniversalBleErrorCode.connectionTerminated; + case 'illegalargument': + case 'illegal_argument': + return UniversalBleErrorCode.illegalArgument; + case 'devicenotfound': + case 'device_not_found': + return UniversalBleErrorCode.deviceNotFound; + case 'servicenotfound': + case 'service_not_found': + return UniversalBleErrorCode.serviceNotFound; + case 'characteristicnotfound': + case 'characteristic_not_found': + return UniversalBleErrorCode.characteristicNotFound; + case 'invalidserviceuuid': + case 'invalid_service_uuid': + return UniversalBleErrorCode.invalidServiceUuid; + case 'invalidcharacteristicuuid': + case 'invalid_characteristic_uuid': + return UniversalBleErrorCode.invalidCharacteristicUuid; + case 'readfailed': + case 'read_failed': + return UniversalBleErrorCode.readFailed; + case 'writefailed': + case 'write_failed': + case 'writeerror': + case 'write_error': + return UniversalBleErrorCode.writeFailed; + case 'invalidaction': + case 'invalid_action': + return UniversalBleErrorCode.invalidAction; + case 'operationnotsupported': + case 'operation_not_supported': + return UniversalBleErrorCode.operationNotSupported; + case 'operationtimeout': + case 'operation_timeout': + return UniversalBleErrorCode.operationTimeout; + case 'operationcancelled': + case 'operation_cancelled': + return UniversalBleErrorCode.operationCancelled; + case 'inprogress': + case 'alreadyinprogress': + case 'already_in_progress': + return UniversalBleErrorCode.operationInProgress; + case 'notpaired': + case 'not_paired': + return UniversalBleErrorCode.notPaired; + case 'notpairable': + case 'not_pairable': + return UniversalBleErrorCode.notPairable; + case 'alreadypaired': + case 'already_paired': + return UniversalBleErrorCode.alreadyPaired; + case 'pairingfailed': + case 'pairing_failed': + return UniversalBleErrorCode.pairingFailed; + case 'pairingcancelled': + case 'pairing_cancelled': + return UniversalBleErrorCode.pairingCancelled; + case 'pairingtimeout': + case 'pairing_timeout': + return UniversalBleErrorCode.pairingTimeout; + case 'pairingnotallowed': + case 'pairing_not_allowed': + return UniversalBleErrorCode.pairingNotAllowed; + case 'authenticationfailure': + case 'authentication_failure': + return UniversalBleErrorCode.authenticationFailure; + case 'authenticationtimeout': + case 'authentication_timeout': + case 'authenticationnotallowed': + case 'authentication_not_allowed': + return UniversalBleErrorCode.authenticationFailure; + case 'hardwarefailure': + case 'hardware_failure': + case 'toomanyconnections': + case 'too_many_connections': + case 'notreadytopair': + case 'not_ready_to_pair': + case 'nonsupportedprofiles': + case 'no_supported_profiles': + case 'invalidceremonydata': + case 'invalid_ceremony_data': + case 'requiredhandlernotregistered': + case 'required_handler_not_registered': + case 'rejectedbyhandler': + case 'rejected_by_handler': + case 'remotedevicehasassociation': + case 'remote_device_has_association': + return UniversalBleErrorCode.pairingFailed; + case 'protectionlevelcouldnotbemet': + case 'protection_level_could_not_be_met': + return UniversalBleErrorCode.protectionLevelNotMet; + case 'unpairingfailed': + case 'unpairing_failed': + return UniversalBleErrorCode.unpairingFailed; + case 'alreadyunpaired': + case 'already_unpaired': + return UniversalBleErrorCode.alreadyUnpaired; + case 'accessdenied': + case 'access_denied': + return UniversalBleErrorCode.accessDenied; + case 'scan_failed_already_started': + case 'scanfailedalreadystarted': + case 'scan_failed_application_registration_failed': + case 'scanfailedapplicationregistrationfailed': + case 'scan_failed_feature_unsupported': + case 'scanfailedfeatureunsupported': + case 'scan_failed_internal_error': + case 'scanfailedinternalerror': + case 'scan_failed_out_of_hardware_resources': + case 'scanfailedoutofhardwareresources': + case 'scan_failed_scanning_too_frequently': + case 'scanfailedscanningtoofrequently': + return UniversalBleErrorCode.scanFailed; + case 'stoppingscaninprogress': + case 'stopping_scan_in_progress': + return UniversalBleErrorCode.stoppingScanInProgress; + case 'gatt_failure': + case 'gattfailure': + case 'unreachable': + case 'protocolerror': + case 'protocol_error': + case 'gattunreachable': + case 'gatt_protocol_error': + case 'gatt_unlikely': + case 'gattunlikely': + case 'gatt_insufficient_resources': + case 'gattinsufficientresources': + return UniversalBleErrorCode.failed; + case 'gatt_read_not_permitted': + case 'gattreadnotpermitted': + return UniversalBleErrorCode.readNotPermitted; + case 'gatt_write_not_permitted': + case 'gattwritenotpermitted': + return UniversalBleErrorCode.writeNotPermitted; + case 'gatt_insufficient_authentication': + case 'gattinsufficientauthentication': + return UniversalBleErrorCode.insufficientAuthentication; + case 'gatt_insufficient_authorization': + case 'gattinsufficientauthorization': + return UniversalBleErrorCode.insufficientAuthorization; + case 'gatt_insufficient_encryption': + case 'gattinsufficientencryption': + return UniversalBleErrorCode.insufficientEncryption; + case 'gatt_insufficient_key_size': + case 'gattinsufficientkeysize': + return UniversalBleErrorCode.insufficientKeySize; + case 'gatt_request_not_supported': + case 'gattrequestnotsupported': + return UniversalBleErrorCode.operationNotSupported; + case 'gatt_invalid_offset': + case 'gattinvalidoffset': + return UniversalBleErrorCode.invalidOffset; + case 'gatt_invalid_attribute_length': + case 'gattinvalidattributelength': + return UniversalBleErrorCode.invalidAttributeLength; + case 'gatt_connection_congested': + case 'gattconnectioncongested': + return UniversalBleErrorCode.connectionFailed; + case 'gatt_invalid_handle': + case 'gattinvalidhandle': + return UniversalBleErrorCode.invalidHandle; + case 'gatt_invalid_pdu': + case 'gattinvalidpdu': + return UniversalBleErrorCode.invalidPdu; + case 'gatt_prepare_queue_full': + case 'gattpreparequeuefull': + return UniversalBleErrorCode.operationInProgress; + case 'gatt_attr_not_found': + case 'gattattrnotfound': + return UniversalBleErrorCode.serviceNotFound; + case 'gatt_attr_not_long': + case 'gattattrnotlong': + return UniversalBleErrorCode.invalidAttributeLength; + case 'gatt_unsupported_group': + case 'gattunsupportedgroup': + return UniversalBleErrorCode.operationNotSupported; + case 'feature_not_configured': + case 'featurenotconfigured': + case 'feature_not_supported': + case 'featurenotsupported': + case 'feature_supported': + case 'featuresupported': + return UniversalBleErrorCode.notSupported; + case 'error_profile_service_not_bound': + case 'errorprofileservicenotbound': + return UniversalBleErrorCode.failed; + case 'error_missing_bluetooth_connect_permission': + case 'errormissingbluetoothconnectpermission': + return UniversalBleErrorCode.bluetoothNotAllowed; + case 'error_device_not_bonded': + case 'errordevicenotbonded': + return UniversalBleErrorCode.notPaired; + case 'error_gatt_write_not_allowed': + case 'errorgattwritenotallowed': + return UniversalBleErrorCode.writeNotPermitted; + case 'error_gatt_write_request_busy': + case 'errorgattwriterequestbusy': + return UniversalBleErrorCode.writeRequestBusy; + case 'error_unknown': + case 'errorunknown': + return UniversalBleErrorCode.unknownError; + case 'webbluetoothgloballydisabled': + case 'web_bluetooth_globally_disabled': + return UniversalBleErrorCode.webBluetoothGloballyDisabled; + default: + return null; + } + } + + static UniversalBleErrorCode _parseNumericErrorCode(int code) { + switch (code) { + case 0x00: + return UniversalBleErrorCode.unknownError; + case 0x01: + return UniversalBleErrorCode.invalidHandle; + case 0x02: + return UniversalBleErrorCode.readNotPermitted; + case 0x03: + return UniversalBleErrorCode.writeNotPermitted; + case 0x04: + return UniversalBleErrorCode.invalidPdu; + case 0x05: + return UniversalBleErrorCode.insufficientAuthentication; + // Consolidated: gattRequestNotSupported -> operationNotSupported + case 0x06: + return UniversalBleErrorCode.operationNotSupported; + case 0x07: + return UniversalBleErrorCode.invalidOffset; + case 0x08: + return UniversalBleErrorCode.insufficientAuthorization; + // Consolidated: gattPrepareQueueFull -> operationInProgress + case 0x09: + return UniversalBleErrorCode.operationInProgress; + // Consolidated: gattAttrNotFound -> serviceNotFound + case 0x0A: + return UniversalBleErrorCode.serviceNotFound; + // Consolidated: gattAttrNotLong -> invalidAttributeLength + case 0x0B: + return UniversalBleErrorCode.invalidAttributeLength; + case 0x0C: + return UniversalBleErrorCode.insufficientKeySize; + case 0x0D: + return UniversalBleErrorCode.invalidAttributeLength; + // Consolidated: gattUnlikely -> failed + case 0x0E: + return UniversalBleErrorCode.failed; + case 0x0F: + return UniversalBleErrorCode.insufficientEncryption; + // Consolidated: gattUnsupportedGroup -> operationNotSupported + case 0x10: + return UniversalBleErrorCode.operationNotSupported; + // Consolidated: gattInsufficientResources -> failed + case 0x11: + return UniversalBleErrorCode.failed; + // Consolidated: gattConnectionCongested -> connectionFailed + case 0x85: + return UniversalBleErrorCode.connectionFailed; + // Consolidated: gattFailure -> failed + case 0x101: + return UniversalBleErrorCode.failed; + } + // HCI error codes - consolidated to higher-level errors + switch (code) { + // Connection errors + case 0x08: // Connection Timeout + case 0x10: // Connection Accept Timeout Exceeded + return UniversalBleErrorCode.connectionTimeout; + case 0x09: // Connection Limit Exceeded + case 0x0A: // Synchronous Connection Limit To A Device Exceeded + return UniversalBleErrorCode.connectionLimitExceeded; + case 0x0B: // Connection Already Exists + return UniversalBleErrorCode.connectionAlreadyExists; + case 0x0D: // Connection Rejected due to Limited Resources + case 0x0F: // Connection Rejected due to Unacceptable BD_ADDR + case 0x39: // Connection Rejected due to No Suitable Channel Found + return UniversalBleErrorCode.connectionRejected; + case 0x0E: // Connection Rejected Due To Security Reasons + return UniversalBleErrorCode.connectionRejected; + case 0x13: // Remote User Terminated Connection + case 0x14: // Remote Device Terminated Connection due to Low Resources + case 0x15: // Remote Device Terminated Connection due to Power Off + case 0x16: // Connection Terminated By Local Host + case 0x3D: // Connection Terminated due to MIC Failure + return UniversalBleErrorCode.connectionTerminated; + case 0x3E: // Connection Failed to be Established + case 0x3F: // MAC Connection Failed + return UniversalBleErrorCode.connectionFailed; + // Pairing/Authentication errors + case 0x05: // Authentication Failure + return UniversalBleErrorCode.authenticationFailure; + case 0x18: // Pairing Not Allowed + return UniversalBleErrorCode.pairingNotAllowed; + case 0x03: // Hardware Failure + case 0x29: // Pairing With Unit Key Not Supported + case 0x37: // Secure Simple Pairing Not Supported By Host + case 0x38: // Host Busy - Pairing + return UniversalBleErrorCode.pairingFailed; + case 0x2F: // Insufficient Security + return UniversalBleErrorCode.insufficientEncryption; + // Operation errors + case 0x0C: // Command Disallowed + case 0x11: // Unsupported Feature or Parameter Value + case 0x12: // Invalid HCI Command Parameters + case 0x1A: // Unsupported Remote Feature + case 0x1E: // Invalid LMP Parameters + case 0x20: // Unsupported LMP Parameter Value + return UniversalBleErrorCode.operationNotSupported; + case 0x22: // LMP Response Timeout + return UniversalBleErrorCode.operationTimeout; + // General errors + case 0x01: // Unknown HCI Command + case 0x02: // Unknown Connection Identifier + return UniversalBleErrorCode.unknownError; + case 0x04: // Page Timeout + return UniversalBleErrorCode.connectionTimeout; + case 0x06: // PIN or Key Missing + return UniversalBleErrorCode.authenticationFailure; + case 0x07: // Memory Capacity Exceeded + return UniversalBleErrorCode.failed; + case 0x17: // Repeated Attempts + return UniversalBleErrorCode.failed; + case 0x25: // Encryption Mode Not Acceptable + return UniversalBleErrorCode.insufficientEncryption; + case 0x26: // Link Key cannot be Changed + case 0x27: // Requested QoS Not Supported + case 0x2C: // QoS Unacceptable Parameter + case 0x2D: // QoS Rejected + case 0x2E: // Channel Classification Not Supported + case 0x30: // Parameter Out Of Mandatory Range + case 0x32: // Role Switch Pending + case 0x35: // Role Switch Failed + case 0x36: // Extended Inquiry Response Too Large + case 0x3A: // Controller Busy + case 0x3B: // Unacceptable Connection Parameters + case 0x3C: // Advertising Timeout + return UniversalBleErrorCode.failed; + } + // Default to unknown error for unmapped codes + return UniversalBleErrorCode.unknownError; + } +} diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index f6f8d8b..b52b8ee 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -169,3 +169,87 @@ class UniversalManufacturerData { required this.data, }); } + +/// Unified error codes for all platforms +enum UniversalBleErrorCode { + // General errors + unknownError, + failed, + notSupported, + notImplemented, + channelError, + + // Bluetooth availability errors + bluetoothNotAvailable, + bluetoothNotEnabled, + bluetoothNotAllowed, + bluetoothUnauthorized, + + // Connection errors + deviceDisconnected, + connectionTimeout, + connectionFailed, + connectionRejected, + connectionLimitExceeded, + connectionAlreadyExists, + connectionTerminated, + connectionInProgress, + + // Device/Service/Characteristic errors + illegalArgument, + deviceNotFound, + serviceNotFound, + characteristicNotFound, + invalidServiceUuid, + invalidCharacteristicUuid, + invalidOffset, + invalidAttributeLength, + invalidPdu, + invalidHandle, + + // Operation errors + readFailed, + readNotPermitted, + writeFailed, + writeNotPermitted, + writeRequestBusy, + invalidAction, + operationNotSupported, + operationTimeout, + operationCancelled, + operationInProgress, + + // Characteristic property errors + characteristicDoesNotSupportRead, + characteristicDoesNotSupportWrite, + characteristicDoesNotSupportWriteWithoutResponse, + characteristicDoesNotSupportNotify, + characteristicDoesNotSupportIndicate, + + // Pairing errors + notPaired, + notPairable, + alreadyPaired, + pairingFailed, + pairingCancelled, + pairingTimeout, + pairingNotAllowed, + authenticationFailure, + insufficientAuthentication, + insufficientAuthorization, + insufficientEncryption, + insufficientKeySize, + protectionLevelNotMet, + accessDenied, + + // Unpairing errors + unpairingFailed, + alreadyUnpaired, + + // Scan errors + scanFailed, + stoppingScanInProgress, + + // Web-specific errors + webBluetoothGloballyDisabled, +} diff --git a/pubspec.yaml b/pubspec.yaml index 40c08a4..5b25216 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: universal_ble description: A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE) plugin for Flutter -version: 0.21.1 +version: 1.0.0 homepage: https://navideck.com repository: https://github.com/Navideck/universal_ble issue_tracker: https://github.com/Navideck/universal_ble/issues @@ -38,7 +38,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 - pigeon: ^22.4.0 + pigeon: ^26.0.5 flutter: plugin: diff --git a/windows/src/enum_parser.h b/windows/src/enum_parser.h index eb7899a..6fc83c4 100644 --- a/windows/src/enum_parser.h +++ b/windows/src/enum_parser.h @@ -1,6 +1,10 @@ #pragma once #include +#include +#include "helper/universal_enum.h" +#include "helper/universal_ble_base.h" +#include "generated/universal_ble.g.h" namespace universal_ble { @@ -46,9 +50,9 @@ namespace universal_ble return std::nullopt; } - inline std::optional parse_pairing_fail_error(const DevicePairingResult& result) + inline std::optional device_pairing_result_to_string(const DevicePairingResultStatus result) { - switch (result.Status()) + switch (result) { case DevicePairingResultStatus::Paired: return std::nullopt; case DevicePairingResultStatus::AlreadyPaired: return "AlreadyPaired"; diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 46ed503..00e3cbf 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v26.0.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #undef _HAS_EXCEPTIONS @@ -425,21 +425,26 @@ EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( flutter::ByteStreamReader* stream) const { switch (type) { case 129: { - return CustomEncodableValue(UniversalBleScanResult::FromEncodableList(std::get(ReadValue(stream)))); + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); } case 130: { - return CustomEncodableValue(UniversalBleService::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleScanResult::FromEncodableList(std::get(ReadValue(stream)))); } case 131: { - return CustomEncodableValue(UniversalBleCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleService::FromEncodableList(std::get(ReadValue(stream)))); } case 132: { - return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); } case 133: { - return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); } case 134: { + return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + } + case 135: { return CustomEncodableValue(UniversalManufacturerData::FromEncodableList(std::get(ReadValue(stream)))); } default: @@ -451,33 +456,38 @@ void PigeonInternalCodecSerializer::WriteValue( const EncodableValue& value, flutter::ByteStreamWriter* stream) const { if (const CustomEncodableValue* custom_value = std::get_if(&value)) { - if (custom_value->type() == typeid(UniversalBleScanResult)) { + if (custom_value->type() == typeid(UniversalBleErrorCode)) { stream->WriteByte(129); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(UniversalBleScanResult)) { + stream->WriteByte(130); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalBleService)) { - stream->WriteByte(130); + stream->WriteByte(131); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalBleCharacteristic)) { - stream->WriteByte(131); + stream->WriteByte(132); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalScanFilter)) { - stream->WriteByte(132); + stream->WriteByte(133); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalManufacturerDataFilter)) { - stream->WriteByte(133); + stream->WriteByte(134); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalManufacturerData)) { - stream->WriteByte(134); + stream->WriteByte(135); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index 2a22a7c..bc77688 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.6.1), do not edit directly. +// Autogenerated from Pigeon (v26.0.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #ifndef PIGEON_UNIVERSAL_BLE_G_H_ @@ -57,6 +57,71 @@ template class ErrorOr { }; +// Unified error codes for all platforms +enum class UniversalBleErrorCode { + kUnknownError = 0, + kFailed = 1, + kNotSupported = 2, + kNotImplemented = 3, + kChannelError = 4, + kBluetoothNotAvailable = 5, + kBluetoothNotEnabled = 6, + kBluetoothNotAllowed = 7, + kBluetoothUnauthorized = 8, + kDeviceDisconnected = 9, + kConnectionTimeout = 10, + kConnectionFailed = 11, + kConnectionRejected = 12, + kConnectionLimitExceeded = 13, + kConnectionAlreadyExists = 14, + kConnectionTerminated = 15, + kConnectionInProgress = 16, + kIllegalArgument = 17, + kDeviceNotFound = 18, + kServiceNotFound = 19, + kCharacteristicNotFound = 20, + kInvalidServiceUuid = 21, + kInvalidCharacteristicUuid = 22, + kInvalidOffset = 23, + kInvalidAttributeLength = 24, + kInvalidPdu = 25, + kInvalidHandle = 26, + kReadFailed = 27, + kReadNotPermitted = 28, + kWriteFailed = 29, + kWriteNotPermitted = 30, + kWriteRequestBusy = 31, + kInvalidAction = 32, + kOperationNotSupported = 33, + kOperationTimeout = 34, + kOperationCancelled = 35, + kOperationInProgress = 36, + kCharacteristicDoesNotSupportRead = 37, + kCharacteristicDoesNotSupportWrite = 38, + kCharacteristicDoesNotSupportWriteWithoutResponse = 39, + kCharacteristicDoesNotSupportNotify = 40, + kCharacteristicDoesNotSupportIndicate = 41, + kNotPaired = 42, + kNotPairable = 43, + kAlreadyPaired = 44, + kPairingFailed = 45, + kPairingCancelled = 46, + kPairingTimeout = 47, + kPairingNotAllowed = 48, + kAuthenticationFailure = 49, + kInsufficientAuthentication = 50, + kInsufficientAuthorization = 51, + kInsufficientEncryption = 52, + kInsufficientKeySize = 53, + kProtectionLevelNotMet = 54, + kAccessDenied = 55, + kUnpairingFailed = 56, + kAlreadyUnpaired = 57, + kScanFailed = 58, + kStoppingScanInProgress = 59, + kWebBluetoothGloballyDisabled = 60 +}; + // Generated class from Pigeon that represents data sent in messages. class UniversalBleScanResult { @@ -96,7 +161,6 @@ class UniversalBleScanResult { void set_services(const flutter::EncodableList* value_arg); void set_services(const flutter::EncodableList& value_arg); - private: static UniversalBleScanResult FromEncodableList(const flutter::EncodableList& list); flutter::EncodableList ToEncodableList() const; @@ -109,7 +173,6 @@ class UniversalBleScanResult { std::optional rssi_; std::optional manufacturer_data_list_; std::optional services_; - }; @@ -131,7 +194,6 @@ class UniversalBleService { void set_characteristics(const flutter::EncodableList* value_arg); void set_characteristics(const flutter::EncodableList& value_arg); - private: static UniversalBleService FromEncodableList(const flutter::EncodableList& list); flutter::EncodableList ToEncodableList() const; @@ -140,7 +202,6 @@ class UniversalBleService { friend class PigeonInternalCodecSerializer; std::string uuid_; std::optional characteristics_; - }; @@ -158,7 +219,6 @@ class UniversalBleCharacteristic { const flutter::EncodableList& properties() const; void set_properties(const flutter::EncodableList& value_arg); - private: static UniversalBleCharacteristic FromEncodableList(const flutter::EncodableList& list); flutter::EncodableList ToEncodableList() const; @@ -167,7 +227,6 @@ class UniversalBleCharacteristic { friend class PigeonInternalCodecSerializer; std::string uuid_; flutter::EncodableList properties_; - }; @@ -191,7 +250,6 @@ class UniversalScanFilter { const flutter::EncodableList& with_manufacturer_data() const; void set_with_manufacturer_data(const flutter::EncodableList& value_arg); - private: static UniversalScanFilter FromEncodableList(const flutter::EncodableList& list); flutter::EncodableList ToEncodableList() const; @@ -201,7 +259,6 @@ class UniversalScanFilter { flutter::EncodableList with_services_; flutter::EncodableList with_name_prefix_; flutter::EncodableList with_manufacturer_data_; - }; @@ -228,7 +285,6 @@ class UniversalManufacturerDataFilter { void set_mask(const std::vector* value_arg); void set_mask(const std::vector& value_arg); - private: static UniversalManufacturerDataFilter FromEncodableList(const flutter::EncodableList& list); flutter::EncodableList ToEncodableList() const; @@ -238,7 +294,6 @@ class UniversalManufacturerDataFilter { int64_t company_identifier_; std::optional> data_; std::optional> mask_; - }; @@ -256,7 +311,6 @@ class UniversalManufacturerData { const std::vector& data() const; void set_data(const std::vector& value_arg); - private: static UniversalManufacturerData FromEncodableList(const flutter::EncodableList& list); flutter::EncodableList ToEncodableList() const; @@ -265,7 +319,6 @@ class UniversalManufacturerData { friend class PigeonInternalCodecSerializer; int64_t company_identifier_; std::vector data_; - }; @@ -280,12 +333,10 @@ class PigeonInternalCodecSerializer : public flutter::StandardCodecSerializer { void WriteValue( const flutter::EncodableValue& value, flutter::ByteStreamWriter* stream) const override; - protected: flutter::EncodableValue ReadValueOfType( uint8_t type, flutter::ByteStreamReader* stream) const override; - }; // Flutter -> Native @@ -352,10 +403,8 @@ class UniversalBlePlatformChannel { const std::string& message_channel_suffix); static flutter::EncodableValue WrapError(std::string_view error_message); static flutter::EncodableValue WrapError(const FlutterError& error); - protected: UniversalBlePlatformChannel() = default; - }; // Native -> Flutter // @@ -393,7 +442,6 @@ class UniversalBleCallbackChannel { const std::string* error, std::function&& on_success, std::function&& on_error); - private: flutter::BinaryMessenger* binary_messenger_; std::string message_channel_suffix_; diff --git a/windows/src/helper/utils.cpp b/windows/src/helper/utils.cpp index a34f8e1..f4decac 100644 --- a/windows/src/helper/utils.cpp +++ b/windows/src/helper/utils.cpp @@ -1,8 +1,11 @@ -#include "Utils.h" +#include "utils.h" +#include "../generated/universal_ble.g.h" +#include "../enum_parser.h" #include #include #include +#include #include #include #include @@ -158,4 +161,142 @@ namespace universal_ble return rove.dwMajorVersion == 10 && rove.dwBuildNumber >= 22000; } + FlutterError create_flutter_error( + UniversalBleErrorCode code, + const std::string& message, + const std::string& details + ) + { + // Pass the enum's underlying integer value as string in code, and enum name or details in details + std::string code_str = std::to_string(static_cast(code)); + std::string details_str = details.empty() ? std::to_string(static_cast(code)) : details; + return FlutterError(code_str, message, details_str); + } + + FlutterError create_flutter_error_from_gatt_communication_status( + GattCommunicationStatus status, + const std::string& message + ) + { + if (status == GattCommunicationStatus::Success) { + // Success case - shouldn't normally create an error, but if called, return unknown + return create_flutter_error(UniversalBleErrorCode::kUnknownError, message); + } + + UniversalBleErrorCode error_code; + switch (status) + { + case GattCommunicationStatus::Unreachable: + case GattCommunicationStatus::ProtocolError: + case GattCommunicationStatus::AccessDenied: + default: + error_code = UniversalBleErrorCode::kFailed; + break; + } + + auto error_message = gatt_communication_status_to_error(status); + std::string final_message = message.empty() && error_message.has_value() + ? error_message.value() + : message; + return create_flutter_error(error_code, final_message); + } + + FlutterError create_flutter_error_from_pairing_status( + DevicePairingResultStatus status, + const std::string& message + ) + { + if (status == DevicePairingResultStatus::Paired) { + // Success case - shouldn't normally create an error, but if called, return unknown + return create_flutter_error(UniversalBleErrorCode::kUnknownError, message); + } + + UniversalBleErrorCode error_code; + switch (status) + { + case DevicePairingResultStatus::AlreadyPaired: + error_code = UniversalBleErrorCode::kAlreadyPaired; + break; + case DevicePairingResultStatus::ConnectionRejected: + error_code = UniversalBleErrorCode::kConnectionRejected; + break; + case DevicePairingResultStatus::NotPaired: + error_code = UniversalBleErrorCode::kNotPaired; + break; + case DevicePairingResultStatus::NotReadyToPair: + case DevicePairingResultStatus::TooManyConnections: + case DevicePairingResultStatus::HardwareFailure: + case DevicePairingResultStatus::NoSupportedProfiles: + case DevicePairingResultStatus::InvalidCeremonyData: + case DevicePairingResultStatus::RequiredHandlerNotRegistered: + case DevicePairingResultStatus::RejectedByHandler: + case DevicePairingResultStatus::RemoteDeviceHasAssociation: + error_code = UniversalBleErrorCode::kPairingFailed; + break; + case DevicePairingResultStatus::AuthenticationTimeout: + case DevicePairingResultStatus::AuthenticationNotAllowed: + case DevicePairingResultStatus::AuthenticationFailure: + error_code = UniversalBleErrorCode::kAuthenticationFailure; + break; + case DevicePairingResultStatus::ProtectionLevelCouldNotBeMet: + error_code = UniversalBleErrorCode::kProtectionLevelNotMet; + break; + case DevicePairingResultStatus::AccessDenied: + error_code = UniversalBleErrorCode::kAccessDenied; + break; + case DevicePairingResultStatus::PairingCanceled: + error_code = UniversalBleErrorCode::kPairingCancelled; + break; + case DevicePairingResultStatus::OperationAlreadyInProgress: + error_code = UniversalBleErrorCode::kOperationInProgress; + break; + default: + error_code = UniversalBleErrorCode::kPairingFailed; + break; + } + + auto error_message = device_pairing_result_to_string(status); + std::string final_message = message.empty() && error_message.has_value() + ? error_message.value() + : message; + return create_flutter_error(error_code, final_message); + } + + FlutterError create_flutter_error_from_unpairing_status( + DeviceUnpairingResultStatus status, + const std::string& message + ) + { + if (status == DeviceUnpairingResultStatus::Unpaired) { + // Success case - shouldn't normally create an error, but if called, return unknown + return create_flutter_error(UniversalBleErrorCode::kUnknownError, message); + } + + UniversalBleErrorCode error_code; + switch (status) + { + case DeviceUnpairingResultStatus::Failed: + error_code = UniversalBleErrorCode::kUnpairingFailed; + break; + case DeviceUnpairingResultStatus::AlreadyUnpaired: + error_code = UniversalBleErrorCode::kAlreadyUnpaired; + break; + case DeviceUnpairingResultStatus::AccessDenied: + error_code = UniversalBleErrorCode::kAccessDenied; + break; + case DeviceUnpairingResultStatus::OperationAlreadyInProgress: + error_code = UniversalBleErrorCode::kOperationInProgress; + break; + default: + error_code = UniversalBleErrorCode::kUnpairingFailed; + break; + } + + auto error_message = device_unpairing_result_to_string(status); + std::string final_message = message.empty() && error_message.has_value() + ? error_message.value() + : message; + return create_flutter_error(error_code, final_message); + } + } // namespace universal_ble diff --git a/windows/src/helper/utils.h b/windows/src/helper/utils.h index 49408bb..9ca39c1 100644 --- a/windows/src/helper/utils.h +++ b/windows/src/helper/utils.h @@ -6,8 +6,10 @@ #include "winrt/Windows.Foundation.h" #include "winrt/Windows.Storage.Streams.h" +#include "winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h" #include "winrt/base.h" #include "universal_ble_base.h" +#include "../generated/universal_ble.g.h" constexpr uint32_t TEN_SECONDS_IN_MSECS = 10000; @@ -28,6 +30,40 @@ namespace universal_ble bool is_little_endian(); bool is_windows11_or_greater(); + /// Creates a FlutterError with the error code enum in details + FlutterError create_flutter_error( + UniversalBleErrorCode code, + const std::string& message = "", + const std::string& details = "" + ); + + + // Error creation functions + /// Creates a FlutterError from GATT communication status + FlutterError create_flutter_error_from_gatt_communication_status( + GattCommunicationStatus status, + const std::string& message = "" + ); + + /// Creates a FlutterError from device pairing result status + FlutterError create_flutter_error_from_pairing_status( + DevicePairingResultStatus status, + const std::string& message = "" + ); + + /// Creates a FlutterError from device unpairing result status + FlutterError create_flutter_error_from_unpairing_status( + DeviceUnpairingResultStatus status, + const std::string& message = "" + ); + + // Create Flutter unknown error + inline FlutterError create_flutter_unknown_error( + const std::string& message = "Unknown error" + ) { + return create_flutter_error(UniversalBleErrorCode::kUnknownError, message); + } + /// To call async functions synchronously template static auto async_get(AsyncT const &async) @@ -42,11 +78,11 @@ namespace universal_ble } catch (const hresult_error &err) { - throw FlutterError("Failed", to_string(err.message())); + throw create_flutter_error(UniversalBleErrorCode::kFailed, to_string(err.message())); } catch (...) { - throw FlutterError("Failed", "Unknown error"); + throw create_flutter_unknown_error(); } } diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 85ca6f6..46b9661 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -5,1267 +5,1234 @@ #include -#include -#include #include #include -#include +#include #include +#include +#include -#include "helper/utils.h" -#include "helper/universal_enum.h" +#include "enum_parser.h" #include "generated/universal_ble.g.h" +#include "helper/universal_enum.h" +#include "helper/utils.h" #include "pin_entry.h" #include "universal_ble_filter_util.h" -#include "enum_parser.h" -namespace universal_ble -{ - using universal_ble::ErrorOr; - using universal_ble::UniversalBleCallbackChannel; - using universal_ble::UniversalBlePlatformChannel; - using universal_ble::UniversalBleScanResult; +namespace universal_ble { +using universal_ble::ErrorOr; +using universal_ble::UniversalBleCallbackChannel; +using universal_ble::UniversalBlePlatformChannel; +using universal_ble::UniversalBleScanResult; - const auto is_connectable_key = L"System.Devices.Aep.Bluetooth.Le.IsConnectable"; - const auto is_connected_key = L"System.Devices.Aep.IsConnected"; - const auto is_paired_key = L"System.Devices.Aep.IsPaired"; - const auto is_present_key = L"System.Devices.Aep.IsPresent"; - const auto device_address_key = L"System.Devices.Aep.DeviceAddress"; - const auto signal_strength_key = L"System.Devices.Aep.SignalStrength"; - static std::unique_ptr callback_channel; +const auto is_connectable_key = + L"System.Devices.Aep.Bluetooth.Le.IsConnectable"; +const auto is_connected_key = L"System.Devices.Aep.IsConnected"; +const auto is_paired_key = L"System.Devices.Aep.IsPaired"; +const auto is_present_key = L"System.Devices.Aep.IsPresent"; +const auto device_address_key = L"System.Devices.Aep.DeviceAddress"; +const auto signal_strength_key = L"System.Devices.Aep.SignalStrength"; +static std::unique_ptr callback_channel; - void UniversalBlePlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar) - { - auto plugin = std::make_unique(registrar); - SetUp(registrar->messenger(), plugin.get()); - callback_channel = std::make_unique(registrar->messenger()); - registrar->AddPlugin(std::move(plugin)); +void UniversalBlePlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows *registrar) { + auto plugin = std::make_unique(registrar); + SetUp(registrar->messenger(), plugin.get()); + callback_channel = + std::make_unique(registrar->messenger()); + registrar->AddPlugin(std::move(plugin)); +} + +UniversalBlePlugin::UniversalBlePlugin( + flutter::PluginRegistrarWindows *registrar) + : ui_thread_handler_(registrar) { + InitializeAsync(); +} + +UniversalBlePlugin::~UniversalBlePlugin() = default; + +// UniversalBlePlatformChannel implementation. +void UniversalBlePlugin::GetBluetoothAvailabilityState( + std::function reply)> result) { + if (!bluetooth_radio_) { + if (!initialized_) { + result(static_cast(AvailabilityState::unknown)); + } else { + result(static_cast(AvailabilityState::unsupported)); + } + } else { + result(static_cast( + get_availability_state_from_radio(bluetooth_radio_.State()))); + } +}; + +void UniversalBlePlugin::EnableBluetooth( + std::function reply)> result) { + if (!bluetooth_radio_) { + result(create_flutter_error(UniversalBleErrorCode::kBluetoothNotAvailable, + "Bluetooth is not available")); + return; } - UniversalBlePlugin::UniversalBlePlugin(flutter::PluginRegistrarWindows *registrar) - : ui_thread_handler_(registrar) - { - InitializeAsync(); + if (bluetooth_radio_.State() == RadioState::On) { + result(true); + return; } - UniversalBlePlugin::~UniversalBlePlugin() = default; + bluetooth_radio_.SetStateAsync(RadioState::On) + .Completed([&, result](const IAsyncOperation &sender, + const AsyncStatus args) { + if (const auto radio_access_status = sender.GetResults(); + radio_access_status == RadioAccessStatus::Allowed) { + result(true); + } else { + result(create_flutter_error(UniversalBleErrorCode::kFailed, + "Failed to enable bluetooth")); + } + }); +} - // UniversalBlePlatformChannel implementation. - void UniversalBlePlugin::GetBluetoothAvailabilityState(std::function reply)> result) - { - if (!bluetooth_radio_) - { - if (!initialized_) - { - result(static_cast(AvailabilityState::unknown)); - } - else - { - result(static_cast(AvailabilityState::unsupported)); - } - } - else - { - result(static_cast(get_availability_state_from_radio(bluetooth_radio_.State()))); - } - }; - - void UniversalBlePlugin::EnableBluetooth(std::function reply)> result) - { - if (!bluetooth_radio_) - { - result(FlutterError("BluetoothNotAvailable", "Bluetooth is not available")); - return; - } - - if (bluetooth_radio_.State() == RadioState::On) - { - result(true); - return; - } - - bluetooth_radio_.SetStateAsync(RadioState::On).Completed( - [&, result](const IAsyncOperation& sender, const AsyncStatus args) - { - if (const auto radio_access_status = sender.GetResults(); radio_access_status == RadioAccessStatus::Allowed) - { - result(true); - } - else - { - result(FlutterError("Failed","Failed to enable bluetooth")); - } - }); +void UniversalBlePlugin::DisableBluetooth( + std::function reply)> result) { + if (!bluetooth_radio_) { + result(create_flutter_error(UniversalBleErrorCode::kBluetoothNotAvailable, + "Bluetooth is not available")); + return; } - void UniversalBlePlugin::DisableBluetooth(std::function reply)> result) - { - if (!bluetooth_radio_) - { - result(FlutterError("BluetoothNotAvailable", "Bluetooth is not available")); - return; - } - - if (bluetooth_radio_.State() == RadioState::Off) - { - result(true); - return; - } - - bluetooth_radio_.SetStateAsync(RadioState::Off).Completed( - [&, result](IAsyncOperation const& sender, AsyncStatus const args) - { - if (const auto radio_access_status = sender.GetResults(); radio_access_status == RadioAccessStatus::Allowed) - { - result(true); - } - else - { - result(FlutterError("Failed","Failed to disable bluetooth")); - } - }); + if (bluetooth_radio_.State() == RadioState::Off) { + result(true); + return; } - std::optional UniversalBlePlugin::StartScan(const UniversalScanFilter *filter) - { + bluetooth_radio_.SetStateAsync(RadioState::Off) + .Completed([&, result](IAsyncOperation const &sender, + AsyncStatus const args) { + if (const auto radio_access_status = sender.GetResults(); + radio_access_status == RadioAccessStatus::Allowed) { + result(true); + } else { + result(create_flutter_error(UniversalBleErrorCode::kFailed, + "Failed to disable bluetooth")); + } + }); +} - if (!bluetooth_radio_ || bluetooth_radio_.State() != RadioState::On) - { - return FlutterError("BluetoothNotAvailable", "Bluetooth is not available"); +std::optional +UniversalBlePlugin::StartScan(const UniversalScanFilter *filter) { + + if (!bluetooth_radio_ || bluetooth_radio_.State() != RadioState::On) { + return create_flutter_error(UniversalBleErrorCode::kBluetoothNotAvailable, + "Bluetooth is not available"); + } + + try { + SetupDeviceWatcher(); + scan_results_.clear(); + const DeviceWatcherStatus device_watcher_status = device_watcher_.Status(); + // std::cout << "DeviceWatcherState: " << + // DeviceWatcherStatusToString(deviceWatcherStatus) << std::endl; + // DeviceWatcher can only start if its in Created, Stopped, or Aborted state + if (device_watcher_status == DeviceWatcherStatus::Created || + device_watcher_status == DeviceWatcherStatus::Stopped || + device_watcher_status == DeviceWatcherStatus::Aborted) { + device_watcher_.Start(); + } else if (device_watcher_status == DeviceWatcherStatus::Stopping) { + return create_flutter_error( + UniversalBleErrorCode::kStoppingScanInProgress, + "StoppingScan in progress"); } - try - { - SetupDeviceWatcher(); - scan_results_.clear(); - const DeviceWatcherStatus device_watcher_status = device_watcher_.Status(); - // std::cout << "DeviceWatcherState: " << DeviceWatcherStatusToString(deviceWatcherStatus) << std::endl; - // DeviceWatcher can only start if its in Created, Stopped, or Aborted state - if (device_watcher_status == DeviceWatcherStatus::Created || device_watcher_status == DeviceWatcherStatus::Stopped || device_watcher_status == DeviceWatcherStatus::Aborted) - { - device_watcher_.Start(); - } - else if (device_watcher_status == DeviceWatcherStatus::Stopping) - { - return FlutterError("AlreadyInProgress", "StoppingScan in progress"); - } + // Setup LeWatcher and apply filters + if (!bluetooth_le_watcher_) { + bluetooth_le_watcher_ = BluetoothLEAdvertisementWatcher(); + bluetooth_le_watcher_.ScanningMode(BluetoothLEScanningMode::Active); + resetScanFilter(); - // Setup LeWatcher and apply filters - if (!bluetooth_le_watcher_) - { - bluetooth_le_watcher_ = BluetoothLEAdvertisementWatcher(); - bluetooth_le_watcher_.ScanningMode(BluetoothLEScanningMode::Active); - resetScanFilter(); + if (filter != nullptr) { + // Native filter supports only 1 service + const bool uses_custom_filters = + filter->with_services().size() > 1 || + filter->with_manufacturer_data().size() > 0 || + filter->with_name_prefix().size() > 0; - if (filter != nullptr) - { - // Native filter supports only 1 service - const bool uses_custom_filters = filter->with_services().size() > 1 || filter->with_manufacturer_data().size() > 0 || filter->with_name_prefix().size() > 0; - - if (uses_custom_filters) - { - std::cout << "Using Custom Scan Filter" << std::endl; - setScanFilter(*filter); - } - else - { - // Apply Services filter - if (!filter->with_services().empty()) - { - for (const auto &uuid : filter->with_services()) - { - bluetooth_le_watcher_.AdvertisementFilter().Advertisement().ServiceUuids().Append(uuid_to_guid(std::get(uuid))); - } + if (uses_custom_filters) { + std::cout << "Using Custom Scan Filter" << std::endl; + setScanFilter(*filter); + } else { + // Apply Services filter + if (!filter->with_services().empty()) { + for (const auto &uuid : filter->with_services()) { + bluetooth_le_watcher_.AdvertisementFilter() + .Advertisement() + .ServiceUuids() + .Append(uuid_to_guid(std::get(uuid))); } } } - - bluetooth_le_watcher_received_token_ = bluetooth_le_watcher_.Received({this, &UniversalBlePlugin::BluetoothLeWatcherReceived}); } - bluetooth_le_watcher_.Start(); + + bluetooth_le_watcher_received_token_ = bluetooth_le_watcher_.Received( + {this, &UniversalBlePlugin::BluetoothLeWatcherReceived}); + } + bluetooth_le_watcher_.Start(); + return std::nullopt; + } catch (...) { + std::cout << "Unknown error StartScan" << std::endl; + return create_flutter_error(UniversalBleErrorCode::kUnknownError, + "Unknown error"); + } +}; + +std::optional UniversalBlePlugin::StopScan() { + if (bluetooth_radio_ && bluetooth_radio_.State() == RadioState::On) { + try { + if (bluetooth_le_watcher_) { + bluetooth_le_watcher_.Received(bluetooth_le_watcher_received_token_); + bluetooth_le_watcher_.Stop(); + } + bluetooth_le_watcher_ = nullptr; + DisposeDeviceWatcher(); + scan_results_.clear(); return std::nullopt; + } catch (const hresult_error &err) { + const int error_code = err.code(); + std::cout << "StopScanLog: " << to_string(err.message()) + << " ErrorCode: " << std::to_string(error_code) << std::endl; + return create_flutter_error(UniversalBleErrorCode::kFailed, + to_string(err.message()), + std::to_string(error_code)); + } catch (...) { + return create_flutter_error(UniversalBleErrorCode::kFailed, + "Failed to Stop"); } - catch (...) - { - std::cout << "Unknown error StartScan" << std::endl; - return FlutterError("Failed", "Unknown error"); - } - }; + } else { + return create_flutter_error(UniversalBleErrorCode::kBluetoothNotAvailable, + "Bluetooth is not available"); + } +}; - std::optional UniversalBlePlugin::StopScan() - { - if (bluetooth_radio_ && bluetooth_radio_.State() == RadioState::On) - { - try - { - if (bluetooth_le_watcher_) - { - bluetooth_le_watcher_.Received(bluetooth_le_watcher_received_token_); - bluetooth_le_watcher_.Stop(); - } - bluetooth_le_watcher_ = nullptr; - DisposeDeviceWatcher(); - scan_results_.clear(); - return std::nullopt; - } - catch (const hresult_error &err) - { - const int error_code = err.code(); - std::cout << "StopScanLog: " << to_string(err.message()) << " ErrorCode: " << std::to_string(error_code) << std::endl; - return FlutterError(std::to_string(error_code), to_string(err.message())); - } - catch (...) - { - return FlutterError("Failed", "Failed to Stop"); - } - } - else - { - return FlutterError("BluetoothNotAvailable", "Bluetooth is not available"); - } - }; - - ErrorOr UniversalBlePlugin::GetConnectionState(const std::string& device_id) - { - const auto it = connected_devices_.find(str_to_mac_address(device_id)); - if (it == connected_devices_.end()) - { - return static_cast(ConnectionState::disconnected); - } - - const auto device_agent = *it->second; - - if (device_agent.device.ConnectionStatus() == BluetoothConnectionStatus::Connected) - { - return static_cast(ConnectionState::connected); - } - else - { - return static_cast(ConnectionState::disconnected); - } +ErrorOr +UniversalBlePlugin::GetConnectionState(const std::string &device_id) { + const auto it = connected_devices_.find(str_to_mac_address(device_id)); + if (it == connected_devices_.end()) { + return static_cast(ConnectionState::disconnected); } - std::optional UniversalBlePlugin::Connect(const std::string &device_id) - { - ConnectAsync(str_to_mac_address(device_id)); - return std::nullopt; - }; + const auto device_agent = *it->second; - std::optional UniversalBlePlugin::Disconnect(const std::string &device_id) - { - auto device_address = str_to_mac_address(device_id); - CleanConnection(device_address); - // TODO: send disconnect event only after disconnect is complete - ui_thread_handler_.Post([device_address] - { callback_channel->OnConnectionChanged(mac_address_to_str(device_address), false, nullptr, SuccessCallback, ErrorCallback); }); - - return std::nullopt; + if (device_agent.device.ConnectionStatus() == + BluetoothConnectionStatus::Connected) { + return static_cast(ConnectionState::connected); + } else { + return static_cast(ConnectionState::disconnected); } +} - void UniversalBlePlugin::DiscoverServices( - const std::string &device_id, - std::function reply)> result) - { - try - { - const auto it = connected_devices_.find(str_to_mac_address(device_id)); - if (it == connected_devices_.end()) - { - result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); +std::optional +UniversalBlePlugin::Connect(const std::string &device_id) { + ConnectAsync(str_to_mac_address(device_id)); + return std::nullopt; +}; + +std::optional +UniversalBlePlugin::Disconnect(const std::string &device_id) { + auto device_address = str_to_mac_address(device_id); + CleanConnection(device_address); + // TODO: send disconnect event only after disconnect is complete + ui_thread_handler_.Post([device_address] { + callback_channel->OnConnectionChanged(mac_address_to_str(device_address), + false, nullptr, SuccessCallback, + ErrorCallback); + }); + + return std::nullopt; +} + +void UniversalBlePlugin::DiscoverServices( + const std::string &device_id, + std::function reply)> result) { + try { + const auto it = connected_devices_.find(str_to_mac_address(device_id)); + if (it == connected_devices_.end()) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); + return; + } + auto device_agent = *it->second; + DiscoverServicesAsync(device_agent, result); + } catch (const FlutterError &err) { + return result(err); + } catch (...) { + std::cout << "DiscoverServicesLog: Unknown error" << std::endl; + return result(create_flutter_error(UniversalBleErrorCode::kUnknownError, + "Unknown error")); + } +} + +void UniversalBlePlugin::SetNotifiable( + const std::string &device_id, const std::string &service, + const std::string &characteristic, int64_t ble_input_property, + std::function reply)> result) { + SetNotifiableAsync(device_id, service, characteristic, ble_input_property, + result); +}; + +void UniversalBlePlugin::ReadValue( + const std::string &device_id, const std::string &service, + const std::string &characteristic, + std::function> reply)> result) { + try { + const auto it = connected_devices_.find(str_to_mac_address(device_id)); + if (it == connected_devices_.end()) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); + return; + } + + auto bluetooth_agent = *it->second; + const GattCharacteristicObject &gatt_characteristic_holder = + bluetooth_agent.FetchCharacteristic(service, characteristic); + const GattCharacteristic gatt_characteristic = + gatt_characteristic_holder.obj; + + const auto properties = gatt_characteristic.CharacteristicProperties(); + if ((properties & GattCharacteristicProperties::Read) == + GattCharacteristicProperties::None) { + result(create_flutter_error( + UniversalBleErrorCode::kCharacteristicDoesNotSupportRead, + "Characteristic does not support read")); + return; + } + + gatt_characteristic.ReadValueAsync(BluetoothCacheMode::Uncached) + .Completed([&, result](IAsyncOperation const &sender, + AsyncStatus const args) { + const auto read_value_result = sender.GetResults(); + const auto status = read_value_result.Status(); + if (status != GattCommunicationStatus::Success) { + result(create_flutter_error_from_gatt_communication_status(status)); + } else { + result(to_bytevc(read_value_result.Value())); + } + }); + } catch (const FlutterError &err) { + return result(err); + } catch (...) { + std::cout << "ReadValueLog: Unknown error" << std::endl; + return result(create_flutter_unknown_error()); + } +} + +void UniversalBlePlugin::WriteValue( + const std::string &device_id, const std::string &service, + const std::string &characteristic, const std::vector &value, + int64_t ble_output_property, + std::function reply)> result) { + try { + const auto it = connected_devices_.find(str_to_mac_address(device_id)); + if (it == connected_devices_.end()) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); + return; + } + auto bluetooth_agent = *it->second; + const GattCharacteristicObject &gatt_characteristic_holder = + bluetooth_agent.FetchCharacteristic(service, characteristic); + const GattCharacteristic gatt_characteristic = + gatt_characteristic_holder.obj; + const auto properties = gatt_characteristic.CharacteristicProperties(); + + auto write_option = GattWriteOption::WriteWithResponse; + if (ble_output_property == + static_cast(BleOutputProperty::withoutResponse)) { + write_option = GattWriteOption::WriteWithoutResponse; + if ((properties & GattCharacteristicProperties::WriteWithoutResponse) == + GattCharacteristicProperties::None) { + result(create_flutter_error( + UniversalBleErrorCode::kCharacteristicDoesNotSupportWriteWithoutResponse, + "Characteristic does not support WriteWithoutResponse")); return; } - auto device_agent = *it->second; - DiscoverServicesAsync(device_agent, result); - } - catch (const FlutterError &err) - { - return result(err); - } - catch (...) - { - std::cout << "DiscoverServicesLog: Unknown error" << std::endl; - return result(FlutterError("Failed", "Unknown error")); - } - } - - void UniversalBlePlugin::SetNotifiable( - const std::string &device_id, - const std::string &service, - const std::string &characteristic, - int64_t ble_input_property, - std::function reply)> result) - { - SetNotifiableAsync(device_id, service, characteristic, ble_input_property, result); - }; - - void UniversalBlePlugin::ReadValue( - const std::string& device_id, - const std::string& service, - const std::string& characteristic, - std::function> reply)> result) - { - try - { - const auto it = connected_devices_.find(str_to_mac_address(device_id)); - if (it == connected_devices_.end()) - { - result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); - return; - } - - auto bluetooth_agent = *it->second; - const GattCharacteristicObject& gatt_characteristic_holder = bluetooth_agent.FetchCharacteristic(service, characteristic); - const GattCharacteristic gatt_characteristic = gatt_characteristic_holder.obj; - - const auto properties = gatt_characteristic.CharacteristicProperties(); - if ((properties & GattCharacteristicProperties::Read) == GattCharacteristicProperties::None) - { - result(FlutterError("NotSupported", "Characteristic does not support read")); - return; - } - - gatt_characteristic.ReadValueAsync(BluetoothCacheMode::Uncached).Completed( - [&, result](IAsyncOperation const &sender, AsyncStatus const args) - { - const auto read_value_result = sender.GetResults(); - auto error = gatt_communication_status_to_error(read_value_result.Status()); - if (error.has_value()) - { - result(FlutterError("Failed", error.value())); - } - else - { - result(to_bytevc(read_value_result.Value())); - } - }); - } - catch (const FlutterError& err) - { - return result(err); - } - catch (...) - { - std::cout << "ReadValueLog: Unknown error" << std::endl; - return result(FlutterError("Failed","Unknown error")); - } - } - - void UniversalBlePlugin::WriteValue( - const std::string& device_id, - const std::string& service, - const std::string& characteristic, - const std::vector& value, - int64_t ble_output_property, - std::function reply)> result) - { - try - { - const auto it = connected_devices_.find(str_to_mac_address(device_id)); - if (it == connected_devices_.end()) - { - result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); - return; - } - auto bluetooth_agent = *it->second; - const GattCharacteristicObject& gatt_characteristic_holder = bluetooth_agent.FetchCharacteristic( - service, characteristic); - const GattCharacteristic gatt_characteristic = gatt_characteristic_holder.obj; - const auto properties = gatt_characteristic.CharacteristicProperties(); - - auto write_option = GattWriteOption::WriteWithResponse; - if (ble_output_property == static_cast(BleOutputProperty::withoutResponse)) - { - write_option = GattWriteOption::WriteWithoutResponse; - if ((properties & GattCharacteristicProperties::WriteWithoutResponse) == GattCharacteristicProperties::None) - { - result(FlutterError("NotSupported", "Characteristic does not support WriteWithoutResponse")); - return; - } - } - else - { - if ((properties & GattCharacteristicProperties::Write) == GattCharacteristicProperties::None) - { - result(FlutterError("NotSupported", "Characteristic does not support Write")); - return; - } - } - - gatt_characteristic.WriteValueAsync(from_bytevc(value), write_option).Completed( - [&, result](IAsyncOperation const &sender, AsyncStatus const args) - { - if (args == AsyncStatus::Error) - { - result(FlutterError("Failed", "Encountered an error.")); - return; - } - - const auto error = gatt_communication_status_to_error(sender.GetResults()); - if (error.has_value()) - { - result(FlutterError("Failed", error.value())); - } - else - { - result(std::nullopt); - } - }); - } - catch (const FlutterError& err) - { - result(err); - } - catch (...) - { - std::cout << "WriteValue: Unknown error" << std::endl; - result(FlutterError("Failed", "Unknown error")); - } - } - - void UniversalBlePlugin::RequestMtu( - const std::string &device_id, - int64_t expected_mtu, - std::function reply)> result) - { - try - { - const auto it = connected_devices_.find(str_to_mac_address(device_id)); - if (it == connected_devices_.end()) - { - result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); + } else { + if ((properties & GattCharacteristicProperties::Write) == + GattCharacteristicProperties::None) { + result(create_flutter_error( + UniversalBleErrorCode::kCharacteristicDoesNotSupportWrite, + "Characteristic does not support Write")); return; } - const auto bluetooth_agent = *it->second; - GattSession::FromDeviceIdAsync(bluetooth_agent.device.BluetoothDeviceId()).Completed( - [&, result](IAsyncOperation const& sender, AsyncStatus const args) - { - if (args == AsyncStatus::Error) - { - result(FlutterError("Failed", "Encountered an error.")); - return; + } + + gatt_characteristic.WriteValueAsync(from_bytevc(value), write_option) + .Completed( + [&, result](IAsyncOperation const &sender, + AsyncStatus const args) { + if (args == AsyncStatus::Error) { + result(create_flutter_error(UniversalBleErrorCode::kFailed, + "Encountered an error.")); + return; } - result((int64_t)sender.GetResults().MaxPduSize()); - }); - } - catch (const FlutterError &err) - { - result(err); - } + const auto status = sender.GetResults(); + if (status != GattCommunicationStatus::Success) { + result(create_flutter_error_from_gatt_communication_status(status)); + } else { + result(std::nullopt); + } + }); + } catch (const FlutterError &err) { + result(err); + } catch (...) { + std::cout << "WriteValue: Unknown error" << std::endl; + result(create_flutter_unknown_error()); } +} - void UniversalBlePlugin::IsPaired( - const std::string &device_id, - std::function reply)> result) - { - IsPairedAsync(device_id, result); - } - - void UniversalBlePlugin::Pair( - const std::string& device_id, - std::function reply)> result) - { - try - { - if (is_windows11_or_greater()) - { - PairAsync(device_id, result); - } - else - { - CustomPairAsync(device_id, result); - } - } - catch (const FlutterError& err) - { - result(err); - } - } - - std::optional UniversalBlePlugin::UnPair(const std::string& device_id) - { - try - { - const auto device = async_get(BluetoothLEDevice::FromBluetoothAddressAsync(str_to_mac_address(device_id))); - if (device == nullptr) - { - return FlutterError("IllegalArgument", "Unknown devicesId:" + device_id); - } - const auto device_information = device.DeviceInformation(); - - if (!device_information.Pairing().IsPaired()) - { - return FlutterError("NotPaired", "Device is not paired"); - } - - const auto device_unpairing_result = async_get(device_information.Pairing().UnpairAsync()); - - const auto error = device_unpairing_result_to_string(device_unpairing_result.Status()); - - if (error.has_value()) - { - return FlutterError("Failed", error.value()); - } - return std::nullopt; - } - catch (const FlutterError& err) - { - return err; - } - } - - void UniversalBlePlugin::GetSystemDevices( - const flutter::EncodableList &with_services, - std::function reply)> result) - { - auto with_services_str = std::vector(); - for (const auto &item : with_services) - { - auto service_id = std::get(item); - with_services_str.push_back(service_id); - } - GetSystemDevicesAsync(with_services_str, result); - } - - /// Helper Methods - - fire_and_forget UniversalBlePlugin::InitializeAsync() - { - const auto radios = co_await Radio::GetRadiosAsync(); - for (auto &&radio : radios) - { - if (radio.Kind() == RadioKind::Bluetooth) - { - bluetooth_radio_ = radio; - radio_state_changed_revoker_ = bluetooth_radio_.StateChanged(auto_revoke, {this, &UniversalBlePlugin::RadioStateChanged}); - RadioStateChanged(bluetooth_radio_, nullptr); - break; - } - } - if (!bluetooth_radio_) - { - std::cout << "Bluetooth is not available" << std::endl; - ui_thread_handler_.Post([] - { callback_channel->OnAvailabilityChanged(static_cast(AvailabilityState::unsupported), SuccessCallback, ErrorCallback); }); - } - initialized_ = true; - } - - fire_and_forget UniversalBlePlugin::PairAsync( - const std::string& device_id, - const std::function reply)> result) - { - try - { - std::cout << "Trying to pair" << std::endl; - - const auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(str_to_mac_address(device_id)); - if (device == nullptr) - { - result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); - co_return; - } - - std::cout << "Got device" << std::endl; - - const auto device_information = device.DeviceInformation(); - if (device_information.Pairing().IsPaired()) - result(true); - else if (!device_information.Pairing().CanPair()) - result(FlutterError("NotPairable", "Device is not pairable")); - else - { - const auto pair_result = co_await device_information.Pairing().PairAsync(); - std::cout << "PairLog: Received pairing status" << std::endl; - bool is_paired = pair_result.Status() == DevicePairingResultStatus::Paired; - result(is_paired); - - const std::string* error_msg = nullptr; - const auto error_str = parse_pairing_fail_error(pair_result); - if (error_str.has_value()) - { - error_msg = &error_str.value(); - } - ui_thread_handler_.Post([device_id, is_paired, error_msg] - { callback_channel->OnPairStateChange(device_id, is_paired, error_msg, SuccessCallback, ErrorCallback); }); - } - } - catch (...) - { - result(false); - std::cout << "PairLog: Unknown error" << std::endl; - } - } - - fire_and_forget UniversalBlePlugin::CustomPairAsync( - const std::string& device_id, - const std::function reply)> result) - { - try - { - const auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(str_to_mac_address(device_id)); - if (device == nullptr) - { - result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); - co_return; - } - const auto device_information = device.DeviceInformation(); - if (device_information.Pairing().IsPaired()) - result(true); - else if (!device_information.Pairing().CanPair()) - result(FlutterError("NotPairable", "Device is not pairable")); - else - { - const auto custom_pairing = device_information.Pairing().Custom(); - const event_token token = custom_pairing.PairingRequested({this, &UniversalBlePlugin::PairingRequestedHandler}); - std::cout << "PairLog: Trying to pair" << std::endl; - const DevicePairingProtectionLevel protection_level = device_information.Pairing().ProtectionLevel(); - // DevicePairingKinds => None, ConfirmOnly, DisplayPin, ProvidePin, ConfirmPinMatch, ProvidePasswordCredential - const auto pair_result = co_await custom_pairing.PairAsync(DevicePairingKinds::ConfirmOnly | DevicePairingKinds::ProvidePin, protection_level); - std::cout << "PairLog: Got Pair Result" << std::endl; - const DevicePairingResultStatus status = pair_result.Status(); - custom_pairing.PairingRequested(token); - bool is_paired = status == DevicePairingResultStatus::Paired; - result(is_paired); - - const std::string* error_msg = nullptr; - const auto error_str = parse_pairing_fail_error(pair_result); - if (error_str.has_value()) - { - error_msg = &error_str.value(); - } - ui_thread_handler_.Post([device_id, is_paired, error_msg] - { callback_channel->OnPairStateChange(device_id, is_paired, error_msg, SuccessCallback, ErrorCallback); }); - } - } - catch (...) - { - result(false); - std::cout << "PairLog Error: Pairing Failed" << std::endl; - } - } - - // ReSharper disable once CppMemberFunctionMayBeStatic - void UniversalBlePlugin::PairingRequestedHandler(DeviceInformationCustomPairing sender, const DevicePairingRequestedEventArgs& event_args) - { - std::cout << "PairLog: Got PairingRequest" << std::endl; - const DevicePairingKinds kind = event_args.PairingKind(); - if (kind != DevicePairingKinds::ProvidePin) - { - event_args.Accept(); +void UniversalBlePlugin::RequestMtu( + const std::string &device_id, int64_t expected_mtu, + std::function reply)> result) { + try { + const auto it = connected_devices_.find(str_to_mac_address(device_id)); + if (it == connected_devices_.end()) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); return; } + const auto bluetooth_agent = *it->second; + GattSession::FromDeviceIdAsync(bluetooth_agent.device.BluetoothDeviceId()) + .Completed([&, result](IAsyncOperation const &sender, + AsyncStatus const args) { + if (args == AsyncStatus::Error) { + result(create_flutter_unknown_error()); + return; + } - std::cout << "PairLog: Trying to get pin from user" << std::endl; - const hstring pin = askForPairingPin(); - std::wcout << "PairLog: Got Pin: " << pin.c_str() << std::endl; - event_args.Accept(pin); + result((int64_t)sender.GetResults().MaxPduSize()); + }); + } catch (const FlutterError &err) { + result(err); + } +} + +void UniversalBlePlugin::IsPaired( + const std::string &device_id, + std::function reply)> result) { + IsPairedAsync(device_id, result); +} + +void UniversalBlePlugin::Pair(const std::string &device_id, + std::function reply)> result) { + try { + if (is_windows11_or_greater()) { + PairAsync(device_id, result); + } else { + CustomPairAsync(device_id, result); + } + } catch (const FlutterError &err) { + result(err); + } +} + +std::optional +UniversalBlePlugin::UnPair(const std::string &device_id) { + try { + const auto device = async_get(BluetoothLEDevice::FromBluetoothAddressAsync( + str_to_mac_address(device_id))); + if (device == nullptr) { + return create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id); + } + const auto device_information = device.DeviceInformation(); + + if (!device_information.Pairing().IsPaired()) { + return create_flutter_error(UniversalBleErrorCode::kNotPaired, + "Device is not paired"); + } + + const auto device_unpairing_result = + async_get(device_information.Pairing().UnpairAsync()); + + const auto status = device_unpairing_result.Status(); + if (status != DeviceUnpairingResultStatus::Unpaired) { + return create_flutter_error_from_unpairing_status(status); + } + return std::nullopt; + } catch (const FlutterError &err) { + return err; + } +} + +void UniversalBlePlugin::GetSystemDevices( + const flutter::EncodableList &with_services, + std::function reply)> result) { + auto with_services_str = std::vector(); + for (const auto &item : with_services) { + auto service_id = std::get(item); + with_services_str.push_back(service_id); + } + GetSystemDevicesAsync(with_services_str, result); +} + +/// Helper Methods + +fire_and_forget UniversalBlePlugin::InitializeAsync() { + const auto radios = co_await Radio::GetRadiosAsync(); + for (auto &&radio : radios) { + if (radio.Kind() == RadioKind::Bluetooth) { + bluetooth_radio_ = radio; + radio_state_changed_revoker_ = bluetooth_radio_.StateChanged( + auto_revoke, {this, &UniversalBlePlugin::RadioStateChanged}); + RadioStateChanged(bluetooth_radio_, nullptr); + break; + } + } + if (!bluetooth_radio_) { + std::cout << "Bluetooth is not available" << std::endl; + ui_thread_handler_.Post([] { + callback_channel->OnAvailabilityChanged( + static_cast(AvailabilityState::unsupported), SuccessCallback, + ErrorCallback); + }); + } + initialized_ = true; +} + +fire_and_forget UniversalBlePlugin::PairAsync( + const std::string &device_id, + const std::function reply)> result) { + try { + std::cout << "Trying to pair" << std::endl; + + const auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync( + str_to_mac_address(device_id)); + if (device == nullptr) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); + co_return; + } + + std::cout << "Got device" << std::endl; + + const auto device_information = device.DeviceInformation(); + if (device_information.Pairing().IsPaired()) + result(true); + else if (!device_information.Pairing().CanPair()) + result(create_flutter_error(UniversalBleErrorCode::kNotPairable, + "Device is not pairable")); + else { + const auto pair_result = + co_await device_information.Pairing().PairAsync(); + std::cout << "PairLog: Received pairing status" << std::endl; + bool is_paired = + pair_result.Status() == DevicePairingResultStatus::Paired; + result(is_paired); + + const std::string *error_msg = nullptr; + const auto error_str = device_pairing_result_to_string(pair_result.Status()); + if (error_str.has_value()) { + error_msg = &error_str.value(); + } + ui_thread_handler_.Post([device_id, is_paired, error_msg] { + callback_channel->OnPairStateChange(device_id, is_paired, error_msg, + SuccessCallback, ErrorCallback); + }); + } + } catch (...) { + result(false); + std::cout << "PairLog: Unknown error" << std::endl; + } +} + +fire_and_forget UniversalBlePlugin::CustomPairAsync( + const std::string &device_id, + const std::function reply)> result) { + try { + const auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync( + str_to_mac_address(device_id)); + if (device == nullptr) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); + co_return; + } + const auto device_information = device.DeviceInformation(); + if (device_information.Pairing().IsPaired()) + result(true); + else if (!device_information.Pairing().CanPair()) + result(create_flutter_error(UniversalBleErrorCode::kNotPairable, + "Device is not pairable")); + else { + const auto custom_pairing = device_information.Pairing().Custom(); + const event_token token = custom_pairing.PairingRequested( + {this, &UniversalBlePlugin::PairingRequestedHandler}); + std::cout << "PairLog: Trying to pair" << std::endl; + const DevicePairingProtectionLevel protection_level = + device_information.Pairing().ProtectionLevel(); + // DevicePairingKinds => None, ConfirmOnly, DisplayPin, ProvidePin, + // ConfirmPinMatch, ProvidePasswordCredential + const auto pair_result = co_await custom_pairing.PairAsync( + DevicePairingKinds::ConfirmOnly | DevicePairingKinds::ProvidePin, + protection_level); + std::cout << "PairLog: Got Pair Result" << std::endl; + const DevicePairingResultStatus status = pair_result.Status(); + custom_pairing.PairingRequested(token); + bool is_paired = status == DevicePairingResultStatus::Paired; + result(is_paired); + + const std::string *error_msg = nullptr; + const auto error_str = device_pairing_result_to_string(status); + if (error_str.has_value()) { + error_msg = &error_str.value(); + } + ui_thread_handler_.Post([device_id, is_paired, error_msg] { + callback_channel->OnPairStateChange(device_id, is_paired, error_msg, + SuccessCallback, ErrorCallback); + }); + } + } catch (...) { + result(false); + std::cout << "PairLog Error: Pairing Failed" << std::endl; + } +} + +// ReSharper disable once CppMemberFunctionMayBeStatic +void UniversalBlePlugin::PairingRequestedHandler( + DeviceInformationCustomPairing sender, + const DevicePairingRequestedEventArgs &event_args) { + std::cout << "PairLog: Got PairingRequest" << std::endl; + const DevicePairingKinds kind = event_args.PairingKind(); + if (kind != DevicePairingKinds::ProvidePin) { + event_args.Accept(); + return; } - // Send device to callback channel - // if device is already discovered in deviceWatcher then merge the scan result - void UniversalBlePlugin::PushUniversalScanResult(UniversalBleScanResult scan_result, const bool is_connectable) - { - const std::optional it = scan_results_.get(scan_result.device_id()); - if (it.has_value()) - { - const UniversalBleScanResult ¤t_scan_result = it.value(); - bool should_update = false; + std::cout << "PairLog: Trying to get pin from user" << std::endl; + const hstring pin = askForPairingPin(); + std::wcout << "PairLog: Got Pin: " << pin.c_str() << std::endl; + event_args.Accept(pin); +} - // Check if current scanResult name is longer than the received scanResult name - if (scan_result.name() != nullptr && !scan_result.name()->empty() && current_scan_result.name() != nullptr && !current_scan_result.name()->empty()) - { - if (current_scan_result.name()->size() > scan_result.name()->size()) - { - scan_result.set_name(*current_scan_result.name()); - } - } +// Send device to callback channel +// if device is already discovered in deviceWatcher then merge the scan result +void UniversalBlePlugin::PushUniversalScanResult( + UniversalBleScanResult scan_result, const bool is_connectable) { + const std::optional it = + scan_results_.get(scan_result.device_id()); + if (it.has_value()) { + const UniversalBleScanResult ¤t_scan_result = it.value(); + bool should_update = false; - if ((scan_result.name() == nullptr || scan_result.name()->empty()) && (current_scan_result.name() != nullptr && !current_scan_result.name()->empty())) - { + // Check if current scanResult name is longer than the received scanResult + // name + if (scan_result.name() != nullptr && !scan_result.name()->empty() && + current_scan_result.name() != nullptr && + !current_scan_result.name()->empty()) { + if (current_scan_result.name()->size() > scan_result.name()->size()) { scan_result.set_name(*current_scan_result.name()); - should_update = true; - } - - if (scan_result.is_paired() == nullptr && current_scan_result.is_paired() != nullptr) - { - scan_result.set_is_paired(current_scan_result.is_paired()); - should_update = true; - } - - if ((scan_result.manufacturer_data_list() == nullptr || scan_result.manufacturer_data_list()->empty()) && current_scan_result.manufacturer_data_list() != nullptr) - { - scan_result.set_manufacturer_data_list(current_scan_result.manufacturer_data_list()); - should_update = true; - } - - if (scan_result.services() == nullptr && current_scan_result.services() != nullptr) - { - scan_result.set_services(current_scan_result.services()); - should_update = true; - } - - // if nothing to update then return - if (!should_update) - { - return; } } - // Update cache - scan_results_.insert_or_assign(scan_result.device_id(), scan_result); - - // Filter final result before sending to Flutter - if (is_connectable && filterDevice(scan_result)) - { - ui_thread_handler_.Post([scan_result] - { callback_channel->OnScanResult(scan_result, SuccessCallback, ErrorCallback); }); + if ((scan_result.name() == nullptr || scan_result.name()->empty()) && + (current_scan_result.name() != nullptr && + !current_scan_result.name()->empty())) { + scan_result.set_name(*current_scan_result.name()); + should_update = true; } - } - void UniversalBlePlugin::SetupDeviceWatcher() - { - if (device_watcher_ != nullptr) + if (scan_result.is_paired() == nullptr && + current_scan_result.is_paired() != nullptr) { + scan_result.set_is_paired(current_scan_result.is_paired()); + should_update = true; + } + + if ((scan_result.manufacturer_data_list() == nullptr || + scan_result.manufacturer_data_list()->empty()) && + current_scan_result.manufacturer_data_list() != nullptr) { + scan_result.set_manufacturer_data_list( + current_scan_result.manufacturer_data_list()); + should_update = true; + } + + if (scan_result.services() == nullptr && + current_scan_result.services() != nullptr) { + scan_result.set_services(current_scan_result.services()); + should_update = true; + } + + // if nothing to update then return + if (!should_update) { return; - - device_watcher_ = DeviceInformation::CreateWatcher( - L"(System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\")", - { - device_address_key, - is_connected_key, - is_paired_key, - is_present_key, - is_connectable_key, - signal_strength_key, - }, - DeviceInformationKind::AssociationEndpoint); - - /// Device Added from DeviceWatcher - device_watcher_added_token_ = device_watcher_.Added([this](DeviceWatcher sender, const DeviceInformation& device_info) - { - const auto properties = device_info.Properties(); - if (!properties.HasKey(device_address_key)) - { - return; - } - const auto device_address = to_string(properties.Lookup(device_address_key).as().GetString()); - const std::string device_info_id = to_string(device_info.Id()); - // Map Id -> MAC and MAC -> DeviceInformation - device_watcher_id_to_mac_.insert_or_assign(device_info_id, device_address); - device_watcher_devices_.insert_or_assign(device_address, device_info); - OnDeviceInfoReceived(device_info); - // On Device Added - }); - - // Update only if device is already discovered in deviceWatcher.Added - device_watcher_updated_token_ = device_watcher_.Updated([this](DeviceWatcher sender, const DeviceInformationUpdate& device_info_update) - { - const std::string device_info_id = to_string(device_info_update.Id()); - // Resolve MAC from Id - const auto mac_lookup = device_watcher_id_to_mac_.get(device_info_id); - if (!mac_lookup.has_value()) - { - return; - } - const std::string mac_key = mac_lookup.value(); - const auto it = device_watcher_devices_.get(mac_key); - if (it.has_value()) - { - const auto value = it.value(); - value.Update(device_info_update); - device_watcher_devices_.insert_or_assign(mac_key, value); - OnDeviceInfoReceived(value); - } - // On Device Updated - }); - - device_watcher_removed_token_ = device_watcher_.Removed([this](DeviceWatcher sender, const DeviceInformationUpdate& args) - { - const std::string device_id = to_string(args.Id()); - const auto mac_lookup = device_watcher_id_to_mac_.get(device_id); - if (mac_lookup.has_value()) - { - const std::string mac_key = mac_lookup.value(); - device_watcher_devices_.remove(mac_key); - device_watcher_id_to_mac_.remove(device_id); - } - // On Device Removed - }); - - device_watcher_enumeration_completed_token_ = device_watcher_.EnumerationCompleted([this](DeviceWatcher sender, IInspectable args) - { - std::cout << "DeviceWatcherEvent: EnumerationCompleted" << std::endl; - DisposeDeviceWatcher(); - // EnumerationCompleted - }); - - device_watcher_stopped_token_ = device_watcher_.Stopped([this](DeviceWatcher sender, IInspectable args) - { - // std::cout << "DeviceWatcherEvent: Stopped" << std::endl; - // disposeDeviceWatcher(); - // DeviceWatcher Stopped - }); - } - - void UniversalBlePlugin::DisposeDeviceWatcher() - { - if (device_watcher_ != nullptr) - { - device_watcher_.Added(device_watcher_added_token_); - device_watcher_.Updated(device_watcher_updated_token_); - device_watcher_.Removed(device_watcher_removed_token_); - device_watcher_.EnumerationCompleted(device_watcher_enumeration_completed_token_); - device_watcher_.Stopped(device_watcher_stopped_token_); - const auto status = device_watcher_.Status(); - // std::cout << "DisposingDeviceWatcher, CurrentState: " << DeviceWatcherStatusToString(status) << std::endl; - if (status == DeviceWatcherStatus::Started) - { - device_watcher_.Stop(); - } - device_watcher_ = nullptr; - device_watcher_devices_.clear(); - device_watcher_id_to_mac_.clear(); } } - void UniversalBlePlugin::OnDeviceInfoReceived(const DeviceInformation& device_info) - { - const auto properties = device_info.Properties(); + // Update cache + scan_results_.insert_or_assign(scan_result.device_id(), scan_result); - // Avoid devices if not connectable or if deviceAddressKey is not present - if (!(properties.HasKey(is_connectable_key) && (properties.Lookup(is_connectable_key).as()).GetBoolean()) || !properties.HasKey(device_address_key)) - return; + // Filter final result before sending to Flutter + if (is_connectable && filterDevice(scan_result)) { + ui_thread_handler_.Post([scan_result] { + callback_channel->OnScanResult(scan_result, SuccessCallback, + ErrorCallback); + }); + } +} - const auto bluetooth_address_property_value = properties.Lookup(device_address_key).as(); - const std::string device_address = to_string(bluetooth_address_property_value.GetString()); +void UniversalBlePlugin::SetupDeviceWatcher() { + if (device_watcher_ != nullptr) + return; - // Update device info if already discovered in advertisementWatcher - if (scan_results_.get(device_address).has_value()) - { + device_watcher_ = DeviceInformation::CreateWatcher( + L"(System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-" + L"76eaa7084d49}\")", + { + device_address_key, + is_connected_key, + is_paired_key, + is_present_key, + is_connectable_key, + signal_strength_key, + }, + DeviceInformationKind::AssociationEndpoint); + + /// Device Added from DeviceWatcher + device_watcher_added_token_ = device_watcher_.Added( + [this](DeviceWatcher sender, const DeviceInformation &device_info) { + const auto properties = device_info.Properties(); + if (!properties.HasKey(device_address_key)) { + return; + } + const auto device_address = + to_string(properties.Lookup(device_address_key) + .as() + .GetString()); + const std::string device_info_id = to_string(device_info.Id()); + // Map Id -> MAC and MAC -> DeviceInformation + device_watcher_id_to_mac_.insert_or_assign(device_info_id, + device_address); + device_watcher_devices_.insert_or_assign(device_address, device_info); + OnDeviceInfoReceived(device_info); + // On Device Added + }); + + // Update only if device is already discovered in deviceWatcher.Added + device_watcher_updated_token_ = device_watcher_.Updated( + [this](DeviceWatcher sender, + const DeviceInformationUpdate &device_info_update) { + const std::string device_info_id = to_string(device_info_update.Id()); + // Resolve MAC from Id + const auto mac_lookup = device_watcher_id_to_mac_.get(device_info_id); + if (!mac_lookup.has_value()) { + return; + } + const std::string mac_key = mac_lookup.value(); + const auto it = device_watcher_devices_.get(mac_key); + if (it.has_value()) { + const auto value = it.value(); + value.Update(device_info_update); + device_watcher_devices_.insert_or_assign(mac_key, value); + OnDeviceInfoReceived(value); + } + // On Device Updated + }); + + device_watcher_removed_token_ = device_watcher_.Removed( + [this](DeviceWatcher sender, const DeviceInformationUpdate &args) { + const std::string device_id = to_string(args.Id()); + const auto mac_lookup = device_watcher_id_to_mac_.get(device_id); + if (mac_lookup.has_value()) { + const std::string mac_key = mac_lookup.value(); + device_watcher_devices_.remove(mac_key); + device_watcher_id_to_mac_.remove(device_id); + } + // On Device Removed + }); + + device_watcher_enumeration_completed_token_ = + device_watcher_.EnumerationCompleted([this](DeviceWatcher sender, + IInspectable args) { + std::cout << "DeviceWatcherEvent: EnumerationCompleted" << std::endl; + DisposeDeviceWatcher(); + // EnumerationCompleted + }); + + device_watcher_stopped_token_ = + device_watcher_.Stopped([this](DeviceWatcher sender, IInspectable args) { + // std::cout << "DeviceWatcherEvent: Stopped" << std::endl; + // disposeDeviceWatcher(); + // DeviceWatcher Stopped + }); +} + +void UniversalBlePlugin::DisposeDeviceWatcher() { + if (device_watcher_ != nullptr) { + device_watcher_.Added(device_watcher_added_token_); + device_watcher_.Updated(device_watcher_updated_token_); + device_watcher_.Removed(device_watcher_removed_token_); + device_watcher_.EnumerationCompleted( + device_watcher_enumeration_completed_token_); + device_watcher_.Stopped(device_watcher_stopped_token_); + const auto status = device_watcher_.Status(); + // std::cout << "DisposingDeviceWatcher, CurrentState: " << + // DeviceWatcherStatusToString(status) << std::endl; + if (status == DeviceWatcherStatus::Started) { + device_watcher_.Stop(); + } + device_watcher_ = nullptr; + device_watcher_devices_.clear(); + device_watcher_id_to_mac_.clear(); + } +} + +void UniversalBlePlugin::OnDeviceInfoReceived( + const DeviceInformation &device_info) { + const auto properties = device_info.Properties(); + + // Avoid devices if not connectable or if deviceAddressKey is not present + if (!(properties.HasKey(is_connectable_key) && + (properties.Lookup(is_connectable_key).as()) + .GetBoolean()) || + !properties.HasKey(device_address_key)) + return; + + const auto bluetooth_address_property_value = + properties.Lookup(device_address_key).as(); + const std::string device_address = + to_string(bluetooth_address_property_value.GetString()); + + // Update device info if already discovered in advertisementWatcher + if (scan_results_.get(device_address).has_value()) { + bool is_paired = device_info.Pairing().IsPaired(); + if (properties.HasKey(is_paired_key)) { + const auto is_paired_property_value = + properties.Lookup(is_paired_key).as(); + is_paired = is_paired_property_value.GetBoolean(); + } + + UniversalBleScanResult universal_scan_result(device_address); + universal_scan_result.set_is_paired(is_paired); + + if (!device_info.Name().empty()) + universal_scan_result.set_name(to_string(device_info.Name())); + + if (properties.HasKey(signal_strength_key)) { + const auto rssi_property_value = + properties.Lookup(signal_strength_key).as(); + const int16_t rssi = rssi_property_value.GetInt16(); + universal_scan_result.set_rssi(rssi); + } + + PushUniversalScanResult(universal_scan_result, true); + } +} + +/// Advertisement received from advertisementWatcher +void UniversalBlePlugin::BluetoothLeWatcherReceived( + const BluetoothLEAdvertisementWatcher &, + const BluetoothLEAdvertisementReceivedEventArgs &args) { + try { + auto device_id = mac_address_to_str(args.BluetoothAddress()); + auto universal_scan_result = UniversalBleScanResult(device_id); + std::string name = to_string(args.Advertisement().LocalName()); + + auto manufacturer_data_encodable_list = flutter::EncodableList(); + if (args.Advertisement() != nullptr) { + for (BluetoothLEManufacturerData msd : + args.Advertisement().ManufacturerData()) { + auto universal_manufacturer_data = UniversalManufacturerData( + static_cast(msd.CompanyId()), to_bytevc(msd.Data())); + manufacturer_data_encodable_list.push_back( + flutter::CustomEncodableValue(universal_manufacturer_data)); + } + } + + auto data_section = args.Advertisement().DataSections(); + for (auto &&data : data_section) { + auto data_bytes = to_bytevc(data.Data()); + // Use CompleteName from dataType if localName is empty + if (name.empty() && + data.DataType() == static_cast( + AdvertisementSectionType::CompleteLocalName)) { + name = std::string(data_bytes.begin(), data_bytes.end()); + } + // Use ShortenedLocalName from dataType if localName is empty + else if (name.empty() && + data.DataType() == + static_cast( + AdvertisementSectionType::ShortenedLocalName)) { + name = std::string(data_bytes.begin(), data_bytes.end()); + } + } + + if (!name.empty()) { + universal_scan_result.set_name(name); + } + + if (!manufacturer_data_encodable_list.empty()) { + universal_scan_result.set_manufacturer_data_list( + manufacturer_data_encodable_list); + } + + universal_scan_result.set_rssi(args.RawSignalStrengthInDBm()); + + // Add services + auto services = flutter::EncodableList(); + for (auto &&uuid : args.Advertisement().ServiceUuids()) + services.push_back(guid_to_uuid(uuid)); + universal_scan_result.set_services(services); + + // check if this device already discovered in deviceWatcher + auto it = device_watcher_devices_.get(device_id); + if (it.has_value()) { + auto &device_info = it.value(); + auto properties = device_info.Properties(); + + // Update Paired Status bool is_paired = device_info.Pairing().IsPaired(); if (properties.HasKey(is_paired_key)) - { - const auto is_paired_property_value = properties.Lookup(is_paired_key).as(); - is_paired = is_paired_property_value.GetBoolean(); - } - - UniversalBleScanResult universal_scan_result(device_address); + is_paired = (properties.Lookup(is_paired_key).as()) + .GetBoolean(); universal_scan_result.set_is_paired(is_paired); - if (!device_info.Name().empty()) + // Update Name + if (name.empty() && !device_info.Name().empty()) universal_scan_result.set_name(to_string(device_info.Name())); - - if (properties.HasKey(signal_strength_key)) - { - const auto rssi_property_value = properties.Lookup(signal_strength_key).as(); - const int16_t rssi = rssi_property_value.GetInt16(); - universal_scan_result.set_rssi(rssi); - } - - PushUniversalScanResult(universal_scan_result, true); } + + // Filter Device + PushUniversalScanResult(universal_scan_result, args.IsConnectable()); + } catch (...) { + std::cout << "ScanResultErrorInParsing" << std::endl; + } +} + +void UniversalBlePlugin::RadioStateChanged(const Radio &sender, + const IInspectable &) { + const auto radio_state = !sender ? RadioState::Disabled : sender.State(); + if (old_radio_state_ == radio_state) { + return; + } + old_radio_state_ = radio_state; + auto state = get_availability_state_from_radio(radio_state); + + ui_thread_handler_.Post([state] { + callback_channel->OnAvailabilityChanged(static_cast(state), + SuccessCallback, ErrorCallback); + }); +} + +fire_and_forget UniversalBlePlugin::ConnectAsync(uint64_t bluetooth_address) { + BluetoothLEDevice device = + co_await BluetoothLEDevice::FromBluetoothAddressAsync(bluetooth_address); + if (!device) { + std::cout << "ConnectionLog: ConnectionFailed: Failed to get device" + << std::endl; + ui_thread_handler_.Post([bluetooth_address] { + callback_channel->OnConnectionChanged( + mac_address_to_str(bluetooth_address), false, + new std::string("Failed to get device"), SuccessCallback, + ErrorCallback); + }); + + co_return; + } + std::cout << "ConnectionLog: Device found" << std::endl; + auto services_result = + co_await device.GetGattServicesAsync((BluetoothCacheMode::Uncached)); + auto services_result_error = + gatt_communication_status_to_error(services_result.Status()); + if (services_result_error.has_value()) { + std::cout << "ConnectionFailed: Failed to get services: " + << services_result_error.value() << std::endl; + ui_thread_handler_.Post([bluetooth_address, services_result_error] { + callback_channel->OnConnectionChanged( + mac_address_to_str(bluetooth_address), false, + &services_result_error.value(), SuccessCallback, ErrorCallback); + }); + co_return; } - /// Advertisement received from advertisementWatcher - void UniversalBlePlugin::BluetoothLeWatcherReceived(const BluetoothLEAdvertisementWatcher&, const BluetoothLEAdvertisementReceivedEventArgs& args) - { - try - { - auto device_id = mac_address_to_str(args.BluetoothAddress()); - auto universal_scan_result = UniversalBleScanResult(device_id); - std::string name = to_string(args.Advertisement().LocalName()); + std::cout << "ConnectionLog: Services discovered" << std::endl; + std::unordered_map gatt_map; + auto gatt_services = services_result.Services(); + for (GattDeviceService &&service : gatt_services) { + GattServiceObject gatt_service; + gatt_service.obj = service; + std::string service_uuid = guid_to_uuid(service.Uuid()); + auto characteristics_result = + co_await service.GetCharacteristicsAsync(BluetoothCacheMode::Uncached); + auto characteristics_result_error = + gatt_communication_status_to_error(characteristics_result.Status()); - auto manufacturer_data_encodable_list = flutter::EncodableList(); - if (args.Advertisement() != nullptr) - { - for (BluetoothLEManufacturerData msd : args.Advertisement().ManufacturerData()) - { - auto universal_manufacturer_data = UniversalManufacturerData(static_cast(msd.CompanyId()), to_bytevc(msd.Data())); - manufacturer_data_encodable_list.push_back(flutter::CustomEncodableValue(universal_manufacturer_data)); + if (characteristics_result_error.has_value()) { + std::cout << "Failed to get characteristics for service: " << service_uuid + << ", With Status: " << characteristics_result_error.value() + << std::endl; + continue; + // PostConnectionUpdate(bluetoothAddress, ConnectionState::disconnected); + // co_return; + } + auto gatt_characteristics = characteristics_result.Characteristics(); + for (GattCharacteristic &&characteristic : gatt_characteristics) { + GattCharacteristicObject gatt_characteristic; + gatt_characteristic.obj = characteristic; + gatt_characteristic.subscription_token = std::nullopt; + std::string characteristic_uuid = guid_to_uuid(characteristic.Uuid()); + gatt_service.characteristics.insert_or_assign( + characteristic_uuid, std::move(gatt_characteristic)); + } + gatt_map.insert_or_assign(service_uuid, std::move(gatt_service)); + } + + event_token connection_status_changed_token = device.ConnectionStatusChanged( + {this, &UniversalBlePlugin::BluetoothLeDeviceConnectionStatusChanged}); + auto device_agent = std::make_unique( + device, connection_status_changed_token, gatt_map); + auto pair = std::make_pair(bluetooth_address, std::move(device_agent)); + connected_devices_.insert(std::move(pair)); + std::cout << "ConnectionLog: Connected" << std::endl; + ui_thread_handler_.Post([bluetooth_address] { + callback_channel->OnConnectionChanged(mac_address_to_str(bluetooth_address), + true, nullptr, SuccessCallback, + ErrorCallback); + }); +} + +void UniversalBlePlugin::BluetoothLeDeviceConnectionStatusChanged( + const BluetoothLEDevice &sender, const IInspectable &) { + if (sender.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) { + CleanConnection(sender.BluetoothAddress()); + auto bluetooth_address = sender.BluetoothAddress(); + ui_thread_handler_.Post([bluetooth_address] { + callback_channel->OnConnectionChanged( + mac_address_to_str(bluetooth_address), false, nullptr, + SuccessCallback, ErrorCallback); + }); + } +} + +void UniversalBlePlugin::CleanConnection(const uint64_t bluetooth_address) { + const auto node = connected_devices_.extract(bluetooth_address); + if (!node.empty()) { + const auto device_agent = std::move(node.mapped()); + device_agent->device.ConnectionStatusChanged( + device_agent->connection_status_changed_token); + // Clean up all characteristics tokens + for (auto &[service_id, service] : device_agent->gatt_map) { + for (auto &[char_id, characteristic] : service.characteristics) { + if (characteristic.subscription_token.has_value()) { + characteristic.obj.ValueChanged( + characteristic.subscription_token.value()); + characteristic.subscription_token = std::nullopt; } } - - auto data_section = args.Advertisement().DataSections(); - for (auto &&data : data_section) - { - auto data_bytes = to_bytevc(data.Data()); - // Use CompleteName from dataType if localName is empty - if (name.empty() && data.DataType() == static_cast(AdvertisementSectionType::CompleteLocalName)) - { - name = std::string(data_bytes.begin(), data_bytes.end()); - } - // Use ShortenedLocalName from dataType if localName is empty - else if (name.empty() && data.DataType() == static_cast(AdvertisementSectionType::ShortenedLocalName)) - { - name = std::string(data_bytes.begin(), data_bytes.end()); - } - } - - if (!name.empty()) - { - universal_scan_result.set_name(name); - } - - if (!manufacturer_data_encodable_list.empty()) - { - universal_scan_result.set_manufacturer_data_list(manufacturer_data_encodable_list); - } - - universal_scan_result.set_rssi(args.RawSignalStrengthInDBm()); - - // Add services - auto services = flutter::EncodableList(); - for (auto &&uuid : args.Advertisement().ServiceUuids()) - services.push_back(guid_to_uuid(uuid)); - universal_scan_result.set_services(services); - - // check if this device already discovered in deviceWatcher - auto it = device_watcher_devices_.get(device_id); - if (it.has_value()) - { - auto &device_info = it.value(); - auto properties = device_info.Properties(); - - // Update Paired Status - bool is_paired = device_info.Pairing().IsPaired(); - if (properties.HasKey(is_paired_key)) - is_paired = (properties.Lookup(is_paired_key).as()).GetBoolean(); - universal_scan_result.set_is_paired(is_paired); - - // Update Name - if (name.empty() && !device_info.Name().empty()) - universal_scan_result.set_name(to_string(device_info.Name())); - } - - // Filter Device - PushUniversalScanResult(universal_scan_result, args.IsConnectable()); - } - catch (...) - { - std::cout << "ScanResultErrorInParsing" << std::endl; } + device_agent->gatt_map.clear(); } +} - void UniversalBlePlugin::RadioStateChanged(const Radio& sender, const IInspectable&) - { - const auto radio_state = !sender ? RadioState::Disabled : sender.State(); - if (old_radio_state_ == radio_state) - { - return; - } - old_radio_state_ = radio_state; - auto state = get_availability_state_from_radio(radio_state); - - ui_thread_handler_.Post([state] - { callback_channel->OnAvailabilityChanged(static_cast(state), SuccessCallback, ErrorCallback); }); - } - - - fire_and_forget UniversalBlePlugin::ConnectAsync(uint64_t bluetooth_address) - { - BluetoothLEDevice device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(bluetooth_address); - if (!device) - { - std::cout << "ConnectionLog: ConnectionFailed: Failed to get device" << std::endl; - ui_thread_handler_.Post([bluetooth_address] - { callback_channel->OnConnectionChanged(mac_address_to_str(bluetooth_address), false, new std::string("Failed to get device"), SuccessCallback, ErrorCallback); }); - - co_return; - } - std::cout << "ConnectionLog: Device found" << std::endl; - auto services_result = co_await device.GetGattServicesAsync((BluetoothCacheMode::Uncached)); - auto services_result_error = gatt_communication_status_to_error(services_result.Status()); - if (services_result_error.has_value()) - { - std::cout << "ConnectionFailed: Failed to get services: " << services_result_error.value() << std::endl; - ui_thread_handler_.Post([bluetooth_address, services_result_error] - { callback_channel->OnConnectionChanged(mac_address_to_str(bluetooth_address), false, &services_result_error.value(), SuccessCallback, ErrorCallback); }); - co_return; - } - - std::cout << "ConnectionLog: Services discovered" << std::endl; - std::unordered_map gatt_map; - auto gatt_services = services_result.Services(); - for (GattDeviceService &&service : gatt_services) - { - GattServiceObject gatt_service; - gatt_service.obj = service; - std::string service_uuid = guid_to_uuid(service.Uuid()); - auto characteristics_result = co_await service.GetCharacteristicsAsync(BluetoothCacheMode::Uncached); - auto characteristics_result_error = gatt_communication_status_to_error(characteristics_result.Status()); - - if (characteristics_result_error.has_value()) - { - std::cout << "Failed to get characteristics for service: " << service_uuid << ", With Status: " << characteristics_result_error.value() << std::endl; - continue; - // PostConnectionUpdate(bluetoothAddress, ConnectionState::disconnected); - // co_return; - } - auto gatt_characteristics = characteristics_result.Characteristics(); - for (GattCharacteristic &&characteristic : gatt_characteristics) - { - GattCharacteristicObject gatt_characteristic; - gatt_characteristic.obj = characteristic; - gatt_characteristic.subscription_token = std::nullopt; - std::string characteristic_uuid = guid_to_uuid(characteristic.Uuid()); - gatt_service.characteristics.insert_or_assign(characteristic_uuid, std::move(gatt_characteristic)); - } - gatt_map.insert_or_assign(service_uuid, std::move(gatt_service)); - } - - event_token connection_status_changed_token = device.ConnectionStatusChanged({this, &UniversalBlePlugin::BluetoothLeDeviceConnectionStatusChanged}); - auto device_agent = std::make_unique(device, connection_status_changed_token, gatt_map); - auto pair = std::make_pair(bluetooth_address, std::move(device_agent)); - connected_devices_.insert(std::move(pair)); - std::cout << "ConnectionLog: Connected" << std::endl; - ui_thread_handler_.Post([bluetooth_address] - { callback_channel->OnConnectionChanged(mac_address_to_str(bluetooth_address), true, nullptr, SuccessCallback, ErrorCallback); }); - } - - void UniversalBlePlugin::BluetoothLeDeviceConnectionStatusChanged(const BluetoothLEDevice& sender, const IInspectable& - ) - { - if (sender.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) - { - CleanConnection(sender.BluetoothAddress()); - auto bluetooth_address = sender.BluetoothAddress(); - ui_thread_handler_.Post([bluetooth_address] - { callback_channel->OnConnectionChanged(mac_address_to_str(bluetooth_address), false, nullptr, SuccessCallback, ErrorCallback); }); - } - } - - void UniversalBlePlugin::CleanConnection(const uint64_t bluetooth_address) - { - const auto node = connected_devices_.extract(bluetooth_address); - if (!node.empty()) - { - const auto device_agent = std::move(node.mapped()); - device_agent->device.ConnectionStatusChanged(device_agent->connection_status_changed_token); - // Clean up all characteristics tokens - for (auto& [service_id, service] : device_agent->gatt_map) - { - for (auto& [char_id, characteristic] : service.characteristics) - { - if (characteristic.subscription_token.has_value()) - { - characteristic.obj.ValueChanged(characteristic.subscription_token.value()); - characteristic.subscription_token = std::nullopt; - } - } - } - device_agent->gatt_map.clear(); - } - } - - fire_and_forget UniversalBlePlugin::GetSystemDevicesAsync( - std::vector with_services, - std::function reply)> result) - { - try - { - auto selector = BluetoothLEDevice::GetDeviceSelectorFromConnectionStatus(BluetoothConnectionStatus::Connected); - DeviceInformationCollection devices = co_await DeviceInformation::FindAllAsync(selector); - auto results = flutter::EncodableList(); - for (auto &&device_info : devices) - { - try - { - BluetoothLEDevice device = co_await BluetoothLEDevice::FromIdAsync(device_info.Id()); - auto device_id = mac_address_to_str(device.BluetoothAddress()); - // Filter by services - if (!with_services.empty()) - { - auto service_result = co_await device.GetGattServicesAsync(BluetoothCacheMode::Cached); - if (service_result.Status() == GattCommunicationStatus::Success) - { - bool has_service = false; - for (auto service : service_result.Services()) - { - std::string service_uuid = to_uuidstr(service.Uuid()); - if (std::find(with_services.begin(), with_services.end(), service_uuid) != with_services.end()) - { - has_service = true; - break; - } +fire_and_forget UniversalBlePlugin::GetSystemDevicesAsync( + std::vector with_services, + std::function reply)> result) { + try { + auto selector = BluetoothLEDevice::GetDeviceSelectorFromConnectionStatus( + BluetoothConnectionStatus::Connected); + DeviceInformationCollection devices = + co_await DeviceInformation::FindAllAsync(selector); + auto results = flutter::EncodableList(); + for (auto &&device_info : devices) { + try { + BluetoothLEDevice device = + co_await BluetoothLEDevice::FromIdAsync(device_info.Id()); + auto device_id = mac_address_to_str(device.BluetoothAddress()); + // Filter by services + if (!with_services.empty()) { + auto service_result = + co_await device.GetGattServicesAsync(BluetoothCacheMode::Cached); + if (service_result.Status() == GattCommunicationStatus::Success) { + bool has_service = false; + for (auto service : service_result.Services()) { + std::string service_uuid = to_uuidstr(service.Uuid()); + if (std::find(with_services.begin(), with_services.end(), + service_uuid) != with_services.end()) { + has_service = true; + break; } - if (!has_service) - continue; } + if (!has_service) + continue; } - // Add to results, if pass all filters - auto universal_scan_result = UniversalBleScanResult(device_id); - universal_scan_result.set_name(to_string(device_info.Name())); - universal_scan_result.set_is_paired(device_info.Pairing().IsPaired()); - results.push_back(flutter::CustomEncodableValue(universal_scan_result)); - } - catch (...) - { } + // Add to results, if pass all filters + auto universal_scan_result = UniversalBleScanResult(device_id); + universal_scan_result.set_name(to_string(device_info.Name())); + universal_scan_result.set_is_paired(device_info.Pairing().IsPaired()); + results.push_back(flutter::CustomEncodableValue(universal_scan_result)); + } catch (...) { } - result(results); - } - catch (const hresult_error &err) - { - int error_code = err.code(); - std::cout << "GetConnectedDeviceLog: " << to_string(err.message()) << " ErrorCode: " << std::to_string(error_code) << std::endl; - result(FlutterError(std::to_string(error_code), to_string(err.message()))); - } - catch (...) - { - std::cout << "Unknown error GetSystemDevicesAsyncAsync" << std::endl; - result(FlutterError("Failed", "Unknown error")); } + result(results); + } catch (const hresult_error &err) { + int error_code = err.code(); + std::cout << "GetConnectedDeviceLog: " << to_string(err.message()) + << " ErrorCode: " << std::to_string(error_code) << std::endl; + result(create_flutter_error(UniversalBleErrorCode::kFailed, + to_string(err.message()), + std::to_string(error_code))); + } catch (...) { + std::cout << "Unknown error GetSystemDevicesAsyncAsync" << std::endl; + result(create_flutter_error(UniversalBleErrorCode::kUnknownError, + "Unknown error")); } +} - void UniversalBlePlugin::DiscoverServicesAsync(BluetoothDeviceAgent &bluetooth_device_agent, const std::function reply)>& result) - { - try - { - auto universal_services = flutter::EncodableList(); - for (auto & [service_id, service] : bluetooth_device_agent.gatt_map) - { - flutter::EncodableList universal_characteristics; - for (auto [char_id, characteristic] : service.characteristics) - { - auto& c = characteristic.obj; +void UniversalBlePlugin::DiscoverServicesAsync( + BluetoothDeviceAgent &bluetooth_device_agent, + const std::function reply)> &result) { + try { + auto universal_services = flutter::EncodableList(); + for (auto &[service_id, service] : bluetooth_device_agent.gatt_map) { + flutter::EncodableList universal_characteristics; + for (auto [char_id, characteristic] : service.characteristics) { + auto &c = characteristic.obj; - const auto properties_value = c.CharacteristicProperties(); - auto properties = properties_to_flutter_encodable(properties_value); + const auto properties_value = c.CharacteristicProperties(); + auto properties = properties_to_flutter_encodable(properties_value); - universal_characteristics.push_back( - flutter::CustomEncodableValue(UniversalBleCharacteristic(to_uuidstr(c.Uuid()), properties))); - } - - auto universal_ble_service = UniversalBleService(to_uuidstr(service.obj.Uuid())); - universal_ble_service.set_characteristics(universal_characteristics); - universal_services.push_back(flutter::CustomEncodableValue(universal_ble_service)); + universal_characteristics.push_back(flutter::CustomEncodableValue( + UniversalBleCharacteristic(to_uuidstr(c.Uuid()), properties))); } - result(universal_services); - } - catch (...) - { - result(FlutterError("Failed", "Unknown error")); - std::cout << "DiscoverServiceError: Unknown error" << '\n'; - } - } - fire_and_forget UniversalBlePlugin::IsPairedAsync( - const std::string& device_id, - const std::function reply)> result) - { - try - { - const auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(str_to_mac_address(device_id)); - if (device == nullptr) - { - result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); - co_return; + auto universal_ble_service = + UniversalBleService(to_uuidstr(service.obj.Uuid())); + universal_ble_service.set_characteristics(universal_characteristics); + universal_services.push_back( + flutter::CustomEncodableValue(universal_ble_service)); + } + result(universal_services); + } catch (...) { + result(create_flutter_error(UniversalBleErrorCode::kUnknownError, + "Unknown error")); + std::cout << "DiscoverServiceError: Unknown error" << '\n'; + } +} + +fire_and_forget UniversalBlePlugin::IsPairedAsync( + const std::string &device_id, + const std::function reply)> result) { + try { + const auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync( + str_to_mac_address(device_id)); + if (device == nullptr) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); + co_return; + } + const bool is_paired = device.DeviceInformation().Pairing().IsPaired(); + result(is_paired); + } catch (...) { + std::cout << "IsPairedAsync: Error " << std::endl; + result(create_flutter_error(UniversalBleErrorCode::kUnknownError, + "Unknown error")); + } +} + +fire_and_forget UniversalBlePlugin::SetNotifiableAsync( + const std::string &device_id, const std::string &service, + const std::string &characteristic, const int64_t ble_input_property, + const std::function reply)> result) { + try { + const auto it = connected_devices_.find(str_to_mac_address(device_id)); + if (it == connected_devices_.end()) { + result(create_flutter_error(UniversalBleErrorCode::kDeviceNotFound, + "Unknown devicesId:" + device_id)); + co_return; + } + + auto &gatt_char = it->second->FetchCharacteristic(service, characteristic); + + const auto properties = gatt_char.obj.CharacteristicProperties(); + auto descriptor_value = + GattClientCharacteristicConfigurationDescriptorValue::None; + if (ble_input_property == + static_cast(BleInputProperty::notification)) { + descriptor_value = + GattClientCharacteristicConfigurationDescriptorValue::Notify; + if ((properties & GattCharacteristicProperties::Notify) == + GattCharacteristicProperties::None) { + result(create_flutter_error( + UniversalBleErrorCode::kCharacteristicDoesNotSupportNotify, + "Characteristic does not support notify")); + co_return; + } + } else if (ble_input_property == + static_cast(BleInputProperty::indication)) { + descriptor_value = + GattClientCharacteristicConfigurationDescriptorValue::Indicate; + if ((properties & GattCharacteristicProperties::Indicate) == + GattCharacteristicProperties::None) { + result(create_flutter_error( + UniversalBleErrorCode::kCharacteristicDoesNotSupportIndicate, + "Characteristic does not support indicate")); + co_return; } - const bool is_paired = device.DeviceInformation().Pairing().IsPaired(); - result(is_paired); } - catch (...) - { - std::cout << "IsPairedAsync: Error " << std::endl; - result(FlutterError("Failed", "Unknown error")); + + const auto gatt_characteristic = gatt_char.obj; + const auto uuid = to_uuidstr(gatt_characteristic.Uuid()); + + // Write to the descriptor. + const auto status = + co_await gatt_characteristic + .WriteClientCharacteristicConfigurationDescriptorAsync( + descriptor_value); + if (status != GattCommunicationStatus::Success) { + result(create_flutter_error_from_gatt_communication_status(status)); + co_return; } + + // Register/UnRegister handler for the ValueChanged event. + if (descriptor_value == + GattClientCharacteristicConfigurationDescriptorValue::None) { + if (gatt_char.subscription_token.has_value()) { + gatt_characteristic.ValueChanged(gatt_char.subscription_token.value()); + gatt_char.subscription_token = std::nullopt; + std::cout << "Unsubscribed " << to_uuidstr(gatt_characteristic.Uuid()) + << std::endl; + } + } else { + // If a notification for the given characteristic is already in progress, + // swap the callbacks. + if (gatt_char.subscription_token.has_value()) { + std::cout << "A notification for the given characteristic is already " + "in progress. Swapping callbacks." + << std::endl; + gatt_characteristic.ValueChanged(gatt_char.subscription_token.value()); + gatt_char.subscription_token = std::nullopt; + } + + gatt_char.subscription_token = + std::make_optional(gatt_characteristic.ValueChanged( + {this, &UniversalBlePlugin::GattCharacteristicValueChanged})); + } + + result(std::nullopt); + } catch (const FlutterError &err) { + result(err); + } catch (...) { + std::cout << "SetNotifiableLog: Unknown error" << std::endl; + result(create_flutter_unknown_error()); } +} - fire_and_forget UniversalBlePlugin::SetNotifiableAsync(const std::string& device_id, - const std::string& service, - const std::string& characteristic, - const int64_t ble_input_property, - const std::function reply)> result) - { - try - { - const auto it = connected_devices_.find(str_to_mac_address(device_id)); - if (it == connected_devices_.end()) - { - result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); - co_return; - } - - auto& gatt_char = it->second->FetchCharacteristic(service, characteristic); - - const auto properties = gatt_char.obj.CharacteristicProperties(); - auto descriptor_value = GattClientCharacteristicConfigurationDescriptorValue::None; - if (ble_input_property == static_cast(BleInputProperty::notification)) - { - descriptor_value = GattClientCharacteristicConfigurationDescriptorValue::Notify; - if ((properties & GattCharacteristicProperties::Notify) == GattCharacteristicProperties::None) - { - result(FlutterError("NotSupported", "Characteristic does not support notify")); - co_return; - } - } - else if (ble_input_property == static_cast(BleInputProperty::indication)) - { - descriptor_value = GattClientCharacteristicConfigurationDescriptorValue::Indicate; - if ((properties & GattCharacteristicProperties::Indicate) == GattCharacteristicProperties::None) - { - result(FlutterError("NotSupported", "Characteristic does not support indicate")); - co_return; - } - } - - const auto gatt_characteristic = gatt_char.obj; - const auto uuid = to_uuidstr(gatt_characteristic.Uuid()); - - // Write to the descriptor. - const auto status = co_await gatt_characteristic.WriteClientCharacteristicConfigurationDescriptorAsync( - descriptor_value); - const auto error = gatt_communication_status_to_error(status); - if (error.has_value()) - { - result(FlutterError("Failed", error.value())); - co_return; - } - - // Register/UnRegister handler for the ValueChanged event. - if (descriptor_value == GattClientCharacteristicConfigurationDescriptorValue::None) - { - if (gatt_char.subscription_token.has_value()) - { - gatt_characteristic.ValueChanged(gatt_char.subscription_token.value()); - gatt_char.subscription_token = std::nullopt; - std::cout << "Unsubscribed " << to_uuidstr(gatt_characteristic.Uuid()) << std::endl; - } - } - else - { - // If a notification for the given characteristic is already in progress, swap the callbacks. - if (gatt_char.subscription_token.has_value()) - { - std::cout << "A notification for the given characteristic is already in progress. Swapping callbacks." << std::endl; - gatt_characteristic.ValueChanged(gatt_char.subscription_token.value()); - gatt_char.subscription_token = std::nullopt; - } - - gatt_char.subscription_token = std::make_optional(gatt_characteristic.ValueChanged({ - this, &UniversalBlePlugin::GattCharacteristicValueChanged - })); - } - - result(std::nullopt); - } - catch (const FlutterError& err) - { - result(err); - } - catch (...) - { - std::cout << "SetNotifiableLog: Unknown error" << std::endl; - result(FlutterError("Failed", "Unknown error")); - } - } - - void UniversalBlePlugin::GattCharacteristicValueChanged(const GattCharacteristic& sender, const GattValueChangedEventArgs& args) - { - auto uuid = to_uuidstr(sender.Uuid()); - auto bytes = to_bytevc(args.CharacteristicValue()); - ui_thread_handler_.Post([sender, uuid, bytes] - { callback_channel->OnValueChanged(mac_address_to_str(sender.Service().Device().BluetoothAddress()), uuid, bytes, SuccessCallback, ErrorCallback); }); - } +void UniversalBlePlugin::GattCharacteristicValueChanged( + const GattCharacteristic &sender, const GattValueChangedEventArgs &args) { + auto uuid = to_uuidstr(sender.Uuid()); + auto bytes = to_bytevc(args.CharacteristicValue()); + ui_thread_handler_.Post([sender, uuid, bytes] { + callback_channel->OnValueChanged( + mac_address_to_str(sender.Service().Device().BluetoothAddress()), uuid, + bytes, SuccessCallback, ErrorCallback); + }); +} } // namespace universal_ble \ No newline at end of file diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 327399b..b3b188b 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -61,11 +61,11 @@ namespace universal_ble { if (gatt_map.count(service_uuid) == 0) { - throw FlutterError("IllegalArgument", "Service not found"); + throw create_flutter_error(UniversalBleErrorCode::kServiceNotFound, "Service not found"); } if (gatt_map[service_uuid].characteristics.count(characteristic_uuid) == 0) { - throw FlutterError("IllegalArgument", "Characteristic not found"); + throw create_flutter_error(UniversalBleErrorCode::kCharacteristicNotFound, "Characteristic not found"); } return gatt_map[service_uuid].characteristics.at(characteristic_uuid); }