From 7dc22c58a55d3b747d680a89d5f7a0b61491aa60 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Tue, 17 Sep 2024 03:13:51 +0530 Subject: [PATCH] Improve scan filter api (#78) * Improve scan filter api * Implement Windows filters * Add manufacturerDataList and deprecate manufacturerData * Fix Windows * Update Readme and changelog * Remove import * Fix Linux * Update readme and changelog * Cleanup Todo * Improve ManufacturerData * Rename webConfig and Manufacturer.data * Return false for receivesAdvertisements on Linux/Web * Add delay in discoverServices on Linux * Remove import * Update changelog --------- Co-authored-by: fotidim --- .vscode/settings.json | 2 - CHANGELOG.md | 12 +- README.md | 22 +- .../navideck/universal_ble/UniversalBle.g.kt | 55 +++- .../universal_ble/UniversalBleFilterUtil.kt | 164 +++++++++ .../universal_ble/UniversalBleHelper.kt | 77 +---- .../universal_ble/UniversalBlePlugin.kt | 62 +++- darwin/Classes/UniversalBle.g.swift | 52 ++- darwin/Classes/UniversalBleFilterUtil.swift | 107 ++++++ darwin/Classes/UniversalBleHelper.swift | 39 +-- darwin/Classes/UniversalBlePlugin.swift | 72 ++-- example/lib/data/mock_universal_ble.dart | 2 +- example/lib/home/home.dart | 26 +- .../lib/home/widgets/scanned_item_widget.dart | 11 +- .../peripheral_detail_page.dart | 4 +- example/pubspec.lock | 6 +- lib/src/models/ble_device.dart | 46 +-- lib/src/models/manufacturer_data.dart | 43 +++ lib/src/models/model_exports.dart | 1 + lib/src/models/platform_config.dart | 8 +- lib/src/models/scan_filter.dart | 10 +- lib/src/universal_ble_filter_util.dart | 98 ++++++ .../universal_ble_linux.dart | 284 +++++++--------- .../universal_ble_pigeon/universal_ble.g.dart | 311 ++++++++---------- .../universal_ble_pigeon_channel.dart | 18 +- lib/src/universal_ble_platform_interface.dart | 12 +- .../universal_ble_web/universal_ble_web.dart | 51 ++- pigeon/universal_ble.dart | 22 +- pubspec.yaml | 2 +- test/scan_filter_test.dart | 198 +++++++++++ windows/CMakeLists.txt | 2 + windows/src/generated/universal_ble.g.cpp | 137 +++++--- windows/src/generated/universal_ble.g.h | 57 +++- windows/src/helper/utils.cpp | 2 +- windows/src/universal_ble_filter_util.cpp | 158 +++++++++ windows/src/universal_ble_filter_util.h | 15 + windows/src/universal_ble_plugin.cpp | 177 +++------- windows/src/universal_ble_plugin.h | 1 - 38 files changed, 1516 insertions(+), 850 deletions(-) create mode 100644 android/src/main/kotlin/com/navideck/universal_ble/UniversalBleFilterUtil.kt create mode 100644 darwin/Classes/UniversalBleFilterUtil.swift create mode 100644 lib/src/models/manufacturer_data.dart create mode 100644 lib/src/universal_ble_filter_util.dart create mode 100644 test/scan_filter_test.dart create mode 100644 windows/src/universal_ble_filter_util.cpp create mode 100644 windows/src/universal_ble_filter_util.h diff --git a/.vscode/settings.json b/.vscode/settings.json index 713d462..c6bb3ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,6 @@ "connnection", "Cupertino", "DFACE", - "DSIMPLEBLE", "HFONT", "HINSTANCE", "HMENU", @@ -45,7 +44,6 @@ "rssi", "SETFONT", "SETTEXT", - "simpleble", "subdata", "sublist", "THICKFRAME", diff --git a/CHANGELOG.md b/CHANGELOG.md index 2afb280..73806ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ -## 0.12.1 +## 0.13.0 +* BREAKING CHANGE: `scanFilter` filters are now in OR relation +* BREAKING CHANGE: `manufacturerDataHead` is removed from `BleDevice` +* BREAKING CHANGE: rename `WebConfig` to `WebOptions` +* BREAKING CHANGE: rename `ManufacturerDataFilter.data` to `ManufacturerDataFilter.payload` +* Deprecation: `manufacturerData` is deprecated in BleDevice and will be removed in the future +* Improve `scanFilter` handling +* Use `ManufacturerData` object instead of `Uint8List` for manufacturerData +* Add `manufacturerDataList` as `List` in `BleDevice` * Auto convert all services passed to `getSystemDevices()` +* Return false for receivesAdvertisements on Linux/Web +* Add 1s delay in discoverServices on Linux ## 0.12.0 * BREAKING CHANGE: `unPair` is now `unpair` diff --git a/README.md b/README.md index 677d632..549a246 100644 --- a/README.md +++ b/README.md @@ -114,16 +114,18 @@ You still need to explicitly [connect](#connecting) to them before being able to #### Scan Filter -You can optionally set filters when scanning. +You can optionally set a filter when scanning. A filter can have multiple conditions (services, manufacturerData, namePrefix) and all conditions are in `OR` relation, returning results that match any of the given conditions. ##### With Services -When setting this parameter, the scan results will only include devices that advertise any of the specified services. This is the primary filter. All devices are first filtered by services, then further filtered by other criteria. This parameter is mandatory on [web](#web) if you want to access those services. +When setting this parameter, the scan results will only include devices that advertise any of the specified services. ```dart List withServices; ``` +Note: On web **you have to** specify services before you are able to use them. See the [web](#web) section for more details. + ##### With ManufacturerData Use the `withManufacturerData` parameter to filter devices by manufacturer data. When you pass a list of `ManufacturerDataFilter` objects to this parameter, the scan results will only include devices that contain any of the specified manufacturer data. @@ -335,11 +337,23 @@ When publishing on Windows you need to declare the following [capabilities](http ### Web -On web, the `withServices` parameter in the ScanFilter is used as [optional_services](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#optionalservices) as well as a services filter. On web you have to set this parameter to ensure that you can access the specified services after connecting to the device. You can leave it empty for the rest of the platforms if your device does not advertise services. +On web, the `withServices` parameter in the ScanFilter is used as [optional_services](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#optionalservices) as well as a services filter. You have to set this parameter to ensure that you can access the specified services after connecting to the device. You can leave it empty for the rest of the platforms if your device does not advertise services. ```dart ScanFilter( - withServices: kIsWeb ? ["SERVICE_UUID"] : [], + withServices: kIsWeb ? ["SERVICE_UUID"] : [], +) +``` + +If you don't want to apply any filter for these services but still want to access them, after connection, use `PlatformConfig`. + +```dart +UniversalBle.startScan( + platformConfig: PlatformConfig( + web: WebOptions( + optionalServices: ["SERVICE_UUID"] + ) + ) ) ``` 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 70cb15a..aa93c1a 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 @@ -53,8 +53,7 @@ data class UniversalBleScanResult ( val name: String? = null, val isPaired: Boolean? = null, val rssi: Long? = null, - val manufacturerData: ByteArray? = null, - val manufacturerDataHead: ByteArray? = null, + val manufacturerDataList: List? = null, val services: List? = null ) { @@ -65,10 +64,9 @@ data class UniversalBleScanResult ( val name = __pigeon_list[1] as String? val isPaired = __pigeon_list[2] as Boolean? val rssi = __pigeon_list[3].let { num -> if (num is Int) num.toLong() else num as Long? } - val manufacturerData = __pigeon_list[4] as ByteArray? - val manufacturerDataHead = __pigeon_list[5] as ByteArray? - val services = __pigeon_list[6] as List? - return UniversalBleScanResult(deviceId, name, isPaired, rssi, manufacturerData, manufacturerDataHead, services) + val manufacturerDataList = __pigeon_list[4] as List? + val services = __pigeon_list[5] as List? + return UniversalBleScanResult(deviceId, name, isPaired, rssi, manufacturerDataList, services) } } fun toList(): List { @@ -77,8 +75,7 @@ data class UniversalBleScanResult ( name, isPaired, rssi, - manufacturerData, - manufacturerDataHead, + manufacturerDataList, services, ) } @@ -135,6 +132,7 @@ data class UniversalBleCharacteristic ( */ data class UniversalScanFilter ( val withServices: List, + val withNamePrefix: List, val withManufacturerData: List ) { @@ -142,13 +140,15 @@ data class UniversalScanFilter ( @Suppress("LocalVariableName") fun fromList(__pigeon_list: List): UniversalScanFilter { val withServices = __pigeon_list[0] as List - val withManufacturerData = __pigeon_list[1] as List - return UniversalScanFilter(withServices, withManufacturerData) + val withNamePrefix = __pigeon_list[1] as List + val withManufacturerData = __pigeon_list[2] as List + return UniversalScanFilter(withServices, withNamePrefix, withManufacturerData) } } fun toList(): List { return listOf( withServices, + withNamePrefix, withManufacturerData, ) } @@ -156,7 +156,7 @@ data class UniversalScanFilter ( /** Generated class from Pigeon that represents data sent in messages. */ data class UniversalManufacturerDataFilter ( - val companyIdentifier: Long? = null, + val companyIdentifier: Long, val data: ByteArray? = null, val mask: ByteArray? = null @@ -164,7 +164,7 @@ data class UniversalManufacturerDataFilter ( companion object { @Suppress("LocalVariableName") fun fromList(__pigeon_list: List): UniversalManufacturerDataFilter { - val companyIdentifier = __pigeon_list[0].let { num -> if (num is Int) num.toLong() else num as Long? } + val companyIdentifier = __pigeon_list[0].let { num -> if (num is Int) num.toLong() else num as Long } val data = __pigeon_list[1] as ByteArray? val mask = __pigeon_list[2] as ByteArray? return UniversalManufacturerDataFilter(companyIdentifier, data, mask) @@ -178,6 +178,28 @@ data class UniversalManufacturerDataFilter ( ) } } + +/** Generated class from Pigeon that represents data sent in messages. */ +data class UniversalManufacturerData ( + val companyIdentifier: Long, + val data: ByteArray + +) { + companion object { + @Suppress("LocalVariableName") + fun fromList(__pigeon_list: List): UniversalManufacturerData { + val companyIdentifier = __pigeon_list[0].let { num -> if (num is Int) num.toLong() else num as Long } + val data = __pigeon_list[1] as ByteArray + return UniversalManufacturerData(companyIdentifier, data) + } + } + fun toList(): List { + return listOf( + companyIdentifier, + data, + ) + } +} private object UniversalBlePigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -206,6 +228,11 @@ private object UniversalBlePigeonCodec : StandardMessageCodec() { UniversalManufacturerDataFilter.fromList(it) } } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalManufacturerData.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -231,6 +258,10 @@ private object UniversalBlePigeonCodec : StandardMessageCodec() { stream.write(133) writeValue(stream, value.toList()) } + is UniversalManufacturerData -> { + stream.write(134) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleFilterUtil.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleFilterUtil.kt new file mode 100644 index 0000000..7e99242 --- /dev/null +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleFilterUtil.kt @@ -0,0 +1,164 @@ +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 + +private const val TAG = "UniversalBlePlugin" + +@SuppressLint("MissingPermission") +class UniversalBleFilterUtil { + var scanFilter: UniversalScanFilter? = null + var serviceFilterUUIDS: List = emptyList() + + fun filterDevice( + name: String?, + manufacturerDataList: List, + serviceUuids: Array, + ): Boolean { + val filter = scanFilter ?: return true + + val hasNamePrefixFilter = filter.withNamePrefix.isNotEmpty() + val hasServiceFilter = filter.withServices.isNotEmpty() + val hasManufacturerDataFilter = filter.withManufacturerData.isNotEmpty() + + // If there is no filter at all, then allow device + if (!hasNamePrefixFilter && + !hasServiceFilter && + !hasManufacturerDataFilter + ) { + return true + } + + // For not, we only have DeviceName filter + return hasNamePrefixFilter && isNameMatchingFilters(filter, name) || + hasServiceFilter && isServicesMatchingFilters(serviceUuids) || + hasManufacturerDataFilter && isManufacturerDataMatchingFilters( + filter, + manufacturerDataList + ) + } + + private fun isNameMatchingFilters(scanFilter: UniversalScanFilter, name: String?): Boolean { + val namePrefixFilter = scanFilter.withNamePrefix.filterNotNull() + if (namePrefixFilter.isEmpty()) { + return true + } + if (name.isNullOrEmpty()) { + return false + } + return namePrefixFilter.any { name.startsWith(it) } + } + + private fun isServicesMatchingFilters( + serviceUuids: Array, + ): Boolean { + if (serviceFilterUUIDS.isEmpty()) { + return true + } + if (serviceUuids.isEmpty()) { + return false + } + return serviceFilterUUIDS.any { + serviceUuids.contains(it) + } + } + + private fun isManufacturerDataMatchingFilters( + scanFilter: UniversalScanFilter, + manufacturerDataList: List, + ): Boolean { + val filters = scanFilter.withManufacturerData.filterNotNull() + if (filters.isEmpty()) return true + if (manufacturerDataList.isEmpty()) return false + return manufacturerDataList.any { mfd -> + filters.any { filter -> + mfd.companyIdentifier == filter.companyIdentifier && + isDataMatching(filter.data, mfd.data, filter.mask) + } + } + } + + private fun isDataMatching( + filterData: ByteArray?, + deviceData: ByteArray, + filterMask: ByteArray?, + ): Boolean { + if (filterData == null) return true + if (filterData.size > deviceData.size) return false + + val mask = filterMask ?: ByteArray(filterData.size) { 0xFF.toByte() } + if (filterData.size != mask.size) return false + + return filterData.indices.all { i -> + (filterData[i] and mask[i]) == (deviceData[i] and mask[i]) + } + } +} + +fun UniversalScanFilter.hasCustomFilter(): Boolean { + // Only NamePrefix Filtering is not allowed in native filters + return withNamePrefix.isNotEmpty() +} + +// Convert UniversalScanFilter to ScanFilter +fun UniversalScanFilter.toScanFilters(serviceUuids: List): List { + val scanFilters: ArrayList = arrayListOf() + + // Add withServices Filter + for (service in serviceUuids) { + try { + service.let { + scanFilters.add( + ScanFilter.Builder().setServiceUuid(ParcelUuid(it)).build() + ) + Log.e(TAG, "scanFilters: $it") + } + } catch (e: Exception) { + Log.e(TAG, e.toString()) + throw FlutterError( + "illegalIllegalArgument", + "Invalid serviceId: $service", + e.toString() + ) + } + } + + // Add ManufacturerData Filter + for (manufacturerData in this.withManufacturerData) { + try { + manufacturerData?.companyIdentifier?.let { + val data: ByteArray = manufacturerData.data ?: ByteArray(0) + val mask: ByteArray? = manufacturerData.mask + if (mask == null) { + scanFilters.add( + ScanFilter.Builder().setManufacturerData( + it.toInt(), data + ).build() + ) + } else { + scanFilters.add( + ScanFilter.Builder().setManufacturerData( + it.toInt(), data, mask + ).build() + ) + } + } + } catch (e: Exception) { + Log.e(TAG, e.toString()) + throw FlutterError( + "illegalIllegalArgument", + "Invalid manufacturerData: ${manufacturerData?.companyIdentifier} ${manufacturerData?.data} ${manufacturerData?.mask}", + e.toString() + ) + } + } + + return scanFilters.toList() +} diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt index 58f3f3b..a0717c8 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -18,6 +18,8 @@ import android.bluetooth.le.ScanResult import android.os.Build import android.os.ParcelUuid import android.util.Log +import android.util.SparseArray +import androidx.core.util.keyIterator import java.nio.ByteBuffer import java.nio.ByteOrder import java.util.UUID @@ -85,6 +87,10 @@ fun String.validFullUUID(): String { } } +fun List.toUUIDList(): List { + return this.map { UUID.fromString(it.validFullUUID()) } +} + fun String.toBluetoothGatt(): BluetoothGatt { return knownGatts.find { it.device.address == this } ?: throw FlutterError("IllegalArgument", "Unknown deviceId: $this", null) @@ -156,70 +162,19 @@ fun Int.parseGattErrorCode(): String? { } } -fun UniversalScanFilter.toScanFilters(): List { - val scanFilters: ArrayList = arrayListOf() - // Add withServices Filter - for (service in this.withServices) { - try { - val serviceUUID = service?.validFullUUID() - serviceUUID?.let { - val parcelUUId = ParcelUuid.fromString(it) - scanFilters.add( - ScanFilter.Builder().setServiceUuid(parcelUUId).build() - ) - Log.e(TAG, "scanFilters: $parcelUUId") - } - } catch (e: Exception) { - Log.e(TAG, e.toString()) - throw FlutterError( - "illegalIllegalArgument", - "Invalid serviceId: $service", - e.toString() - ) - } - } - - // Add ManufacturerData Filter - for (manufacturerData in this.withManufacturerData) { - try { - manufacturerData?.companyIdentifier?.let { - val data: ByteArray = manufacturerData.data ?: ByteArray(0) - val mask: ByteArray? = manufacturerData.mask - if (mask == null) { - scanFilters.add( - ScanFilter.Builder().setManufacturerData( - it.toInt(), data - ).build() - ) - } else { - scanFilters.add( - ScanFilter.Builder().setManufacturerData( - it.toInt(), data, mask - ).build() - ) - } - } - } catch (e: Exception) { - Log.e(TAG, e.toString()) - throw FlutterError( - "illegalIllegalArgument", - "Invalid manufacturerData: ${manufacturerData?.companyIdentifier} ${manufacturerData?.data} ${manufacturerData?.mask}", - e.toString() - ) - } - } - - return scanFilters.toList() -} - -val ScanResult.manufacturerDataHead: ByteArray? +val ScanResult.manufacturerDataList: List get() { - val sparseArray = scanRecord?.manufacturerSpecificData ?: return null - if (sparseArray.size() == 0) return null - - return sparseArray.keyAt(0).toShort().toByteArray() + sparseArray.valueAt(0) + return scanRecord?.manufacturerSpecificData?.toList()?.map { (key, value) -> + UniversalManufacturerData(key.toLong(), value) + } ?: emptyList() } +fun SparseArray.toList(): List> { + return (0 until size()).map { index -> + keyAt(index) to valueAt(index) + } +} + fun BluetoothGatt.getCharacteristic( service: String, characteristic: String, diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index d4a8be7..cc3a977 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -21,6 +21,7 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.* +import java.util.UUID import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -38,6 +39,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), private lateinit var bluetoothManager: BluetoothManager private val cachedServicesMap = mutableMapOf>() private val devicesStateMap = mutableMapOf() + private val universalBleFilterUtil = UniversalBleFilterUtil() // Flutter Futures private var bluetoothEnableRequestFuture: ((Result) -> Unit)? = null @@ -114,11 +116,32 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } val settings = builder.build() - bluetoothManager.adapter.bluetoothLeScanner?.startScan( - filter?.toScanFilters() ?: emptyList(), - settings, - scanCallback - ) + val hasCustomFilters = filter?.hasCustomFilter() ?: false; + + try { + val filterServices = filter?.withServices?.filterNotNull()?.toUUIDList() ?: emptyList() + var scanFilters = emptyList() + + // Set custom scan filter only if required + if (hasCustomFilters) { + Log.e(TAG, "Using Custom Filters") + universalBleFilterUtil.scanFilter = filter + universalBleFilterUtil.serviceFilterUUIDS = filterServices + } else { + universalBleFilterUtil.scanFilter = null + scanFilters = filter?.toScanFilters(filterServices) ?: emptyList() + } + + bluetoothManager.adapter.bluetoothLeScanner?.startScan( + scanFilters, settings, scanCallback + ) + } catch (e: Exception) { + throw FlutterError( + "illegalIllegalArgument", + "Failed to start Scan", + e.toString() + ) + } } override fun stopScan() { @@ -573,8 +596,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), name = it.name, deviceId = it.address, isPaired = it.bondState == BOND_BONDED, - manufacturerDataHead = null, - manufacturerData = null, + manufacturerDataList = null, rssi = null, ) } @@ -790,31 +812,45 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } + private val scanCallback = object : ScanCallback() { override fun onScanFailed(errorCode: Int) { Log.e(TAG, "OnScanFailed: ${errorCode.parseScanErrorMessage()}") } override fun onScanResult(callbackType: Int, result: ScanResult) { + // Log.v(TAG, "onScanResult: $result") - var serviceUuids: Array = arrayOf() + var serviceUuids: Array = arrayOf() result.device.uuids?.forEach { - serviceUuids += it.uuid.toString() + serviceUuids += it.uuid } result.scanRecord?.serviceUuids?.forEach { - if (!serviceUuids.contains(it.uuid.toString())) { - serviceUuids += it.uuid.toString() + if (!serviceUuids.contains(it.uuid)) { + serviceUuids += it.uuid } } + + val name = result.device.name + val manufacturerDataList = result.manufacturerDataList + + if (!universalBleFilterUtil.filterDevice( + name, + manufacturerDataList, + serviceUuids + ) + ) return + + mainThreadHandler?.post { callbackChannel?.onScanResult( UniversalBleScanResult( name = result.device.name, deviceId = result.device.address, isPaired = result.device.bondState == BOND_BONDED, - manufacturerDataHead = result.manufacturerDataHead, + manufacturerDataList = manufacturerDataList, rssi = result.rssi.toLong(), - services = serviceUuids.toList() + services = serviceUuids.map { it.toString() }.toList() ) ) {} } diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index c2db1f0..6f134e7 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -74,8 +74,7 @@ struct UniversalBleScanResult { var name: String? = nil var isPaired: Bool? = nil var rssi: Int64? = nil - var manufacturerData: FlutterStandardTypedData? = nil - var manufacturerDataHead: FlutterStandardTypedData? = nil + var manufacturerDataList: [UniversalManufacturerData?]? = nil var services: [String?]? = nil // swift-format-ignore: AlwaysUseLowerCamelCase @@ -84,17 +83,15 @@ struct UniversalBleScanResult { let name: String? = nilOrValue(__pigeon_list[1]) let isPaired: Bool? = nilOrValue(__pigeon_list[2]) let rssi: Int64? = isNullish(__pigeon_list[3]) ? nil : (__pigeon_list[3] is Int64? ? __pigeon_list[3] as! Int64? : Int64(__pigeon_list[3] as! Int32)) - let manufacturerData: FlutterStandardTypedData? = nilOrValue(__pigeon_list[4]) - let manufacturerDataHead: FlutterStandardTypedData? = nilOrValue(__pigeon_list[5]) - let services: [String?]? = nilOrValue(__pigeon_list[6]) + let manufacturerDataList: [UniversalManufacturerData?]? = nilOrValue(__pigeon_list[4]) + let services: [String?]? = nilOrValue(__pigeon_list[5]) return UniversalBleScanResult( deviceId: deviceId, name: name, isPaired: isPaired, rssi: rssi, - manufacturerData: manufacturerData, - manufacturerDataHead: manufacturerDataHead, + manufacturerDataList: manufacturerDataList, services: services ) } @@ -104,8 +101,7 @@ struct UniversalBleScanResult { name, isPaired, rssi, - manufacturerData, - manufacturerDataHead, + manufacturerDataList, services, ] } @@ -162,21 +158,25 @@ struct UniversalBleCharacteristic { /// Generated class from Pigeon that represents data sent in messages. struct UniversalScanFilter { var withServices: [String?] + var withNamePrefix: [String?] var withManufacturerData: [UniversalManufacturerDataFilter?] // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ __pigeon_list: [Any?]) -> UniversalScanFilter? { let withServices = __pigeon_list[0] as! [String?] - let withManufacturerData = __pigeon_list[1] as! [UniversalManufacturerDataFilter?] + let withNamePrefix = __pigeon_list[1] as! [String?] + let withManufacturerData = __pigeon_list[2] as! [UniversalManufacturerDataFilter?] return UniversalScanFilter( withServices: withServices, + withNamePrefix: withNamePrefix, withManufacturerData: withManufacturerData ) } func toList() -> [Any?] { return [ withServices, + withNamePrefix, withManufacturerData, ] } @@ -184,13 +184,13 @@ struct UniversalScanFilter { /// Generated class from Pigeon that represents data sent in messages. struct UniversalManufacturerDataFilter { - var companyIdentifier: Int64? = nil + var companyIdentifier: Int64 var data: FlutterStandardTypedData? = nil var mask: FlutterStandardTypedData? = nil // swift-format-ignore: AlwaysUseLowerCamelCase static func fromList(_ __pigeon_list: [Any?]) -> UniversalManufacturerDataFilter? { - let companyIdentifier: Int64? = isNullish(__pigeon_list[0]) ? nil : (__pigeon_list[0] is Int64? ? __pigeon_list[0] as! Int64? : Int64(__pigeon_list[0] as! Int32)) + let companyIdentifier = __pigeon_list[0] is Int64 ? __pigeon_list[0] as! Int64 : Int64(__pigeon_list[0] as! Int32) let data: FlutterStandardTypedData? = nilOrValue(__pigeon_list[1]) let mask: FlutterStandardTypedData? = nilOrValue(__pigeon_list[2]) @@ -208,6 +208,29 @@ struct UniversalManufacturerDataFilter { ] } } + +/// Generated class from Pigeon that represents data sent in messages. +struct UniversalManufacturerData { + var companyIdentifier: Int64 + var data: FlutterStandardTypedData + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ __pigeon_list: [Any?]) -> UniversalManufacturerData? { + let companyIdentifier = __pigeon_list[0] is Int64 ? __pigeon_list[0] as! Int64 : Int64(__pigeon_list[0] as! Int32) + let data = __pigeon_list[1] as! FlutterStandardTypedData + + return UniversalManufacturerData( + companyIdentifier: companyIdentifier, + data: data + ) + } + func toList() -> [Any?] { + return [ + companyIdentifier, + data, + ] + } +} private class UniversalBlePigeonCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { @@ -221,6 +244,8 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader { return UniversalScanFilter.fromList(self.readValue() as! [Any?]) case 133: return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + case 134: + return UniversalManufacturerData.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } @@ -244,6 +269,9 @@ private class UniversalBlePigeonCodecWriter: FlutterStandardWriter { } else if let value = value as? UniversalManufacturerDataFilter { super.writeByte(133) super.writeValue(value.toList()) + } else if let value = value as? UniversalManufacturerData { + super.writeByte(134) + super.writeValue(value.toList()) } else { super.writeValue(value) } diff --git a/darwin/Classes/UniversalBleFilterUtil.swift b/darwin/Classes/UniversalBleFilterUtil.swift new file mode 100644 index 0000000..9c2e8e8 --- /dev/null +++ b/darwin/Classes/UniversalBleFilterUtil.swift @@ -0,0 +1,107 @@ +// +// UniversalBleFilterUtil.swift +// universal_ble +// +// Created by Rohit Sangwan on 23/08/24. +// + +import CoreBluetooth +import Foundation + +public class UniversalBleFilterUtil { + var scanFilter: UniversalScanFilter? + var scanFilterServicesUUID: [CBUUID] = [] + + func filterDevice(name: String?, manufacturerData: UniversalManufacturerData?, services: [CBUUID]?) -> Bool { + guard let filter = scanFilter else { + return true + } + + let hasNamePrefixFilter = !filter.withNamePrefix.isEmpty + let hasServiceFilter = !filter.withServices.isEmpty + let hasManufacturerDataFilter = !filter.withManufacturerData.isEmpty + + // If there is no filter at all, then allow device + if !hasNamePrefixFilter && !hasServiceFilter && !hasManufacturerDataFilter { + return true + } + + // Else check one of the filter passes + return hasNamePrefixFilter && isNameMatchingFilters(filter: filter, name: name) || + hasServiceFilter && isServicesMatchingFilters(services: services) || + hasManufacturerDataFilter && isManufacturerDataMatchingFilters(scanFilter: filter, msd: manufacturerData) + } + + func isNameMatchingFilters(filter: UniversalScanFilter, name: String?) -> Bool { + let prefixFilters = filter.withNamePrefix.compactMap { $0 }.filter { !$0.isEmpty } + + guard !prefixFilters.isEmpty else { + return true + } + + guard let name = name, !name.isEmpty else { + return false + } + + return prefixFilters.contains { name.hasPrefix($0) } + } + + func isServicesMatchingFilters(services: [CBUUID]?) -> Bool { + let serviceFilters = Set(scanFilterServicesUUID.compactMap { $0 }) + + guard !serviceFilters.isEmpty else { + return true + } + + guard let services = services, !services.isEmpty else { + return false + } + + return !Set(services).isDisjoint(with: serviceFilters) + } + + func isManufacturerDataMatchingFilters(scanFilter: UniversalScanFilter, msd: UniversalManufacturerData?) -> Bool { + let filters = scanFilter.withManufacturerData.compactMap { $0 } + if filters.isEmpty { + return true + } + + guard let msd = msd else { + return false + } + + for filter in filters { + let companyIdentifier: Int64 = filter.companyIdentifier + if msd.companyIdentifier == companyIdentifier && findData(find: filter.data?.toData(), inData: msd.data.toData(), usingMask: filter.mask?.toData()) { + return true + } + } + return false + } + + func findData(find: Data?, inData data: Data, usingMask mask: Data?) -> Bool { + if let find = find { + // If mask is null, use a default mask of all 1s + let mask = mask ?? Data(repeating: 0xFF, count: find.count) + + // Ensure find & mask are same length + guard find.count == mask.count else { + return false + } + + for i in 0 ..< find.count { + // Perform bitwise AND with mask and then compare + if (find[i] & mask[i]) != (data[i] & mask[i]) { + return false + } + } + } + return true + } +} + +extension UniversalScanFilter { + var hasCustomFilters: Bool { + return !withManufacturerData.isEmpty || !withNamePrefix.isEmpty + } +} diff --git a/darwin/Classes/UniversalBleHelper.swift b/darwin/Classes/UniversalBleHelper.swift index d10d282..9090f97 100644 --- a/darwin/Classes/UniversalBleHelper.swift +++ b/darwin/Classes/UniversalBleHelper.swift @@ -150,7 +150,7 @@ extension String { let baseUuid = "00000000-0000-1000-8000-00805F9B34FB" let start = baseUuid.startIndex let range = baseUuid.index(start, offsetBy: 4 - uuidLength) ..< baseUuid.index(start, offsetBy: 4) - return baseUuid.replacingCharacters(in: range, with: self) + return baseUuid.replacingCharacters(in: range, with: self).lowercased() } else { return self } @@ -163,43 +163,6 @@ extension FlutterStandardTypedData { } } -func isManufacturerDataMatchingFilters(filters: [UniversalManufacturerDataFilter], msd: Data?) -> Bool { - guard let msd = msd, !msd.isEmpty else { - return false - } - for filter in filters { - guard let companyIdentifier: Int64 = filter.companyIdentifier else { - continue - } - let manufacturerId = msd.subdata(in: 0 ..< 2).withUnsafeBytes { $0.load(as: UInt16.self) } - let manufacturerData = msd.subdata(in: 2 ..< msd.count) - if manufacturerId == companyIdentifier && findData(find: filter.data?.toData(), inData: manufacturerData, usingMask: filter.mask?.toData()) { - return true - } - } - return false -} - -func findData(find: Data?, inData data: Data, usingMask mask: Data?) -> Bool { - if let find = find { - // If mask is null, use a default mask of all 1s - let mask = mask ?? Data(repeating: 0xFF, count: find.count) - - // Ensure find & mask are same length - guard find.count == mask.count else { - return false - } - - for i in 0 ..< find.count { - // Perform bitwise AND with mask and then compare - if (find[i] & mask[i]) != (data[i] & mask[i]) { - return false - } - } - } - return true -} - // Future classes class CharacteristicReadFuture { let deviceId: String diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index a827af2..e2f41d8 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -26,8 +26,8 @@ private var discoveredPeripherals = [String: CBPeripheral]() private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentralManagerDelegate, CBPeripheralDelegate { var callbackChannel: UniversalBleCallbackChannel + private var universalBleFilterUtil = UniversalBleFilterUtil() private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil) - private var scanFilter: UniversalScanFilter? = nil private var discoveredServicesProgressMap: [String: [UniversalBleService]] = [:] private var characteristicReadFutures = [CharacteristicReadFuture]() private var characteristicWriteFutures = [CharacteristicWriteFuture]() @@ -59,19 +59,21 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } func startScan(filter: UniversalScanFilter?) throws { - // Apply services filter - var withServices: [CBUUID] = [] - for service in filter?.withServices ?? [] { - if let service = service { - if UUID(uuidString: service.validFullUUID) == nil { - throw FlutterError(code: "IllegalArgument", message: "Invalid service UUID:\(service)", details: nil) - } - withServices.append(CBUUID(string: service)) - } - } + // If filter have any other filter other then official one + let hasCustomFilter = filter?.hasCustomFilters ?? false - // Save scanFilter for later user - scanFilter = filter + // Apply services filter + var withServices: [CBUUID] = try filter?.withServices.compactMap { $0 }.toCBUUID() ?? [] + + if hasCustomFilter { + print("Using Custom Filters") + universalBleFilterUtil.scanFilter = filter + universalBleFilterUtil.scanFilterServicesUUID = withServices + withServices = [] + } else { + universalBleFilterUtil.scanFilter = nil + universalBleFilterUtil.scanFilterServicesUUID = [] + } let options = [CBCentralManagerScanOptionAllowDuplicatesKey: true] manager.scanForPeripherals(withServices: withServices, options: options) @@ -280,7 +282,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral completion(Result.failure(FlutterError(code: "NotSupported", message: nil, details: nil))) } - func pair(deviceId _: String, completion: @escaping (Result) -> Void){ + func pair(deviceId _: String, completion: @escaping (Result) -> Void) { completion(Result.failure(FlutterError(code: "Implemented in Dart", message: nil, details: nil))) } @@ -310,17 +312,26 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } public func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { + // Store the discovered peripheral using its UUID as the key discoveredPeripherals[peripheral.uuid.uuidString] = peripheral - let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data - let services = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] - // Handle ScanFilters - if let filter = scanFilter { - let manufacturerFilter = filter.withManufacturerData.compactMap { $0 } - // If scan filters are not empty, check if manufacturer data matches filters - if !manufacturerFilter.isEmpty, !isManufacturerDataMatchingFilters(filters: manufacturerFilter, msd: manufacturerData) { - return - } + // Extract manufacturer data and service UUIDs from the advertisement data + let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data + let services = (advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]) + + var manufacturerDataList: [UniversalManufacturerData] = [] + var universalManufacturerData: UniversalManufacturerData? = nil + + if let msd = manufacturerData, msd.count > 2 { + let companyIdentifier = msd.prefix(2).withUnsafeBytes { $0.load(as: UInt16.self) } + let data = FlutterStandardTypedData(bytes: msd.suffix(from: 2)) + universalManufacturerData = UniversalManufacturerData(companyIdentifier: Int64(companyIdentifier), data: data) + manufacturerDataList.append(universalManufacturerData!) + } + + // Apply custom filters and return early if the peripheral doesn't match + if !universalBleFilterUtil.filterDevice(name: peripheral.name, manufacturerData: universalManufacturerData, services: services) { + return } callbackChannel.onScanResult(result: UniversalBleScanResult( @@ -328,8 +339,8 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral name: peripheral.name, isPaired: nil, rssi: RSSI as? Int64, - manufacturerData: FlutterStandardTypedData(bytes: manufacturerData ?? Data()), - services: services?.map { $0.uuidStr } + manufacturerDataList: manufacturerDataList, + services: services?.map { $0.uuidStr.validFullUUID } )) { _ in } } @@ -451,4 +462,15 @@ extension String { } } +extension [String] { + func toCBUUID() throws -> [CBUUID] { + return try compactMap { serviceUUID in + guard UUID(uuidString: serviceUUID.validFullUUID) != nil else { + throw FlutterError(code: "IllegalArgument", message: "Invalid service UUID:\(serviceUUID)", details: nil) + } + return CBUUID(string: serviceUUID) + } + } +} + extension FlutterError: Error {} diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index f0a8c6f..b08b493 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -9,7 +9,7 @@ class MockUniversalBle extends UniversalBlePlatform { name: 'MockDevice', deviceId: 'MockDeviceId', rssi: 50, - manufacturerData: Uint8List(0), + manufacturerDataList: [], ); Uint8List _serviceValue = utf8.encode('Result'); diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart index a423eca..95c1410 100644 --- a/example/lib/home/home.dart +++ b/example/lib/home/home.dart @@ -1,5 +1,3 @@ -// ignore_for_file: use_build_context_synchronously - import 'package:flutter/material.dart'; import 'package:universal_ble/universal_ble.dart'; import 'package:universal_ble_example/data/mock_universal_ble.dart'; @@ -92,6 +90,12 @@ class _MyAppState extends State { ); } + void showSnackbar(message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message.toString())), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -130,9 +134,7 @@ class _MyAppState extends State { setState(() { _isScanning = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.toString())), - ); + showSnackbar(e); } }, ), @@ -151,9 +153,7 @@ class _MyAppState extends State { text: 'Enable Bluetooth', onPressed: () async { bool isEnabled = await UniversalBle.enableBluetooth(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("BluetoothEnabled: $isEnabled")), - ); + showSnackbar("BluetoothEnabled: $isEnabled"); }, ), if (BleCapabilities.requiresRuntimePermission) @@ -163,9 +163,7 @@ class _MyAppState extends State { bool hasPermissions = await PermissionHandler.arePermissionsGranted(); if (hasPermissions) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Permissions granted")), - ); + showSnackbar("Permissions granted"); } }, ), @@ -176,11 +174,7 @@ class _MyAppState extends State { List devices = await UniversalBle.getSystemDevices(); if (devices.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("No Connected Devices Found"), - ), - ); + showSnackbar("No Connected Devices Found"); } setState(() { _bleDevices.clear(); diff --git a/example/lib/home/widgets/scanned_item_widget.dart b/example/lib/home/widgets/scanned_item_widget.dart index b7b7848..32e79ac 100644 --- a/example/lib/home/widgets/scanned_item_widget.dart +++ b/example/lib/home/widgets/scanned_item_widget.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:universal_ble/universal_ble.dart'; @@ -10,10 +9,10 @@ class ScannedItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { String? name = bleDevice.name; - Uint8List? rawManufacturerData = bleDevice.manufacturerData; + List rawManufacturerData = bleDevice.manufacturerDataList; ManufacturerData? manufacturerData; - if (rawManufacturerData != null && rawManufacturerData.isNotEmpty) { - manufacturerData = ManufacturerData.fromData(rawManufacturerData); + if (rawManufacturerData.isNotEmpty) { + manufacturerData = rawManufacturerData.first; } if (name == null || name.isEmpty) name = 'NA'; return Padding( @@ -29,9 +28,7 @@ class ScannedItemWidget extends StatelessWidget { Text(bleDevice.deviceId), Visibility( visible: manufacturerData != null, - child: Text( - 'CompanyIdentifier: ${manufacturerData.toString()} (${manufacturerData?.companyIdRadix16})', - ), + child: Text(manufacturerData.toString()), ), bleDevice.isPaired == true ? const Text( diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index 06bf7b3..9b0fb72 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -98,13 +98,13 @@ class _PeripheralDetailPageState extends State { if (kIsWeb) { _addLog( "DiscoverServices", - '${services.length} services discovered,\nNote: Only services added in ScanFilter or WebConfig will be discovered', + '${services.length} services discovered,\nNote: Only services added in ScanFilter or WebOptions will be discovered', ); } } catch (e) { _addLog( "DiscoverServicesError", - '$e\nNote: Only services added in ScanFilter or WebConfig will be discovered', + '$e\nNote: Only services added in ScanFilter or WebOptions will be discovered', ); } } diff --git a/example/pubspec.lock b/example/pubspec.lock index 7c083fa..d0b6e7e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -402,7 +402,7 @@ packages: path: ".." relative: true source: path - version: "0.12.0" + version: "0.13.0" vector_math: dependency: transitive description: @@ -415,10 +415,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" webdriver: dependency: transitive description: diff --git a/lib/src/models/ble_device.dart b/lib/src/models/ble_device.dart index 360f9cb..0232803 100644 --- a/lib/src/models/ble_device.dart +++ b/lib/src/models/ble_device.dart @@ -9,8 +9,12 @@ class BleDevice { bool? isPaired; List services; bool? isSystemDevice; - Uint8List? manufacturerDataHead; - Uint8List? manufacturerData; + List manufacturerDataList; + + @Deprecated("Use `manufacturerDataList` instead") + Uint8List? get manufacturerData => manufacturerDataList.isEmpty + ? null + : manufacturerDataList.first.toUint8List(); /// Returns connection state of the device. /// All platforms will return `Connected/Disconnected` states. @@ -30,12 +34,8 @@ class BleDevice { this.isPaired, this.services = const [], this.isSystemDevice, - Uint8List? manufacturerData, - Uint8List? manufacturerDataHead, - }) { - this.manufacturerDataHead = manufacturerDataHead ?? Uint8List.fromList([]); - this.manufacturerData = manufacturerData ?? manufacturerDataHead; - } + this.manufacturerDataList = const [], + }); @override String toString() { @@ -46,34 +46,6 @@ class BleDevice { 'isPaired: $isPaired, ' 'services: $services, ' 'isSystemDevice: $isSystemDevice, ' - 'manufacturerDataHead: $manufacturerDataHead, ' - 'manufacturerData: $manufacturerData'; - } -} - -/// Represents the manufacturer data of a BLE device. -/// Use [BleDevice.manufacturerData] with [ManufacturerData.fromData] to create an instance of this class. -class ManufacturerData { - final int? companyId; - final Uint8List? data; - String? companyIdRadix16; - ManufacturerData(this.companyId, this.data) { - if (companyId != null) { - companyIdRadix16 = "0x0${companyId!.toRadixString(16)}"; - } - } - - factory ManufacturerData.fromData(Uint8List data) { - if (data.length < 2) return ManufacturerData(null, data); - int manufacturerIdInt = (data[0] + (data[1] << 8)); - return ManufacturerData( - manufacturerIdInt, - data.sublist(2), - ); - } - - @override - String toString() { - return 'ManufacturerData: companyId: $companyIdRadix16, data: $data'; + 'manufacturerDataList: $manufacturerDataList'; } } diff --git a/lib/src/models/manufacturer_data.dart b/lib/src/models/manufacturer_data.dart new file mode 100644 index 0000000..c6a64ba --- /dev/null +++ b/lib/src/models/manufacturer_data.dart @@ -0,0 +1,43 @@ +import 'package:flutter/foundation.dart'; + +/// Represents the manufacturer data of a BLE device. +class ManufacturerData { + final int companyId; + final Uint8List payload; + ManufacturerData(this.companyId, this.payload); + + String get companyIdRadix16 => "0x0${companyId.toRadixString(16)}"; + + factory ManufacturerData.fromData(Uint8List data) { + if (data.length < 2) { + throw const FormatException("Invalid Manufacturer Data"); + } + return ManufacturerData( + (data[0] + (data[1] << 8)), + data.sublist(2), + ); + } + + Uint8List toUint8List() { + final byteData = ByteData(2); + byteData.setInt16(0, companyId, Endian.host); + return Uint8List.fromList( + byteData.buffer.asUint8List() + payload.toList(), + ); + } + + @override + int get hashCode => companyId.hashCode ^ payload.hashCode; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ManufacturerData) return false; + return companyId == other.companyId && listEquals(payload, other.payload); + } + + @override + String toString() { + return 'Manufacturer: $companyIdRadix16 - $payload'; + } +} diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart index 82b7aac..0c65eb9 100644 --- a/lib/src/models/model_exports.dart +++ b/lib/src/models/model_exports.dart @@ -1,3 +1,4 @@ +export 'package:universal_ble/src/models/manufacturer_data.dart'; export 'package:universal_ble/src/models/platform_config.dart'; export 'package:universal_ble/src/models/queue_type.dart'; export 'package:universal_ble/src/models/ble_uuid_parser.dart'; diff --git a/lib/src/models/platform_config.dart b/lib/src/models/platform_config.dart index c8097a2..df2c823 100644 --- a/lib/src/models/platform_config.dart +++ b/lib/src/models/platform_config.dart @@ -1,22 +1,22 @@ /// Platform specific config to scan devices class PlatformConfig { - WebConfig? web; + WebOptions? web; PlatformConfig({this.web}); } -/// Web config to scan devices +/// Web options to scan devices /// [optionalServices] is a list of service uuid's to ensure that you can access the specified services after connecting to the device, /// by default services from scanFilter will be used /// [optionalManufacturerData] is list of `CompanyIdentifier's` and used to add `ManufacturerData` in advertisement results of selected device from web dialog, /// by default manufacturerData from scanFilter will be used /// Checkout more details on [web](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice) /// Note: you will only get advertisements if Experimental Flag is enabled in the browser -class WebConfig { +class WebOptions { List optionalServices; List optionalManufacturerData; - WebConfig({ + WebOptions({ this.optionalServices = const [], this.optionalManufacturerData = const [], }); diff --git a/lib/src/models/scan_filter.dart b/lib/src/models/scan_filter.dart index 7e6f522..82885fa 100644 --- a/lib/src/models/scan_filter.dart +++ b/lib/src/models/scan_filter.dart @@ -18,10 +18,10 @@ class ScanFilter { } class ManufacturerDataFilter { - int? companyIdentifier; + int companyIdentifier; // Mask and data must be of same length - Uint8List? data; + Uint8List? payload; /// For any bit in the mask, set it the 1 if it needs to match /// the one in manufacturer data, otherwise set it to 0. @@ -29,13 +29,13 @@ class ManufacturerDataFilter { Uint8List? mask; ManufacturerDataFilter({ - this.companyIdentifier, - this.data, + required this.companyIdentifier, + this.payload, this.mask, }); @override String toString() { - return 'ManufacturerDataFilter(companyIdentifier: $companyIdentifier, data: $data, mask: $mask)'; + return 'ManufacturerDataFilter(companyIdentifier: $companyIdentifier, payload: $payload, mask: $mask)'; } } diff --git a/lib/src/universal_ble_filter_util.dart b/lib/src/universal_ble_filter_util.dart new file mode 100644 index 0000000..ddcb98c --- /dev/null +++ b/lib/src/universal_ble_filter_util.dart @@ -0,0 +1,98 @@ +import 'dart:typed_data'; + +import 'package:universal_ble/universal_ble.dart'; + +// A default Filter on dart side +// Used on Linux only +class UniversalBleFilterUtil { + ScanFilter? scanFilter; + + bool filterDevice(BleDevice device) { + final filter = scanFilter; + if (filter == null) return true; + + final hasNamePrefixFilter = filter.withNamePrefix.isNotEmpty; + final hasServiceFilter = filter.withServices.isNotEmpty; + final hasManufacturerDataFilter = filter.withManufacturerData.isNotEmpty; + + // If there is no filter at all, then allow device + if (!hasNamePrefixFilter && + !hasServiceFilter && + !hasManufacturerDataFilter) { + return true; + } + + // Else check one of the filter passes + return hasNamePrefixFilter && isNameMatchingFilters(filter, device) || + hasServiceFilter && isServicesMatchingFilters(filter, device) || + hasManufacturerDataFilter && + isManufacturerDataMatchingFilters(filter, device); + } + + bool isNameMatchingFilters(ScanFilter scanFilter, BleDevice device) { + var namePrefixFilter = scanFilter.withNamePrefix; + if (namePrefixFilter.isEmpty) return true; + + String? name = device.name; + if (name == null || name.isEmpty) return false; + return namePrefixFilter.any(name.startsWith); + } + + bool isServicesMatchingFilters(ScanFilter scanFilter, BleDevice device) { + var serviceFilters = scanFilter.withServices; + if (serviceFilters.isEmpty) return true; + + List serviceUuids = device.services; + if (serviceUuids.isEmpty) { + return false; + } + return serviceFilters.any(serviceUuids.contains); + } + + bool isManufacturerDataMatchingFilters( + ScanFilter scanFilter, + BleDevice device, + ) { + final filterMfdList = scanFilter.withManufacturerData; + if (filterMfdList.isEmpty) return true; + + List manufacturerDataList = device.manufacturerDataList; + if (manufacturerDataList.isEmpty) return false; + + return manufacturerDataList.any((deviceMfd) => filterMfdList.any( + (filterMfd) => _isManufacturerDataMatch(filterMfd, deviceMfd), + )); + } + + bool _isManufacturerDataMatch( + ManufacturerDataFilter filterMfd, + ManufacturerData deviceMfd, + ) { + if (filterMfd.companyIdentifier != deviceMfd.companyId) return false; + + Uint8List? filterPayload = filterMfd.payload; + Uint8List devicePayload = deviceMfd.payload; + + if (filterPayload == null || filterPayload.isEmpty) return true; + if (devicePayload.isEmpty) return false; + if (filterPayload.length > devicePayload.length) return false; + + Uint8List? filterMask = filterMfd.mask; + + if (filterMask != null && filterMask.length == filterPayload.length) { + for (int i = 0; i < filterPayload.length; i++) { + if ((filterPayload[i] & filterMask[i]) != + (devicePayload[i] & filterMask[i])) { + return false; + } + } + } else { + for (int i = 0; i < filterPayload.length; i++) { + if (filterPayload[i] != devicePayload[i]) { + return false; + } + } + } + return true; + } +} diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 55f2cee..1f6981e 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:bluez/bluez.dart'; import 'package:flutter/services.dart'; import 'package:universal_ble/src/models/model_exports.dart'; +import 'package:universal_ble/src/universal_ble_filter_util.dart'; import 'package:universal_ble/src/universal_ble_platform_interface.dart'; class UniversalBleLinux extends UniversalBlePlatform { @@ -14,12 +14,13 @@ class UniversalBleLinux extends UniversalBlePlatform { bool isInitialized = false; final BlueZClient _client = BlueZClient(); - + late final UniversalBleFilterUtil _bleFilter = UniversalBleFilterUtil(); BlueZAdapter? _activeAdapter; - ScanFilter? _scanFilter; Completer? _initializationCompleter; final Map _devices = {}; - final Map _deviceStreamSubscriptions = {}; + final Map _deviceUpdateStreamSubscriptions = {}; + final Map _deviceAdvertisementSubscriptions = {}; + final Map _characteristicPropertiesSubscriptions = {}; @@ -58,15 +59,44 @@ class UniversalBleLinux extends UniversalBlePlatform { PlatformConfig? platformConfig, }) async { await _ensureInitialized(); - await super.startScan(scanFilter: scanFilter); - if (_activeAdapter?.discovering != true) { - // Add services filter - _activeAdapter?.setDiscoveryFilter( - uuids: scanFilter?.withServices.toValidUUIDList(), - ); - _scanFilter = scanFilter; - await _activeAdapter?.startDiscovery(); - _client.devices.forEach(_onDeviceAdd); + var adapter = _activeAdapter; + if (adapter == null) { + throw "Adapter not available"; + } + + // Stop scan and clean all old advertisement listeners + await stopScan(); + + bool hasCustomFilter = scanFilter?.hasCustomFilter() ?? false; + List withServicesFilter = []; + + if (hasCustomFilter) { + _bleFilter.scanFilter = scanFilter; + } else { + _bleFilter.scanFilter = null; + withServicesFilter = scanFilter?.withServices.toValidUUIDList() ?? []; + } + + // Add services filter + await adapter.setDiscoveryFilter( + uuids: withServicesFilter, + ); + + await _activeAdapter?.startDiscovery(); + + // Apply custom Services filter to these devices + ScanFilter customServicesFilter = ScanFilter( + withServices: withServicesFilter, + ); + for (var device in _client.devices) { + if (!hasCustomFilter && withServicesFilter.isNotEmpty) { + if (_bleFilter.isServicesMatchingFilters( + customServicesFilter, device.toBleDevice())) { + _onDeviceAdd(device); + } + } else { + _onDeviceAdd(device); + } } } @@ -77,6 +107,11 @@ class UniversalBleLinux extends UniversalBlePlatform { if (_activeAdapter?.discovering == true) { await _activeAdapter?.stopDiscovery(); } + // Clean all advertiseemnt listeners + _deviceAdvertisementSubscriptions.removeWhere((e, value) { + value.cancel(); + return true; + }); } catch (e) { UniversalBlePlatform.logInfo( "stopScan error: $e", @@ -139,6 +174,9 @@ class UniversalBleLinux extends UniversalBlePlatform { }); } + // Few ble devices requires delay to perform operations after discovering services + await Future.delayed(const Duration(seconds: 1)); + if (device.gattServices.isEmpty && !device.servicesResolved) { throw "Failed to resolve services"; } @@ -413,40 +451,41 @@ class UniversalBleLinux extends UniversalBlePlatform { } void _onDeviceAdd(BlueZDevice device) { - if (!_isValidDevice(device)) return; - - // Apply Filters - if (_scanFilter != null) { - List? manufacturerDataFilter = - _scanFilter?.withManufacturerData; - if (manufacturerDataFilter != null && - manufacturerDataFilter.isNotEmpty) {} + BleDevice bleDevice = device.toBleDevice(); + if (!_bleFilter.filterDevice(bleDevice)) { + return; } // Update scan results only if rssi is available - if (device.rssi != 0) updateScanResult(device.toBleDevice()); + if (device.rssi != 0) { + updateScanResult(bleDevice); + } // Setup Cache _devices[device.address] = device; - // Setup update listener - if (_deviceStreamSubscriptions[device.address] != null) { - _deviceStreamSubscriptions[device.address]?.cancel(); - } + // Setup advertisements Listener + _deviceAdvertisementSubscriptions[device.address] ??= device + .propertiesChanged + .where((e) => + e.contains(BluezProperty.rssi) || + e.contains(BluezProperty.manufacturerData) || + e.contains(BluezProperty.uuids)) + .listen((_) { + if (_bleFilter.filterDevice(bleDevice)) { + updateScanResult(device.toBleDevice()); + } + }); - _deviceStreamSubscriptions[device.address] = + // Setup update listener + _deviceUpdateStreamSubscriptions[device.address] ??= device.propertiesChanged.listen((properties) { for (final property in properties) { switch (property) { - case BluezProperty.rssi: - updateScanResult(device.toBleDevice()); - break; + // Connection/Pair updates case BluezProperty.connected: updateConnection(device.address, device.connected); break; - case BluezProperty.manufacturerData: - updateScanResult(device.toBleDevice()); - break; case BluezProperty.paired: updatePairingState(device.address, device.paired); break; @@ -458,95 +497,37 @@ class UniversalBleLinux extends UniversalBlePlatform { case BluezProperty.txPower: case BluezProperty.address: case BluezProperty.addressType: + case BluezProperty.rssi: + case BluezProperty.manufacturerData: break; default: UniversalBlePlatform.logInfo( - "UnhandledDevicePropertyChanged ${device.name} ${device.address}: $property"); + "UnhandledDevicePropertyChanged ${device.name} ${device.address}: $property", + ); break; } } }); } - bool _isValidDevice(BlueZDevice device) { - ScanFilter? scanFilter = _scanFilter; - if (scanFilter == null) return true; - - // Check manufacturerData filter - if (!_isValidManufacturerData( - scanFilter.withManufacturerData, - device.manufacturerDataFilter, - )) { - return false; - } - - return true; - } - - bool _isValidManufacturerData( - List filterMfdList, - List deviceMfdList, - ) { - if (filterMfdList.isEmpty) return true; - if (deviceMfdList.isEmpty) return false; - - // Check all filters - for (final filterMfd in filterMfdList) { - // Check if device have manufacturerData for this filter - for (final deviceMfd in deviceMfdList) { - // Check companyIdentifier - if (filterMfd.companyIdentifier != deviceMfd.companyIdentifier) { - continue; - } - - // Check data - Uint8List? filterData = filterMfd.data; - Uint8List? deviceData = deviceMfd.data; - - // If filter data is null and device data is not, continue to next deviceMfd - if (filterData != null && deviceData == null) continue; - - if (filterData == null || deviceData == null) { - return true; - } - - if (filterData.length > deviceData.length) continue; - - // Apply mask - Uint8List? filterMask = filterMfd.mask; - bool dataMatched = true; - if (filterMask != null && (filterMask.length == filterData.length)) { - for (int i = 0; i < filterData.length; i++) { - if ((filterData[i] & filterMask[i]) != - (deviceData[i] & filterMask[i])) { - dataMatched = false; - break; - } - } - } - // Compare data directly - else { - for (int i = 0; i < filterData.length; i++) { - if (filterData[i] != deviceData[i]) { - dataMatched = false; - break; - } - } - } - - if (dataMatched) return true; - } - } - - return false; - } - void _onDeviceRemoved(BlueZDevice device) { _devices.remove(device.address); - // Stop listener - _deviceStreamSubscriptions[device.address]?.cancel(); - _deviceStreamSubscriptions - .removeWhere((key, value) => key == device.address); + // Clean Update listeners + _deviceUpdateStreamSubscriptions.removeWhere((key, value) { + if (key == device.address) { + value.cancel(); + return true; + } + return false; + }); + // Clean Advertisement listeners + _deviceAdvertisementSubscriptions.removeWhere((key, value) { + if (key == device.address) { + value.cancel(); + return true; + } + return false; + }); } } @@ -570,62 +551,6 @@ class BluezProperty { static const String propertyClass = 'Class'; } -extension BlueZDeviceExtension on BlueZDevice { - List get manufacturerDataFilter { - try { - if (manufacturerData.isEmpty) return []; - return manufacturerData.entries - .map((e) => ManufacturerDataFilter( - companyIdentifier: e.key.id, - data: Uint8List.fromList(e.value), - )) - .toList(); - } catch (e) { - UniversalBlePlatform.logInfo( - 'Error parsing manufacturerData: $e', - isError: true, - ); - return []; - } - } - - Uint8List get manufacturerDataHead { - try { - if (manufacturerData.isEmpty) return Uint8List(0); - final sorted = manufacturerData.entries.toList() - ..sort((a, b) => a.key.id - b.key.id); - int companyId = sorted.first.key.id; - List manufacturerDataValue = sorted.first.value; - final byteData = ByteData(2); - // TODO: Verify that this works regardless of the endianess - byteData.setInt16(0, companyId, Endian.host); - List bytes = byteData.buffer.asUint8List(); - return Uint8List.fromList(bytes + manufacturerDataValue); - } catch (e) { - UniversalBlePlatform.logInfo( - 'Error parsing manufacturerData: $e', - isError: true, - ); - return Uint8List(0); - } - } - - BleDevice toBleDevice({ - bool? isSystemDevice, - }) { - return BleDevice( - name: alias, - deviceId: address, - isPaired: paired, - manufacturerData: manufacturerDataHead, - manufacturerDataHead: manufacturerDataHead, - rssi: rssi, - isSystemDevice: isSystemDevice, - services: uuids.map((e) => e.toString()).toList(), - ); - } -} - extension on BlueZGattCharacteristicFlag { CharacteristicProperty? toCharacteristicProperty() { return switch (this) { @@ -664,3 +589,28 @@ extension on BlueZFailedException { } } } + +extension on ScanFilter { + bool hasCustomFilter() { + return withNamePrefix.isNotEmpty || withManufacturerData.isNotEmpty; + } +} + +extension BlueZDeviceExtension on BlueZDevice { + List get manufacturerDataList => manufacturerData.entries + .map((MapEntry> data) => + ManufacturerData(data.key.id, Uint8List.fromList(data.value))) + .toList(); + + BleDevice toBleDevice({bool? isSystemDevice}) { + return BleDevice( + name: name, + deviceId: address, + isPaired: paired, + rssi: rssi, + isSystemDevice: isSystemDevice, + services: uuids.map((e) => e.toString()).toList(), + manufacturerDataList: manufacturerDataList, + ); + } +} diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index 7e048f1..b6f8f03 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -15,8 +15,7 @@ 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 []; } @@ -32,8 +31,7 @@ class UniversalBleScanResult { this.name, this.isPaired, this.rssi, - this.manufacturerData, - this.manufacturerDataHead, + this.manufacturerDataList, this.services, }); @@ -45,9 +43,7 @@ class UniversalBleScanResult { int? rssi; - Uint8List? manufacturerData; - - Uint8List? manufacturerDataHead; + List? manufacturerDataList; List? services; @@ -57,8 +53,7 @@ class UniversalBleScanResult { name, isPaired, rssi, - manufacturerData, - manufacturerDataHead, + manufacturerDataList, services, ]; } @@ -70,9 +65,8 @@ class UniversalBleScanResult { name: result[1] as String?, isPaired: result[2] as bool?, rssi: result[3] as int?, - manufacturerData: result[4] as Uint8List?, - manufacturerDataHead: result[5] as Uint8List?, - services: (result[6] as List?)?.cast(), + manufacturerDataList: (result[4] as List?)?.cast(), + services: (result[5] as List?)?.cast(), ); } } @@ -98,8 +92,7 @@ class UniversalBleService { result as List; return UniversalBleService( uuid: result[0]! as String, - characteristics: - (result[1] as List?)?.cast(), + characteristics: (result[1] as List?)?.cast(), ); } } @@ -134,16 +127,20 @@ class UniversalBleCharacteristic { class UniversalScanFilter { UniversalScanFilter({ required this.withServices, + required this.withNamePrefix, required this.withManufacturerData, }); List withServices; + List withNamePrefix; + List withManufacturerData; Object encode() { return [ withServices, + withNamePrefix, withManufacturerData, ]; } @@ -152,20 +149,20 @@ class UniversalScanFilter { result as List; return UniversalScanFilter( withServices: (result[0] as List?)!.cast(), - withManufacturerData: (result[1] as List?)! - .cast(), + withNamePrefix: (result[1] as List?)!.cast(), + withManufacturerData: (result[2] as List?)!.cast(), ); } } class UniversalManufacturerDataFilter { UniversalManufacturerDataFilter({ - this.companyIdentifier, + required this.companyIdentifier, this.data, this.mask, }); - int? companyIdentifier; + int companyIdentifier; Uint8List? data; @@ -182,13 +179,40 @@ class UniversalManufacturerDataFilter { static UniversalManufacturerDataFilter decode(Object result) { result as List; return UniversalManufacturerDataFilter( - companyIdentifier: result[0] as int?, + companyIdentifier: result[0]! as int, data: result[1] as Uint8List?, mask: result[2] as Uint8List?, ); } } +class UniversalManufacturerData { + UniversalManufacturerData({ + required this.companyIdentifier, + required this.data, + }); + + int companyIdentifier; + + Uint8List data; + + Object encode() { + return [ + companyIdentifier, + data, + ]; + } + + static UniversalManufacturerData decode(Object result) { + result as List; + return UniversalManufacturerData( + companyIdentifier: result[0]! as int, + data: result[1]! as Uint8List, + ); + } +} + + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -196,18 +220,21 @@ class _PigeonCodec extends StandardMessageCodec { if (value is UniversalBleScanResult) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is UniversalBleService) { + } else if (value is UniversalBleService) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is UniversalBleCharacteristic) { + } else if (value is UniversalBleCharacteristic) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is UniversalScanFilter) { + } else if (value is UniversalScanFilter) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is UniversalManufacturerDataFilter) { + } else if (value is UniversalManufacturerDataFilter) { buffer.putUint8(133); writeValue(buffer, value.encode()); + } else if (value is UniversalManufacturerData) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -216,16 +243,18 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return UniversalBleScanResult.decode(readValue(buffer)!); - case 130: + case 130: return UniversalBleService.decode(readValue(buffer)!); - case 131: + case 131: return UniversalBleCharacteristic.decode(readValue(buffer)!); - case 132: + case 132: return UniversalScanFilter.decode(readValue(buffer)!); - case 133: + case 133: return UniversalManufacturerDataFilter.decode(readValue(buffer)!); + case 134: + return UniversalManufacturerData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -237,11 +266,9 @@ 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 = ''}) : __pigeon_binaryMessenger = binaryMessenger, - __pigeon_messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + __pigeon_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? __pigeon_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -249,10 +276,8 @@ class UniversalBlePlatformChannel { final String __pigeon_messageChannelSuffix; Future getBluetoothAvailabilityState() async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -278,10 +303,8 @@ class UniversalBlePlatformChannel { } Future enableBluetooth() async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -307,10 +330,8 @@ class UniversalBlePlatformChannel { } Future startScan(UniversalScanFilter? filter) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -331,10 +352,8 @@ class UniversalBlePlatformChannel { } Future stopScan() async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -355,10 +374,8 @@ class UniversalBlePlatformChannel { } Future connect(String deviceId) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -379,10 +396,8 @@ class UniversalBlePlatformChannel { } Future disconnect(String deviceId) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -402,19 +417,15 @@ class UniversalBlePlatformChannel { } } - Future setNotifiable(String deviceId, String service, - String characteristic, int bleInputProperty) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + Future setNotifiable(String deviceId, String service, String characteristic, int bleInputProperty) async { + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel.send( - [deviceId, service, characteristic, bleInputProperty]) - as List?; + final List? __pigeon_replyList = + await __pigeon_channel.send([deviceId, service, characteristic, bleInputProperty]) as List?; if (__pigeon_replyList == null) { throw _createConnectionError(__pigeon_channelName); } else if (__pigeon_replyList.length > 1) { @@ -429,10 +440,8 @@ class UniversalBlePlatformChannel { } Future> discoverServices(String deviceId) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -453,23 +462,19 @@ class UniversalBlePlatformChannel { message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as List?)! - .cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } - Future readValue( - String deviceId, String service, String characteristic) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + Future readValue(String deviceId, String service, String characteristic) async { + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel - .send([deviceId, service, characteristic]) as List?; + final List? __pigeon_replyList = + await __pigeon_channel.send([deviceId, service, characteristic]) as List?; if (__pigeon_replyList == null) { throw _createConnectionError(__pigeon_channelName); } else if (__pigeon_replyList.length > 1) { @@ -489,16 +494,14 @@ class UniversalBlePlatformChannel { } Future requestMtu(String deviceId, int expectedMtu) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel - .send([deviceId, expectedMtu]) as List?; + final List? __pigeon_replyList = + await __pigeon_channel.send([deviceId, expectedMtu]) as List?; if (__pigeon_replyList == null) { throw _createConnectionError(__pigeon_channelName); } else if (__pigeon_replyList.length > 1) { @@ -517,24 +520,15 @@ class UniversalBlePlatformChannel { } } - Future writeValue(String deviceId, String service, - String characteristic, Uint8List value, int bleOutputProperty) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + Future writeValue(String deviceId, String service, String characteristic, Uint8List value, int bleOutputProperty) async { + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, ); - final List? __pigeon_replyList = await __pigeon_channel - .send([ - deviceId, - service, - characteristic, - value, - bleOutputProperty - ]) as List?; + final List? __pigeon_replyList = + await __pigeon_channel.send([deviceId, service, characteristic, value, bleOutputProperty]) as List?; if (__pigeon_replyList == null) { throw _createConnectionError(__pigeon_channelName); } else if (__pigeon_replyList.length > 1) { @@ -549,10 +543,8 @@ class UniversalBlePlatformChannel { } Future isPaired(String deviceId) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -578,10 +570,8 @@ class UniversalBlePlatformChannel { } Future pair(String deviceId) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -607,10 +597,8 @@ class UniversalBlePlatformChannel { } Future unPair(String deviceId) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -630,12 +618,9 @@ class UniversalBlePlatformChannel { } } - Future> getSystemDevices( - List withServices) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + Future> getSystemDevices(List withServices) async { + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -656,16 +641,13 @@ class UniversalBlePlatformChannel { message: 'Host platform returned null value for non-null return value.', ); } else { - return (__pigeon_replyList[0] as List?)! - .cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } Future getConnectionState(String deviceId) async { - final String __pigeon_channelName = - 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectionState$__pigeon_messageChannelSuffix'; - final BasicMessageChannel __pigeon_channel = - BasicMessageChannel( + final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectionState$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( __pigeon_channelName, pigeonChannelCodec, binaryMessenger: __pigeon_binaryMessenger, @@ -701,30 +683,22 @@ 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); - 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 __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged$messageChannelSuffix', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_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, @@ -734,25 +708,22 @@ 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 __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange$messageChannelSuffix', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_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, @@ -766,28 +737,24 @@ 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 __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult$messageChannelSuffix', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_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 { @@ -795,25 +762,22 @@ 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 __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged$messageChannelSuffix', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_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, @@ -825,30 +789,26 @@ 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 __pigeon_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged$messageChannelSuffix', - pigeonChannelCodec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged$messageChannelSuffix', pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { __pigeon_channel.setMessageHandler(null); } else { __pigeon_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, @@ -861,9 +821,8 @@ 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 ad33bf3..99d1d89 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -32,7 +32,6 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { ScanFilter? scanFilter, PlatformConfig? platformConfig, }) async { - await super.startScan(scanFilter: scanFilter); await _channel.startScan( scanFilter.toUniversalScanFilter(), ); @@ -194,22 +193,16 @@ class _UniversalBleCallbackHandler extends UniversalBleCallbackChannel { } extension _UniversalBleScanResultExtension on UniversalBleScanResult { - BleDevice toBleDevice({ - bool? isSystemDevice, - }) { - var mnfDataHead = manufacturerDataHead ?? Uint8List.fromList([]); - var mnfData = manufacturerData ?? mnfDataHead; + BleDevice toBleDevice({bool? isSystemDevice}) { return BleDevice( name: name, deviceId: deviceId, - manufacturerData: mnfData, - manufacturerDataHead: mnfDataHead, rssi: rssi, isPaired: isPaired, isSystemDevice: isSystemDevice, - services: services - ?.where((e) => e != null) - .map((e) => BleUuidParser.string(e!)) + services: services?.nonNulls.map(BleUuidParser.string).toList() ?? [], + manufacturerDataList: manufacturerDataList?.nonNulls + .map((e) => ManufacturerData(e.companyIdentifier, e.data)) .toList() ?? [], ); @@ -222,7 +215,7 @@ extension _ScanFilterExtension on ScanFilter? { ?.withManufacturerData .map((e) => UniversalManufacturerDataFilter( companyIdentifier: e.companyIdentifier, - data: e.data, + data: e.payload, mask: e.mask, )) .toList(); @@ -230,6 +223,7 @@ extension _ScanFilterExtension on ScanFilter? { // Windows crashes if it's null, so we need to pass empty scan filter in this case return UniversalScanFilter( withServices: this?.withServices.toValidUUIDList() ?? [], + withNamePrefix: this?.withNamePrefix ?? [], withManufacturerData: manufacturerDataFilters ?? [], ); } diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index e8bf9c2..b539cd8 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; import 'package:universal_ble/universal_ble.dart'; abstract class UniversalBlePlatform { - ScanFilter? _scanFilter; StreamController? _connectionStreamController; final Map _pairStateMap = {}; @@ -15,9 +14,7 @@ abstract class UniversalBlePlatform { Future startScan({ ScanFilter? scanFilter, PlatformConfig? platformConfig, - }) async { - _scanFilter = scanFilter; - } + }); Future stopScan(); @@ -68,13 +65,6 @@ abstract class UniversalBlePlatform { } void updateScanResult(BleDevice bleDevice) { - // Filter by name - ScanFilter? scanFilter = _scanFilter; - if (scanFilter != null && scanFilter.withNamePrefix.isNotEmpty) { - if (bleDevice.name == null || - !scanFilter.withNamePrefix - .any((e) => bleDevice.name?.startsWith(e) == true)) return; - } onScanResult?.call(bleDevice); } diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 5e5e942..b19d038 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart'; import 'package:universal_ble/src/models/model_exports.dart'; import 'package:universal_ble/src/universal_ble_platform_interface.dart'; @@ -95,8 +95,14 @@ class UniversalBleWeb extends UniversalBlePlatform { } @override - bool receivesAdvertisements(String deviceId) => - _getDeviceById(deviceId)?.hasWatchAdvertisements() ?? false; + bool receivesAdvertisements(String deviceId) { + // Advertisements do not work on Linux/Web even with the "Experimental Web Platform features" flag enabled. Verified with Chrome Version 128.0.6613.138 + if (kIsWeb && defaultTargetPlatform == TargetPlatform.linux) { + return false; + } + + return _getDeviceById(deviceId)?.hasWatchAdvertisements() ?? false; + } /// This will work only if `chrome://flags/#enable-experimental-web-platform-features` is enabled Future _watchDeviceAdvertisements(BluetoothDevice device) async { @@ -342,22 +348,22 @@ class UniversalBleWeb extends UniversalBlePlatform { RequestOptionsBuilder _getRequestOptionBuilder( ScanFilter? scanFilter, - WebConfig? webConfig, + WebOptions? webOptions, ) { List filters = []; List optionalManufacturerData = []; List optionalServices = []; - if (webConfig != null) { - optionalServices.addAll(webConfig.optionalServices.toValidUUIDList()); - optionalManufacturerData.addAll(webConfig.optionalManufacturerData); + if (webOptions != null) { + optionalServices.addAll(webOptions.optionalServices.toValidUUIDList()); + optionalManufacturerData.addAll(webOptions.optionalManufacturerData); } if (scanFilter != null) { // Add services filter for (var service in scanFilter.withServices.toValidUUIDList()) { filters.add(RequestFilterBuilder(services: [service])); - if (webConfig == null || webConfig.optionalServices.isEmpty) { + if (webOptions == null || webOptions.optionalServices.isEmpty) { optionalServices.add(service); } } @@ -369,19 +375,16 @@ class UniversalBleWeb extends UniversalBlePlatform { manufacturerData: [ ManufacturerDataFilterBuilder( companyIdentifier: manufacturerData.companyIdentifier, - dataPrefix: manufacturerData.data, + dataPrefix: manufacturerData.payload, mask: manufacturerData.mask, ), ], ), ); - // Add optionalManufacturerData from scanFilter if webConfig is not provided - if (webConfig == null || webConfig.optionalManufacturerData.isEmpty) { - int? companyId = manufacturerData.companyIdentifier; - if (companyId != null) { - optionalManufacturerData.add(companyId); - } + // Add optionalManufacturerData from scanFilter if webOptions is not provided + if (webOptions == null || webOptions.optionalManufacturerData.isEmpty) { + optionalManufacturerData.add(manufacturerData.companyIdentifier); } } @@ -422,8 +425,7 @@ extension _BluetoothDeviceExtension on BluetoothDevice { return BleDevice( name: name, deviceId: id, - manufacturerData: manufacturerDataMap?.toUint8List(), - manufacturerDataHead: manufacturerDataMap?.toUint8List(), + manufacturerDataList: manufacturerDataMap?.toManufacturerDataList() ?? [], rssi: rssi, services: services, ); @@ -431,17 +433,10 @@ extension _BluetoothDeviceExtension on BluetoothDevice { } extension _UnmodifiableMapViewExtension on UnmodifiableMapView { - Uint8List? toUint8List() { - List> sorted = entries.toList() - ..sort((a, b) => a.key - b.key); - if (sorted.isEmpty) return null; - int companyId = sorted.first.key; - List manufacturerDataValue = sorted.first.value.buffer.asUint8List(); - final byteData = ByteData(2); - byteData.setInt16(0, companyId, Endian.host); - List bytes = byteData.buffer.asUint8List(); - return Uint8List.fromList(bytes + manufacturerDataValue); - } + List? toManufacturerDataList() => entries + .map((MapEntry data) => + ManufacturerData(data.key, data.value.buffer.asUint8List())) + .toList(); } class _UniversalWebBluetoothService { diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 9f1d2af..ff57f65 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -107,8 +107,7 @@ class UniversalBleScanResult { final String? name; final bool? isPaired; final int? rssi; - final Uint8List? manufacturerData; - final Uint8List? manufacturerDataHead; + final List? manufacturerDataList; final List? services; UniversalBleScanResult({ @@ -116,8 +115,7 @@ class UniversalBleScanResult { required this.deviceId, required this.isPaired, required this.rssi, - required this.manufacturerData, - required this.manufacturerDataHead, + required this.manufacturerDataList, required this.services, }); } @@ -137,21 +135,33 @@ class UniversalBleCharacteristic { /// Scan Filters class UniversalScanFilter { final List withServices; + final List withNamePrefix; final List withManufacturerData; UniversalScanFilter( this.withServices, + this.withNamePrefix, this.withManufacturerData, ); } class UniversalManufacturerDataFilter { - int? companyIdentifier; + int companyIdentifier; Uint8List? data; Uint8List? mask; UniversalManufacturerDataFilter({ - this.companyIdentifier, + required this.companyIdentifier, this.data, this.mask, }); } + +class UniversalManufacturerData { + final int companyIdentifier; + final Uint8List data; + + UniversalManufacturerData({ + required this.companyIdentifier, + required this.data, + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index d4ebc5b..0a60599 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.12.1 +version: 0.13.0 homepage: https://navideck.com repository: https://github.com/Navideck/universal_ble issue_tracker: https://github.com/Navideck/universal_ble/issues diff --git a/test/scan_filter_test.dart b/test/scan_filter_test.dart new file mode 100644 index 0000000..2e5ea74 --- /dev/null +++ b/test/scan_filter_test.dart @@ -0,0 +1,198 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:universal_ble/src/universal_ble_filter_util.dart'; +import 'package:universal_ble/universal_ble.dart'; + +void main() { + final universalBleFilter = UniversalBleFilterUtil(); + var device1 = BleDevice( + deviceId: '1', + name: '1_device', + services: ['1_ser'], + manufacturerDataList: [ + ManufacturerData(0x01, Uint8List.fromList([1, 2, 3])), + ], + ); + var device2 = BleDevice( + deviceId: '2', + name: '2_device', + services: ['2_ser'], + manufacturerDataList: [ + ManufacturerData(0x02, Uint8List.fromList([1, 2, 3])) + ], + ); + var device3 = BleDevice( + deviceId: '3', + name: '3_device', + services: ['3_ser'], + manufacturerDataList: [ + ManufacturerData(0x03, Uint8List.fromList([1, 2, 3])) + ], + ); + + group("Test Individual Filter", () { + test('Test isNameMatchingFilters', () { + var scanFilter = ScanFilter( + withNamePrefix: ['1', '2'], + ); + expect( + universalBleFilter.isNameMatchingFilters(scanFilter, device1), + isTrue, + ); + expect( + universalBleFilter.isNameMatchingFilters(scanFilter, device2), + isTrue, + ); + expect( + universalBleFilter.isNameMatchingFilters(scanFilter, device3), + isFalse, + ); + }); + + test('Test isServicesMatchingFilters', () { + var scanFilter = ScanFilter( + withServices: ['1_ser', 'random', '3_ser'], + ); + expect( + universalBleFilter.isServicesMatchingFilters(scanFilter, device1), + isTrue, + ); + expect( + universalBleFilter.isServicesMatchingFilters(scanFilter, device2), + isFalse, + ); + expect( + universalBleFilter.isNameMatchingFilters(scanFilter, device3), + isTrue, + ); + }); + + test('Test isManufacturerDataMatchingFilters', () { + var scanFilter = ScanFilter(withManufacturerData: [ + ManufacturerDataFilter( + companyIdentifier: 0x01, + payload: Uint8List.fromList([1, 2]), + ), + ManufacturerDataFilter( + companyIdentifier: 0x02, + ), + ManufacturerDataFilter( + companyIdentifier: 0x03, + payload: Uint8List.fromList([3, 4]), + ) + ]); + expect( + universalBleFilter.isManufacturerDataMatchingFilters( + scanFilter, + device1, + ), + isTrue, + ); + expect( + universalBleFilter.isManufacturerDataMatchingFilters( + scanFilter, + device2, + ), + isTrue, + ); + expect( + universalBleFilter.isManufacturerDataMatchingFilters( + scanFilter, + device3, + ), + isFalse, + ); + }); + }); + + group("Test Scan Filter", () { + test('Test filterDevice: Have filter for all', () { + universalBleFilter.scanFilter = ScanFilter( + withNamePrefix: ['1'], + withServices: ['3_ser'], + withManufacturerData: [ + ManufacturerDataFilter( + companyIdentifier: 0x02, + ) + ], + ); + expect( + universalBleFilter.filterDevice(device1), + isTrue, + ); + expect( + universalBleFilter.filterDevice(device2), + isTrue, + ); + expect( + universalBleFilter.filterDevice(device3), + isTrue, + ); + }); + test('Test filterDevice: Filter for one', () { + universalBleFilter.scanFilter = ScanFilter( + withNamePrefix: ['1'], + ); + expect( + universalBleFilter.filterDevice(device1), + isTrue, + ); + expect( + universalBleFilter.filterDevice(device2), + isFalse, + ); + expect( + universalBleFilter.filterDevice(device3), + isFalse, + ); + }); + test('Test filterDevice: Filter for two', () { + universalBleFilter.scanFilter = ScanFilter( + withNamePrefix: ['1', '2'], + ); + expect( + universalBleFilter.filterDevice(device1), + isTrue, + ); + expect( + universalBleFilter.filterDevice(device2), + isTrue, + ); + expect( + universalBleFilter.filterDevice(device3), + isFalse, + ); + }); + test('Test filterDevice: Empty Filter', () { + universalBleFilter.scanFilter = ScanFilter(); + expect( + universalBleFilter.filterDevice(device1), + isTrue, + ); + expect( + universalBleFilter.filterDevice(device2), + isTrue, + ); + expect( + universalBleFilter.filterDevice(device3), + isTrue, + ); + }); + test('Test filterDevice: Null Filter', () { + universalBleFilter.scanFilter = ScanFilter(); + expect( + universalBleFilter.filterDevice(device1), + isTrue, + ); + expect( + universalBleFilter.filterDevice(device2), + isTrue, + ); + expect( + universalBleFilter.filterDevice(device3), + isTrue, + ); + }); + }); +} diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 73267dd..b924871 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -42,6 +42,8 @@ list(APPEND PLUGIN_SOURCES "src/generated/universal_ble.g.cpp" "src/generated/universal_ble.g.h" "src/pin_entry.h" + "src/universal_ble_filter_util.cpp" + "src/universal_ble_filter_util.h" ) add_library(${PLUGIN_NAME} SHARED diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index d14b422..72f40e2 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -38,15 +38,13 @@ UniversalBleScanResult::UniversalBleScanResult( const std::string* name, const bool* is_paired, const int64_t* rssi, - const std::vector* manufacturer_data, - const std::vector* manufacturer_data_head, + const EncodableList* manufacturer_data_list, const EncodableList* services) : device_id_(device_id), name_(name ? std::optional(*name) : std::nullopt), is_paired_(is_paired ? std::optional(*is_paired) : std::nullopt), rssi_(rssi ? std::optional(*rssi) : std::nullopt), - manufacturer_data_(manufacturer_data ? std::optional>(*manufacturer_data) : std::nullopt), - manufacturer_data_head_(manufacturer_data_head ? std::optional>(*manufacturer_data_head) : std::nullopt), + manufacturer_data_list_(manufacturer_data_list ? std::optional(*manufacturer_data_list) : std::nullopt), services_(services ? std::optional(*services) : std::nullopt) {} const std::string& UniversalBleScanResult::device_id() const { @@ -97,29 +95,16 @@ void UniversalBleScanResult::set_rssi(int64_t value_arg) { } -const std::vector* UniversalBleScanResult::manufacturer_data() const { - return manufacturer_data_ ? &(*manufacturer_data_) : nullptr; +const EncodableList* UniversalBleScanResult::manufacturer_data_list() const { + return manufacturer_data_list_ ? &(*manufacturer_data_list_) : nullptr; } -void UniversalBleScanResult::set_manufacturer_data(const std::vector* value_arg) { - manufacturer_data_ = value_arg ? std::optional>(*value_arg) : std::nullopt; +void UniversalBleScanResult::set_manufacturer_data_list(const EncodableList* value_arg) { + manufacturer_data_list_ = value_arg ? std::optional(*value_arg) : std::nullopt; } -void UniversalBleScanResult::set_manufacturer_data(const std::vector& value_arg) { - manufacturer_data_ = value_arg; -} - - -const std::vector* UniversalBleScanResult::manufacturer_data_head() const { - return manufacturer_data_head_ ? &(*manufacturer_data_head_) : nullptr; -} - -void UniversalBleScanResult::set_manufacturer_data_head(const std::vector* value_arg) { - manufacturer_data_head_ = value_arg ? std::optional>(*value_arg) : std::nullopt; -} - -void UniversalBleScanResult::set_manufacturer_data_head(const std::vector& value_arg) { - manufacturer_data_head_ = value_arg; +void UniversalBleScanResult::set_manufacturer_data_list(const EncodableList& value_arg) { + manufacturer_data_list_ = value_arg; } @@ -138,13 +123,12 @@ void UniversalBleScanResult::set_services(const EncodableList& value_arg) { EncodableList UniversalBleScanResult::ToEncodableList() const { EncodableList list; - list.reserve(7); + list.reserve(6); list.push_back(EncodableValue(device_id_)); list.push_back(name_ ? EncodableValue(*name_) : EncodableValue()); list.push_back(is_paired_ ? EncodableValue(*is_paired_) : EncodableValue()); list.push_back(rssi_ ? EncodableValue(*rssi_) : EncodableValue()); - list.push_back(manufacturer_data_ ? EncodableValue(*manufacturer_data_) : EncodableValue()); - list.push_back(manufacturer_data_head_ ? EncodableValue(*manufacturer_data_head_) : EncodableValue()); + list.push_back(manufacturer_data_list_ ? EncodableValue(*manufacturer_data_list_) : EncodableValue()); list.push_back(services_ ? EncodableValue(*services_) : EncodableValue()); return list; } @@ -164,15 +148,11 @@ UniversalBleScanResult UniversalBleScanResult::FromEncodableList(const Encodable if (!encodable_rssi.IsNull()) { decoded.set_rssi(encodable_rssi.LongValue()); } - auto& encodable_manufacturer_data = list[4]; - if (!encodable_manufacturer_data.IsNull()) { - decoded.set_manufacturer_data(std::get>(encodable_manufacturer_data)); + auto& encodable_manufacturer_data_list = list[4]; + if (!encodable_manufacturer_data_list.IsNull()) { + decoded.set_manufacturer_data_list(std::get(encodable_manufacturer_data_list)); } - auto& encodable_manufacturer_data_head = list[5]; - if (!encodable_manufacturer_data_head.IsNull()) { - decoded.set_manufacturer_data_head(std::get>(encodable_manufacturer_data_head)); - } - auto& encodable_services = list[6]; + auto& encodable_services = list[5]; if (!encodable_services.IsNull()) { decoded.set_services(std::get(encodable_services)); } @@ -275,8 +255,10 @@ UniversalBleCharacteristic UniversalBleCharacteristic::FromEncodableList(const E UniversalScanFilter::UniversalScanFilter( const EncodableList& with_services, + const EncodableList& with_name_prefix, const EncodableList& with_manufacturer_data) : with_services_(with_services), + with_name_prefix_(with_name_prefix), with_manufacturer_data_(with_manufacturer_data) {} const EncodableList& UniversalScanFilter::with_services() const { @@ -288,6 +270,15 @@ void UniversalScanFilter::set_with_services(const EncodableList& value_arg) { } +const EncodableList& UniversalScanFilter::with_name_prefix() const { + return with_name_prefix_; +} + +void UniversalScanFilter::set_with_name_prefix(const EncodableList& value_arg) { + with_name_prefix_ = value_arg; +} + + const EncodableList& UniversalScanFilter::with_manufacturer_data() const { return with_manufacturer_data_; } @@ -299,8 +290,9 @@ void UniversalScanFilter::set_with_manufacturer_data(const EncodableList& value_ EncodableList UniversalScanFilter::ToEncodableList() const { EncodableList list; - list.reserve(2); + list.reserve(3); list.push_back(EncodableValue(with_services_)); + list.push_back(EncodableValue(with_name_prefix_)); list.push_back(EncodableValue(with_manufacturer_data_)); return list; } @@ -308,28 +300,26 @@ EncodableList UniversalScanFilter::ToEncodableList() const { UniversalScanFilter UniversalScanFilter::FromEncodableList(const EncodableList& list) { UniversalScanFilter decoded( std::get(list[0]), - std::get(list[1])); + std::get(list[1]), + std::get(list[2])); return decoded; } // UniversalManufacturerDataFilter -UniversalManufacturerDataFilter::UniversalManufacturerDataFilter() {} +UniversalManufacturerDataFilter::UniversalManufacturerDataFilter(int64_t company_identifier) + : company_identifier_(company_identifier) {} UniversalManufacturerDataFilter::UniversalManufacturerDataFilter( - const int64_t* company_identifier, + int64_t company_identifier, const std::vector* data, const std::vector* mask) - : company_identifier_(company_identifier ? std::optional(*company_identifier) : std::nullopt), + : company_identifier_(company_identifier), data_(data ? std::optional>(*data) : std::nullopt), mask_(mask ? std::optional>(*mask) : std::nullopt) {} -const int64_t* UniversalManufacturerDataFilter::company_identifier() const { - return company_identifier_ ? &(*company_identifier_) : nullptr; -} - -void UniversalManufacturerDataFilter::set_company_identifier(const int64_t* value_arg) { - company_identifier_ = value_arg ? std::optional(*value_arg) : std::nullopt; +int64_t UniversalManufacturerDataFilter::company_identifier() const { + return company_identifier_; } void UniversalManufacturerDataFilter::set_company_identifier(int64_t value_arg) { @@ -366,18 +356,15 @@ void UniversalManufacturerDataFilter::set_mask(const std::vector& value EncodableList UniversalManufacturerDataFilter::ToEncodableList() const { EncodableList list; list.reserve(3); - list.push_back(company_identifier_ ? EncodableValue(*company_identifier_) : EncodableValue()); + list.push_back(EncodableValue(company_identifier_)); list.push_back(data_ ? EncodableValue(*data_) : EncodableValue()); list.push_back(mask_ ? EncodableValue(*mask_) : EncodableValue()); return list; } UniversalManufacturerDataFilter UniversalManufacturerDataFilter::FromEncodableList(const EncodableList& list) { - UniversalManufacturerDataFilter decoded; - auto& encodable_company_identifier = list[0]; - if (!encodable_company_identifier.IsNull()) { - decoded.set_company_identifier(encodable_company_identifier.LongValue()); - } + UniversalManufacturerDataFilter decoded( + list[0].LongValue()); auto& encodable_data = list[1]; if (!encodable_data.IsNull()) { decoded.set_data(std::get>(encodable_data)); @@ -389,6 +376,47 @@ UniversalManufacturerDataFilter UniversalManufacturerDataFilter::FromEncodableLi return decoded; } +// UniversalManufacturerData + +UniversalManufacturerData::UniversalManufacturerData( + int64_t company_identifier, + const std::vector& data) + : company_identifier_(company_identifier), + data_(data) {} + +int64_t UniversalManufacturerData::company_identifier() const { + return company_identifier_; +} + +void UniversalManufacturerData::set_company_identifier(int64_t value_arg) { + company_identifier_ = value_arg; +} + + +const std::vector& UniversalManufacturerData::data() const { + return data_; +} + +void UniversalManufacturerData::set_data(const std::vector& value_arg) { + data_ = value_arg; +} + + +EncodableList UniversalManufacturerData::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(company_identifier_)); + list.push_back(EncodableValue(data_)); + return list; +} + +UniversalManufacturerData UniversalManufacturerData::FromEncodableList(const EncodableList& list) { + UniversalManufacturerData decoded( + list[0].LongValue(), + std::get>(list[1])); + return decoded; +} + PigeonCodecSerializer::PigeonCodecSerializer() {} @@ -406,6 +434,8 @@ EncodableValue PigeonCodecSerializer::ReadValueOfType( return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); case 133: return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + case 134: + return CustomEncodableValue(UniversalManufacturerData::FromEncodableList(std::get(ReadValue(stream)))); default: return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); } @@ -440,6 +470,11 @@ void PigeonCodecSerializer::WriteValue( WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } + if (custom_value->type() == typeid(UniversalManufacturerData)) { + stream->WriteByte(134); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } } flutter::StandardCodecSerializer::WriteValue(value, stream); } diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index ec7d190..edfca14 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -69,8 +69,7 @@ class UniversalBleScanResult { const std::string* name, const bool* is_paired, const int64_t* rssi, - const std::vector* manufacturer_data, - const std::vector* manufacturer_data_head, + const flutter::EncodableList* manufacturer_data_list, const flutter::EncodableList* services); const std::string& device_id() const; @@ -88,13 +87,9 @@ class UniversalBleScanResult { void set_rssi(const int64_t* value_arg); void set_rssi(int64_t value_arg); - const std::vector* manufacturer_data() const; - void set_manufacturer_data(const std::vector* value_arg); - void set_manufacturer_data(const std::vector& value_arg); - - const std::vector* manufacturer_data_head() const; - void set_manufacturer_data_head(const std::vector* value_arg); - void set_manufacturer_data_head(const std::vector& value_arg); + const flutter::EncodableList* manufacturer_data_list() const; + void set_manufacturer_data_list(const flutter::EncodableList* value_arg); + void set_manufacturer_data_list(const flutter::EncodableList& value_arg); const flutter::EncodableList* services() const; void set_services(const flutter::EncodableList* value_arg); @@ -111,8 +106,7 @@ class UniversalBleScanResult { std::optional name_; std::optional is_paired_; std::optional rssi_; - std::optional> manufacturer_data_; - std::optional> manufacturer_data_head_; + std::optional manufacturer_data_list_; std::optional services_; }; @@ -184,11 +178,15 @@ class UniversalScanFilter { // Constructs an object setting all fields. explicit UniversalScanFilter( const flutter::EncodableList& with_services, + const flutter::EncodableList& with_name_prefix, const flutter::EncodableList& with_manufacturer_data); const flutter::EncodableList& with_services() const; void set_with_services(const flutter::EncodableList& value_arg); + const flutter::EncodableList& with_name_prefix() const; + void set_with_name_prefix(const flutter::EncodableList& value_arg); + const flutter::EncodableList& with_manufacturer_data() const; void set_with_manufacturer_data(const flutter::EncodableList& value_arg); @@ -200,6 +198,7 @@ class UniversalScanFilter { friend class UniversalBleCallbackChannel; friend class PigeonCodecSerializer; flutter::EncodableList with_services_; + flutter::EncodableList with_name_prefix_; flutter::EncodableList with_manufacturer_data_; }; @@ -209,16 +208,15 @@ class UniversalScanFilter { class UniversalManufacturerDataFilter { public: // Constructs an object setting all non-nullable fields. - UniversalManufacturerDataFilter(); + explicit UniversalManufacturerDataFilter(int64_t company_identifier); // Constructs an object setting all fields. explicit UniversalManufacturerDataFilter( - const int64_t* company_identifier, + int64_t company_identifier, const std::vector* data, const std::vector* mask); - const int64_t* company_identifier() const; - void set_company_identifier(const int64_t* value_arg); + int64_t company_identifier() const; void set_company_identifier(int64_t value_arg); const std::vector* data() const; @@ -236,12 +234,39 @@ class UniversalManufacturerDataFilter { friend class UniversalBlePlatformChannel; friend class UniversalBleCallbackChannel; friend class PigeonCodecSerializer; - std::optional company_identifier_; + int64_t company_identifier_; std::optional> data_; std::optional> mask_; }; + +// Generated class from Pigeon that represents data sent in messages. +class UniversalManufacturerData { + public: + // Constructs an object setting all fields. + explicit UniversalManufacturerData( + int64_t company_identifier, + const std::vector& data); + + int64_t company_identifier() const; + void set_company_identifier(int64_t value_arg); + + 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; + friend class UniversalBlePlatformChannel; + friend class UniversalBleCallbackChannel; + friend class PigeonCodecSerializer; + int64_t company_identifier_; + std::vector data_; + +}; + class PigeonCodecSerializer : public flutter::StandardCodecSerializer { public: PigeonCodecSerializer(); diff --git a/windows/src/helper/utils.cpp b/windows/src/helper/utils.cpp index 4f9d4b2..f94c90a 100644 --- a/windows/src/helper/utils.cpp +++ b/windows/src/helper/utils.cpp @@ -158,4 +158,4 @@ namespace universal_ble return rove.dwMajorVersion == 10 && rove.dwBuildNumber >= 22000; } -} // namespace SimpleBLE +} // namespace universal_ble diff --git a/windows/src/universal_ble_filter_util.cpp b/windows/src/universal_ble_filter_util.cpp new file mode 100644 index 0000000..27225d9 --- /dev/null +++ b/windows/src/universal_ble_filter_util.cpp @@ -0,0 +1,158 @@ +#include +#include +#include +#include +#include +#include +#include +#include "helper/utils.h" +#include "generated/universal_ble.g.h" +#include + +namespace universal_ble +{ + std::vector manufacturerScanFilter = std::vector(); + std::vector serviceFilterUUIDS = std::vector(); + std::vector namePrefixFilter = std::vector(); + + void setScanFilter(const UniversalScanFilter filter) + { + // Set ManufacturerData filter + const auto &manufacturerData = filter.with_manufacturer_data(); + for (const flutter::EncodableValue &data : manufacturerData) + { + UniversalManufacturerDataFilter manufacturerDataFilter = std::any_cast(std::get(data)); + manufacturerScanFilter.push_back(manufacturerDataFilter); + } + // Set Services Filter + if (!filter.with_services().empty()) + { + for (const auto &uuid : filter.with_services()) + { + serviceFilterUUIDS.push_back(uuid_to_guid(std::get(uuid))); + } + } + // Set Names Filter + if (!filter.with_name_prefix().empty()) + { + for (const auto &name : filter.with_name_prefix()) + { + namePrefixFilter.push_back(std::get(name)); + } + } + } + + void resetScanFilter() + { + manufacturerScanFilter.clear(); + serviceFilterUUIDS.clear(); + namePrefixFilter.clear(); + } + + bool isNameMatchingFilters(const std::string *name) + { + if (namePrefixFilter.empty()) + return true; + if (name == nullptr || name->empty()) + return false; + return std::any_of(namePrefixFilter.begin(), namePrefixFilter.end(), + [&name](const flutter::EncodableValue &prefix) + { + return name->find(std::get(prefix)) == 0; + }); + } + + bool isServicesMatchingFilters(const flutter::EncodableList *services) + { + if (serviceFilterUUIDS.empty()) + return true; + if (services == nullptr || services->empty()) + return false; + + std::unordered_set serviceGUIDs; + serviceGUIDs.reserve(services->size()); + for (const auto &service : *services) + { + if (const auto *str = std::get_if(&service)) + { + serviceGUIDs.insert(uuid_to_guid(*str)); + } + } + + // Check if any of the filter UUIDs are in the set of service GUIDs + return std::any_of(serviceFilterUUIDS.begin(), serviceFilterUUIDS.end(), + [&serviceGUIDs](const winrt::guid &filterUUID) + { + return serviceGUIDs.find(filterUUID) != serviceGUIDs.end(); + }); + } + + bool isManufacturerDataMatchingFilters(const flutter::EncodableList *manufacturerDataList) + { + if (manufacturerScanFilter.empty()) + return true; + if (manufacturerDataList == nullptr || manufacturerDataList->empty()) + return false; + + for (const auto &filter : manufacturerScanFilter) + { + const auto *data_filter = filter.data(); + const auto *mask = filter.mask(); + + for (const auto &value : *manufacturerDataList) + { + const auto &deviceMfData = std::any_cast( + std::get(value)); + + if (deviceMfData.company_identifier() != filter.company_identifier()) + continue; + + // If no data filter, all data matches + if (data_filter == nullptr) + return true; + + const auto &deviceData = deviceMfData.data(); + if (deviceData.size() < data_filter->size()) + continue; + + bool isMatch = true; + for (size_t i = 0; i < data_filter->size(); ++i) + { + const bool hasMask = mask != nullptr && i < mask->size(); + const uint8_t maskByte = hasMask ? (*mask)[i] : 0xFF; + if ((maskByte & (*data_filter)[i]) != (maskByte & deviceData[i])) + { + isMatch = false; + break; + } + } + + if (isMatch) + return true; + } + } + + return false; + } + + bool filterDevice(UniversalBleScanResult scanResult) + { + bool hasNamePrefixFilter = !namePrefixFilter.empty(); + bool hasServiceFilter = !serviceFilterUUIDS.empty(); + bool hasManufacturerDataFilter = !manufacturerScanFilter.empty(); + + // If there is no filter at all, then allow device + if (!hasNamePrefixFilter && + !hasServiceFilter && + !hasManufacturerDataFilter) + { + return true; + } + + // Check each filter condition + return (hasNamePrefixFilter && isNameMatchingFilters(scanResult.name())) || + (hasServiceFilter && isServicesMatchingFilters(scanResult.services())) || + (hasManufacturerDataFilter && isManufacturerDataMatchingFilters(scanResult.manufacturer_data_list())); + } + +} // namespace universal_ble diff --git a/windows/src/universal_ble_filter_util.h b/windows/src/universal_ble_filter_util.h new file mode 100644 index 0000000..13496d8 --- /dev/null +++ b/windows/src/universal_ble_filter_util.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include + +#include "helper/universal_ble_base.h" +#include "generated/universal_ble.g.h" + +namespace universal_ble +{ + void setScanFilter(const UniversalScanFilter filter); + void resetScanFilter(); + bool filterDevice(UniversalBleScanResult scanResult); +} // namespace universal_ble \ No newline at end of file diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index b5b8eff..29d4bbb 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -15,6 +15,7 @@ #include "helper/universal_enum.h" #include "generated/universal_ble.g.h" #include "pin_entry.h" +#include "universal_ble_filter_util.h" namespace universal_ble { @@ -32,7 +33,6 @@ namespace universal_ble std::unique_ptr callbackChannel; std::unordered_map characteristicsTokens{}; // TODO: Remove the map and store the token inside the characteristic object - std::vector manufacturerScanFilter = std::vector(); void UniversalBlePlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar) { @@ -107,30 +107,30 @@ namespace universal_ble { bluetoothLEWatcher = BluetoothLEAdvertisementWatcher(); bluetoothLEWatcher.ScanningMode(BluetoothLEScanningMode::Active); + resetScanFilter(); - // reset scan filters - manufacturerScanFilter.clear(); if (filter != nullptr) { - // Apply Services filter - const auto &services = filter->with_services(); - if (!services.empty()) + // Only Services filter supported natively + bool hasCustomFilters = filter->with_manufacturer_data().size() > 0 || filter->with_name_prefix().size() > 0; + if (hasCustomFilters) { - for (const auto &uuid : services) + std::cout << "Using Custom Scan Filter" << std::endl; + setScanFilter(*filter); + } + else + { + // Apply Services filter + if (!filter->with_services().empty()) { - std::string uuid_str = std::get(uuid); - bluetoothLEWatcher.AdvertisementFilter().Advertisement().ServiceUuids().Append(uuid_to_guid(uuid_str)); + for (const auto &uuid : filter->with_services()) + { + bluetoothLEWatcher.AdvertisementFilter().Advertisement().ServiceUuids().Append(uuid_to_guid(std::get(uuid))); + } } } - - // Set ManufacturerData filter - const auto &manufacturerData = filter->with_manufacturer_data(); - for (const flutter::EncodableValue &data : manufacturerData) - { - UniversalManufacturerDataFilter manufacturerDataFilter = std::any_cast(std::get(data)); - manufacturerScanFilter.push_back(manufacturerDataFilter); - } } + bluetoothLEWatcherReceivedToken = bluetoothLEWatcher.Received({this, &UniversalBlePlugin::BluetoothLEWatcher_Received}); } bluetoothLEWatcher.Start(); @@ -476,52 +476,6 @@ namespace universal_ble /// Helper Methods - std::vector parseManufacturerDataHead(BluetoothLEAdvertisement advertisement, std::string deviceId) - { - try - { - if (advertisement == nullptr) - return {}; - - if (advertisement.ManufacturerData().Size() == 0) - return {}; - - auto manufacturerData = advertisement.ManufacturerData().GetAt(0); - if (manufacturerData == nullptr) - return {}; - - uint16_t companyId = manufacturerData.CompanyId(); - uint8_t prefix[2]; - auto leastSignificantBit = static_cast(companyId & 0xFF); - auto mostSignificantBit = static_cast(companyId >> 8); - if (isLittleEndian()) - { - prefix[0] = leastSignificantBit; - prefix[1] = mostSignificantBit; - } - else - { - prefix[0] = mostSignificantBit; - prefix[1] = leastSignificantBit; - } - std::vector result = {prefix[0], prefix[1]}; - - auto data = to_bytevc(manufacturerData.Data()); - result.insert(result.end(), data.begin(), data.end()); - - return result; - } - catch (const std::exception &e) - { - std::cerr << "Error in parsing manufacturer data for device " << deviceId << ": " << e.what() << std::endl; - } - catch (...) - { - std::cerr << "Unknown error occurred in parsing manufacturer data for device " << deviceId << std::endl; - } - return {}; - } - winrt::fire_and_forget UniversalBlePlugin::InitializeAsync() { auto radios = co_await Radio::GetRadiosAsync(); @@ -653,11 +607,13 @@ namespace universal_ble scanResult.set_is_paired(currentScanResult.is_paired()); shouldUpdate = true; } - if (scanResult.manufacturer_data_head() == nullptr && currentScanResult.manufacturer_data_head() != nullptr) + + if ((scanResult.manufacturer_data_list() == nullptr || scanResult.manufacturer_data_list()->empty()) && currentScanResult.manufacturer_data_list() != nullptr) { - scanResult.set_manufacturer_data_head(currentScanResult.manufacturer_data_head()); + scanResult.set_manufacturer_data_list(currentScanResult.manufacturer_data_list()); shouldUpdate = true; } + if (scanResult.services() == nullptr && currentScanResult.services() != nullptr) { scanResult.set_services(currentScanResult.services()); @@ -677,7 +633,8 @@ namespace universal_ble scanResults.insert(std::make_pair(scanResult.device_id(), scanResult)); } - if (isConnectable) + // Filter final result before sending to Flutter + if (isConnectable && filterDevice(scanResult)) { uiThreadHandler_.Post([scanResult] { callbackChannel->OnScanResult(scanResult, SuccessCallback, ErrorCallback); }); @@ -789,21 +746,19 @@ namespace universal_ble { try { - // Apply ManufacturerData filter - if (!manufacturerScanFilter.empty()) - { - // Avoid devices if ManufacturerData is not present - if (args.Advertisement().ManufacturerData().Size() == 0) - return; - // Avoid devices if ManufacturerData does not match the filter - if (!filterByManufacturerData(args.Advertisement().ManufacturerData())) - return; - } - auto deviceId = _mac_address_to_str(args.BluetoothAddress()); auto universalScanResult = UniversalBleScanResult(deviceId); std::string name = winrt::to_string(args.Advertisement().LocalName()); - auto manufacturerData = parseManufacturerDataHead(args.Advertisement(), deviceId); + + flutter::EncodableList manufacturerDataEncodableList = flutter::EncodableList(); + if (args.Advertisement() != nullptr) + { + for (BluetoothLEManufacturerData mfd : args.Advertisement().ManufacturerData()) + { + UniversalManufacturerData universalManufacturerData = UniversalManufacturerData(static_cast(mfd.CompanyId()), to_bytevc(mfd.Data())); + manufacturerDataEncodableList.push_back(flutter::CustomEncodableValue(universalManufacturerData)); + } + } auto dataSection = args.Advertisement().DataSections(); for (auto &&data : dataSection) @@ -822,10 +777,14 @@ namespace universal_ble } if (!name.empty()) + { universalScanResult.set_name(name); + } - if (!manufacturerData.empty()) - universalScanResult.set_manufacturer_data_head(manufacturerData); + if (!manufacturerDataEncodableList.empty()) + { + universalScanResult.set_manufacturer_data_list(manufacturerDataEncodableList); + } universalScanResult.set_rssi(args.RawSignalStrengthInDBm()); @@ -853,6 +812,7 @@ namespace universal_ble universalScanResult.set_name(winrt::to_string(deviceInfo.Name())); } + // Filter Device pushUniversalScanResult(universalScanResult, args.IsConnectable()); } catch (...) @@ -861,63 +821,6 @@ namespace universal_ble } } - bool UniversalBlePlugin::filterByManufacturerData(IVector deviceManufactureData) - { - if (manufacturerScanFilter.empty()) - return true; - - for (auto &&filter : manufacturerScanFilter) - { - const int64_t *company_identifier = filter.company_identifier(); - const std::vector *data_filter = filter.data(); - const std::vector *mask = filter.mask(); - - if (company_identifier == nullptr) - continue; - - uint16_t companyId = static_cast(*company_identifier); - - for (auto &&deviceMfData : deviceManufactureData) - { - if (deviceMfData.CompanyId() == companyId) - { - // If data filter is not present then return true - if (data_filter == nullptr) - return true; - - auto deviceData = to_bytevc(deviceMfData.Data()); - if (deviceData.size() < data_filter->size()) - continue; - - bool isMatch = true; - for (size_t i = 0; i < data_filter->size(); i++) - { - if (mask != nullptr && mask->size() > i) - { - if (((*mask)[i] & (*data_filter)[i]) != ((*mask)[i] & deviceData[i])) - { - isMatch = false; - break; - } - } - else - { - if ((*data_filter)[i] != deviceData[i]) - { - isMatch = false; - break; - } - } - } - if (isMatch) - return true; - } - } - } - - return false; - } - AvailabilityState UniversalBlePlugin::getAvailabilityStateFromRadio(RadioState radioState) { auto state = [=]() -> AvailabilityState diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index a7548d0..b0f9251 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -124,7 +124,6 @@ namespace universal_ble winrt::fire_and_forget WriteAsync(GattCharacteristic characteristic, GattWriteOption writeOption, const std::vector &value, std::function reply)> result); - bool filterByManufacturerData(IVector deviceManufactureData); winrt::fire_and_forget PairAsync(std::string device_id, std::function reply)> result); winrt::fire_and_forget CustomPairAsync(std::string device_id, std::function reply)> result); void PairingRequestedHandler(DeviceInformationCustomPairing sender, DevicePairingRequestedEventArgs eventArgs);