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 <foti@navideck.com>
This commit is contained in:
Rohit Sangwan
2024-09-17 03:13:51 +05:30
committed by GitHub
parent 639384af30
commit 7dc22c58a5
38 changed files with 1516 additions and 850 deletions
-2
View File
@@ -8,7 +8,6 @@
"connnection",
"Cupertino",
"DFACE",
"DSIMPLEBLE",
"HFONT",
"HINSTANCE",
"HMENU",
@@ -45,7 +44,6 @@
"rssi",
"SETFONT",
"SETTEXT",
"simpleble",
"subdata",
"sublist",
"THICKFRAME",
+11 -1
View File
@@ -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<ManufacturerData>` 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`
+18 -4
View File
@@ -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<String> 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"]
)
)
)
```
@@ -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<UniversalManufacturerData?>? = null,
val services: List<String?>? = 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<String?>?
return UniversalBleScanResult(deviceId, name, isPaired, rssi, manufacturerData, manufacturerDataHead, services)
val manufacturerDataList = __pigeon_list[4] as List<UniversalManufacturerData?>?
val services = __pigeon_list[5] as List<String?>?
return UniversalBleScanResult(deviceId, name, isPaired, rssi, manufacturerDataList, services)
}
}
fun toList(): List<Any?> {
@@ -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<String?>,
val withNamePrefix: List<String?>,
val withManufacturerData: List<UniversalManufacturerDataFilter?>
) {
@@ -142,13 +140,15 @@ data class UniversalScanFilter (
@Suppress("LocalVariableName")
fun fromList(__pigeon_list: List<Any?>): UniversalScanFilter {
val withServices = __pigeon_list[0] as List<String?>
val withManufacturerData = __pigeon_list[1] as List<UniversalManufacturerDataFilter?>
return UniversalScanFilter(withServices, withManufacturerData)
val withNamePrefix = __pigeon_list[1] as List<String?>
val withManufacturerData = __pigeon_list[2] as List<UniversalManufacturerDataFilter?>
return UniversalScanFilter(withServices, withNamePrefix, withManufacturerData)
}
}
fun toList(): List<Any?> {
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<Any?>): 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<Any?>): 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<Any?> {
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<Any?>)?.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)
}
}
@@ -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<UUID> = emptyList()
fun filterDevice(
name: String?,
manufacturerDataList: List<UniversalManufacturerData>,
serviceUuids: Array<UUID>,
): 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<UUID>,
): Boolean {
if (serviceFilterUUIDS.isEmpty()) {
return true
}
if (serviceUuids.isEmpty()) {
return false
}
return serviceFilterUUIDS.any {
serviceUuids.contains(it)
}
}
private fun isManufacturerDataMatchingFilters(
scanFilter: UniversalScanFilter,
manufacturerDataList: List<UniversalManufacturerData>,
): 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<UUID>): List<ScanFilter> {
val scanFilters: ArrayList<ScanFilter> = 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()
}
@@ -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<String>.toUUIDList(): List<UUID> {
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<ScanFilter> {
val scanFilters: ArrayList<ScanFilter> = 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<UniversalManufacturerData>
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 <T> SparseArray<T>.toList(): List<Pair<Int, T>> {
return (0 until size()).map { index ->
keyAt(index) to valueAt(index)
}
}
fun BluetoothGatt.getCharacteristic(
service: String,
characteristic: String,
@@ -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<String, List<String>>()
private val devicesStateMap = mutableMapOf<String, Int>()
private val universalBleFilterUtil = UniversalBleFilterUtil()
// Flutter Futures
private var bluetoothEnableRequestFuture: ((Result<Boolean>) -> Unit)? = null
@@ -114,11 +116,32 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
val settings = builder.build()
bluetoothManager.adapter.bluetoothLeScanner?.startScan(
filter?.toScanFilters() ?: emptyList<ScanFilter>(),
settings,
scanCallback
)
val hasCustomFilters = filter?.hasCustomFilter() ?: false;
try {
val filterServices = filter?.withServices?.filterNotNull()?.toUUIDList() ?: emptyList()
var scanFilters = emptyList<ScanFilter>()
// 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<ScanFilter>()
}
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<String> = arrayOf()
var serviceUuids: Array<UUID> = 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()
)
) {}
}
+40 -12
View File
@@ -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)
}
+107
View File
@@ -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
}
}
+1 -38
View File
@@ -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
+47 -25
View File
@@ -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<Bool, Error>) -> Void){
func pair(deviceId _: String, completion: @escaping (Result<Bool, Error>) -> 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 {}
+1 -1
View File
@@ -9,7 +9,7 @@ class MockUniversalBle extends UniversalBlePlatform {
name: 'MockDevice',
deviceId: 'MockDeviceId',
rssi: 50,
manufacturerData: Uint8List(0),
manufacturerDataList: [],
);
Uint8List _serviceValue = utf8.encode('Result');
+10 -16
View File
@@ -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<MyApp> {
);
}
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<MyApp> {
setState(() {
_isScanning = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
showSnackbar(e);
}
},
),
@@ -151,9 +153,7 @@ class _MyAppState extends State<MyApp> {
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<MyApp> {
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<MyApp> {
List<BleDevice> 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();
@@ -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<ManufacturerData> 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(
@@ -98,13 +98,13 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
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',
);
}
}
+3 -3
View File
@@ -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:
+9 -37
View File
@@ -9,8 +9,12 @@ class BleDevice {
bool? isPaired;
List<String> services;
bool? isSystemDevice;
Uint8List? manufacturerDataHead;
Uint8List? manufacturerData;
List<ManufacturerData> 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';
}
}
+43
View File
@@ -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';
}
}
+1
View File
@@ -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';
+4 -4
View File
@@ -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<String> optionalServices;
List<int> optionalManufacturerData;
WebConfig({
WebOptions({
this.optionalServices = const [],
this.optionalManufacturerData = const [],
});
+5 -5
View File
@@ -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)';
}
}
+98
View File
@@ -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<String> 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<ManufacturerData> 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;
}
}
@@ -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<void>? _initializationCompleter;
final Map<String, BlueZDevice> _devices = {};
final Map<String, StreamSubscription> _deviceStreamSubscriptions = {};
final Map<String, StreamSubscription> _deviceUpdateStreamSubscriptions = {};
final Map<String, StreamSubscription> _deviceAdvertisementSubscriptions = {};
final Map<String, StreamSubscription> _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<String> 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>? 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<ManufacturerDataFilter> filterMfdList,
List<ManufacturerDataFilter> 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<ManufacturerDataFilter> 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<int> manufacturerDataValue = sorted.first.value;
final byteData = ByteData(2);
// TODO: Verify that this works regardless of the endianess
byteData.setInt16(0, companyId, Endian.host);
List<int> 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<ManufacturerData> get manufacturerDataList => manufacturerData.entries
.map((MapEntry<BlueZManufacturerId, List<int>> 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,
);
}
}
+135 -176
View File
@@ -15,8 +15,7 @@ PlatformException _createConnectionError(String channelName) {
);
}
List<Object?> wrapResponse(
{Object? result, PlatformException? error, bool empty = false}) {
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
if (empty) {
return <Object?>[];
}
@@ -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<UniversalManufacturerData?>? manufacturerDataList;
List<String?>? 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<Object?>?)?.cast<String?>(),
manufacturerDataList: (result[4] as List<Object?>?)?.cast<UniversalManufacturerData?>(),
services: (result[5] as List<Object?>?)?.cast<String?>(),
);
}
}
@@ -98,8 +92,7 @@ class UniversalBleService {
result as List<Object?>;
return UniversalBleService(
uuid: result[0]! as String,
characteristics:
(result[1] as List<Object?>?)?.cast<UniversalBleCharacteristic?>(),
characteristics: (result[1] as List<Object?>?)?.cast<UniversalBleCharacteristic?>(),
);
}
}
@@ -134,16 +127,20 @@ class UniversalBleCharacteristic {
class UniversalScanFilter {
UniversalScanFilter({
required this.withServices,
required this.withNamePrefix,
required this.withManufacturerData,
});
List<String?> withServices;
List<String?> withNamePrefix;
List<UniversalManufacturerDataFilter?> withManufacturerData;
Object encode() {
return <Object?>[
withServices,
withNamePrefix,
withManufacturerData,
];
}
@@ -152,20 +149,20 @@ class UniversalScanFilter {
result as List<Object?>;
return UniversalScanFilter(
withServices: (result[0] as List<Object?>?)!.cast<String?>(),
withManufacturerData: (result[1] as List<Object?>?)!
.cast<UniversalManufacturerDataFilter?>(),
withNamePrefix: (result[1] as List<Object?>?)!.cast<String?>(),
withManufacturerData: (result[2] as List<Object?>?)!.cast<UniversalManufacturerDataFilter?>(),
);
}
}
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<Object?>;
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 <Object?>[
companyIdentifier,
data,
];
}
static UniversalManufacturerData decode(Object result) {
result as List<Object?>;
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<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -249,10 +276,8 @@ class UniversalBlePlatformChannel {
final String __pigeon_messageChannelSuffix;
Future<int> getBluetoothAvailabilityState() async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
@@ -278,10 +303,8 @@ class UniversalBlePlatformChannel {
}
Future<bool> enableBluetooth() async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
@@ -307,10 +330,8 @@ class UniversalBlePlatformChannel {
}
Future<void> startScan(UniversalScanFilter? filter) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
@@ -331,10 +352,8 @@ class UniversalBlePlatformChannel {
}
Future<void> stopScan() async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
@@ -355,10 +374,8 @@ class UniversalBlePlatformChannel {
}
Future<void> connect(String deviceId) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
@@ -379,10 +396,8 @@ class UniversalBlePlatformChannel {
}
Future<void> disconnect(String deviceId) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
@@ -402,19 +417,15 @@ class UniversalBlePlatformChannel {
}
}
Future<void> 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<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
Future<void> 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<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList = await __pigeon_channel.send(
<Object?>[deviceId, service, characteristic, bleInputProperty])
as List<Object?>?;
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[deviceId, service, characteristic, bleInputProperty]) as List<Object?>?;
if (__pigeon_replyList == null) {
throw _createConnectionError(__pigeon_channelName);
} else if (__pigeon_replyList.length > 1) {
@@ -429,10 +440,8 @@ class UniversalBlePlatformChannel {
}
Future<List<UniversalBleService?>> discoverServices(String deviceId) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__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<Object?>?)!
.cast<UniversalBleService?>();
return (__pigeon_replyList[0] as List<Object?>?)!.cast<UniversalBleService?>();
}
}
Future<Uint8List> readValue(
String deviceId, String service, String characteristic) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
Future<Uint8List> readValue(String deviceId, String service, String characteristic) async {
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList = await __pigeon_channel
.send(<Object?>[deviceId, service, characteristic]) as List<Object?>?;
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[deviceId, service, characteristic]) as List<Object?>?;
if (__pigeon_replyList == null) {
throw _createConnectionError(__pigeon_channelName);
} else if (__pigeon_replyList.length > 1) {
@@ -489,16 +494,14 @@ class UniversalBlePlatformChannel {
}
Future<int> requestMtu(String deviceId, int expectedMtu) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList = await __pigeon_channel
.send(<Object?>[deviceId, expectedMtu]) as List<Object?>?;
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[deviceId, expectedMtu]) as List<Object?>?;
if (__pigeon_replyList == null) {
throw _createConnectionError(__pigeon_channelName);
} else if (__pigeon_replyList.length > 1) {
@@ -517,24 +520,15 @@ class UniversalBlePlatformChannel {
}
}
Future<void> 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<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
Future<void> 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<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
);
final List<Object?>? __pigeon_replyList = await __pigeon_channel
.send(<Object?>[
deviceId,
service,
characteristic,
value,
bleOutputProperty
]) as List<Object?>?;
final List<Object?>? __pigeon_replyList =
await __pigeon_channel.send(<Object?>[deviceId, service, characteristic, value, bleOutputProperty]) as List<Object?>?;
if (__pigeon_replyList == null) {
throw _createConnectionError(__pigeon_channelName);
} else if (__pigeon_replyList.length > 1) {
@@ -549,10 +543,8 @@ class UniversalBlePlatformChannel {
}
Future<bool> isPaired(String deviceId) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
@@ -578,10 +570,8 @@ class UniversalBlePlatformChannel {
}
Future<bool> pair(String deviceId) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
@@ -607,10 +597,8 @@ class UniversalBlePlatformChannel {
}
Future<void> unPair(String deviceId) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__pigeon_channelName,
pigeonChannelCodec,
binaryMessenger: __pigeon_binaryMessenger,
@@ -630,12 +618,9 @@ class UniversalBlePlatformChannel {
}
}
Future<List<UniversalBleScanResult?>> getSystemDevices(
List<String?> withServices) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
Future<List<UniversalBleScanResult?>> getSystemDevices(List<String?> withServices) async {
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getSystemDevices$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__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<Object?>?)!
.cast<UniversalBleScanResult?>();
return (__pigeon_replyList[0] as List<Object?>?)!.cast<UniversalBleScanResult?>();
}
}
Future<int> getConnectionState(String deviceId) async {
final String __pigeon_channelName =
'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectionState$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel =
BasicMessageChannel<Object?>(
final String __pigeon_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectionState$__pigeon_messageChannelSuffix';
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
__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<Object?> __pigeon_channel = BasicMessageChannel<
Object?>(
'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged$messageChannelSuffix',
pigeonChannelCodec,
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
'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<Object?> args = (message as List<Object?>?)!;
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<Object?> __pigeon_channel = BasicMessageChannel<
Object?>(
'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange$messageChannelSuffix',
pigeonChannelCodec,
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
'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<Object?> args = (message as List<Object?>?)!;
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<Object?> __pigeon_channel = BasicMessageChannel<
Object?>(
'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult$messageChannelSuffix',
pigeonChannelCodec,
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
'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<Object?> args = (message as List<Object?>?)!;
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<Object?> __pigeon_channel = BasicMessageChannel<
Object?>(
'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged$messageChannelSuffix',
pigeonChannelCodec,
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
'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<Object?> args = (message as List<Object?>?)!;
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<Object?> __pigeon_channel = BasicMessageChannel<
Object?>(
'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged$messageChannelSuffix',
pigeonChannelCodec,
final BasicMessageChannel<Object?> __pigeon_channel = BasicMessageChannel<Object?>(
'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<Object?> args = (message as List<Object?>?)!;
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()));
}
});
}
@@ -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 ?? [],
);
}
+1 -11
View File
@@ -4,7 +4,6 @@ import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';
abstract class UniversalBlePlatform {
ScanFilter? _scanFilter;
StreamController? _connectionStreamController;
final Map<String, bool> _pairStateMap = {};
@@ -15,9 +14,7 @@ abstract class UniversalBlePlatform {
Future<void> startScan({
ScanFilter? scanFilter,
PlatformConfig? platformConfig,
}) async {
_scanFilter = scanFilter;
}
});
Future<void> 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);
}
@@ -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<void> _watchDeviceAdvertisements(BluetoothDevice device) async {
@@ -342,22 +348,22 @@ class UniversalBleWeb extends UniversalBlePlatform {
RequestOptionsBuilder _getRequestOptionBuilder(
ScanFilter? scanFilter,
WebConfig? webConfig,
WebOptions? webOptions,
) {
List<RequestFilterBuilder> filters = [];
List<int> optionalManufacturerData = [];
List<String> 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<int, ByteData> {
Uint8List? toUint8List() {
List<MapEntry<int, ByteData>> sorted = entries.toList()
..sort((a, b) => a.key - b.key);
if (sorted.isEmpty) return null;
int companyId = sorted.first.key;
List<int> manufacturerDataValue = sorted.first.value.buffer.asUint8List();
final byteData = ByteData(2);
byteData.setInt16(0, companyId, Endian.host);
List<int> bytes = byteData.buffer.asUint8List();
return Uint8List.fromList(bytes + manufacturerDataValue);
}
List<ManufacturerData>? toManufacturerDataList() => entries
.map((MapEntry<int, ByteData> data) =>
ManufacturerData(data.key, data.value.buffer.asUint8List()))
.toList();
}
class _UniversalWebBluetoothService {
+16 -6
View File
@@ -107,8 +107,7 @@ class UniversalBleScanResult {
final String? name;
final bool? isPaired;
final int? rssi;
final Uint8List? manufacturerData;
final Uint8List? manufacturerDataHead;
final List<UniversalManufacturerData?>? manufacturerDataList;
final List<String?>? 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<String?> withServices;
final List<String?> withNamePrefix;
final List<UniversalManufacturerDataFilter?> 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,
});
}
+1 -1
View File
@@ -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
+198
View File
@@ -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,
);
});
});
}
+2
View File
@@ -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
+86 -51
View File
@@ -38,15 +38,13 @@ UniversalBleScanResult::UniversalBleScanResult(
const std::string* name,
const bool* is_paired,
const int64_t* rssi,
const std::vector<uint8_t>* manufacturer_data,
const std::vector<uint8_t>* manufacturer_data_head,
const EncodableList* manufacturer_data_list,
const EncodableList* services)
: device_id_(device_id),
name_(name ? std::optional<std::string>(*name) : std::nullopt),
is_paired_(is_paired ? std::optional<bool>(*is_paired) : std::nullopt),
rssi_(rssi ? std::optional<int64_t>(*rssi) : std::nullopt),
manufacturer_data_(manufacturer_data ? std::optional<std::vector<uint8_t>>(*manufacturer_data) : std::nullopt),
manufacturer_data_head_(manufacturer_data_head ? std::optional<std::vector<uint8_t>>(*manufacturer_data_head) : std::nullopt),
manufacturer_data_list_(manufacturer_data_list ? std::optional<EncodableList>(*manufacturer_data_list) : std::nullopt),
services_(services ? std::optional<EncodableList>(*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<uint8_t>* 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<uint8_t>* value_arg) {
manufacturer_data_ = value_arg ? std::optional<std::vector<uint8_t>>(*value_arg) : std::nullopt;
void UniversalBleScanResult::set_manufacturer_data_list(const EncodableList* value_arg) {
manufacturer_data_list_ = value_arg ? std::optional<EncodableList>(*value_arg) : std::nullopt;
}
void UniversalBleScanResult::set_manufacturer_data(const std::vector<uint8_t>& value_arg) {
manufacturer_data_ = value_arg;
}
const std::vector<uint8_t>* UniversalBleScanResult::manufacturer_data_head() const {
return manufacturer_data_head_ ? &(*manufacturer_data_head_) : nullptr;
}
void UniversalBleScanResult::set_manufacturer_data_head(const std::vector<uint8_t>* value_arg) {
manufacturer_data_head_ = value_arg ? std::optional<std::vector<uint8_t>>(*value_arg) : std::nullopt;
}
void UniversalBleScanResult::set_manufacturer_data_head(const std::vector<uint8_t>& 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<std::vector<uint8_t>>(encodable_manufacturer_data));
auto& encodable_manufacturer_data_list = list[4];
if (!encodable_manufacturer_data_list.IsNull()) {
decoded.set_manufacturer_data_list(std::get<EncodableList>(encodable_manufacturer_data_list));
}
auto& encodable_manufacturer_data_head = list[5];
if (!encodable_manufacturer_data_head.IsNull()) {
decoded.set_manufacturer_data_head(std::get<std::vector<uint8_t>>(encodable_manufacturer_data_head));
}
auto& encodable_services = list[6];
auto& encodable_services = list[5];
if (!encodable_services.IsNull()) {
decoded.set_services(std::get<EncodableList>(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<EncodableList>(list[0]),
std::get<EncodableList>(list[1]));
std::get<EncodableList>(list[1]),
std::get<EncodableList>(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<uint8_t>* data,
const std::vector<uint8_t>* mask)
: company_identifier_(company_identifier ? std::optional<int64_t>(*company_identifier) : std::nullopt),
: company_identifier_(company_identifier),
data_(data ? std::optional<std::vector<uint8_t>>(*data) : std::nullopt),
mask_(mask ? std::optional<std::vector<uint8_t>>(*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<int64_t>(*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<uint8_t>& 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<std::vector<uint8_t>>(encodable_data));
@@ -389,6 +376,47 @@ UniversalManufacturerDataFilter UniversalManufacturerDataFilter::FromEncodableLi
return decoded;
}
// UniversalManufacturerData
UniversalManufacturerData::UniversalManufacturerData(
int64_t company_identifier,
const std::vector<uint8_t>& 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<uint8_t>& UniversalManufacturerData::data() const {
return data_;
}
void UniversalManufacturerData::set_data(const std::vector<uint8_t>& 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<std::vector<uint8_t>>(list[1]));
return decoded;
}
PigeonCodecSerializer::PigeonCodecSerializer() {}
@@ -406,6 +434,8 @@ EncodableValue PigeonCodecSerializer::ReadValueOfType(
return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get<EncodableList>(ReadValue(stream))));
case 133:
return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get<EncodableList>(ReadValue(stream))));
case 134:
return CustomEncodableValue(UniversalManufacturerData::FromEncodableList(std::get<EncodableList>(ReadValue(stream))));
default:
return flutter::StandardCodecSerializer::ReadValueOfType(type, stream);
}
@@ -440,6 +470,11 @@ void PigeonCodecSerializer::WriteValue(
WriteValue(EncodableValue(std::any_cast<UniversalManufacturerDataFilter>(*custom_value).ToEncodableList()), stream);
return;
}
if (custom_value->type() == typeid(UniversalManufacturerData)) {
stream->WriteByte(134);
WriteValue(EncodableValue(std::any_cast<UniversalManufacturerData>(*custom_value).ToEncodableList()), stream);
return;
}
}
flutter::StandardCodecSerializer::WriteValue(value, stream);
}
+41 -16
View File
@@ -69,8 +69,7 @@ class UniversalBleScanResult {
const std::string* name,
const bool* is_paired,
const int64_t* rssi,
const std::vector<uint8_t>* manufacturer_data,
const std::vector<uint8_t>* 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<uint8_t>* manufacturer_data() const;
void set_manufacturer_data(const std::vector<uint8_t>* value_arg);
void set_manufacturer_data(const std::vector<uint8_t>& value_arg);
const std::vector<uint8_t>* manufacturer_data_head() const;
void set_manufacturer_data_head(const std::vector<uint8_t>* value_arg);
void set_manufacturer_data_head(const std::vector<uint8_t>& 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<std::string> name_;
std::optional<bool> is_paired_;
std::optional<int64_t> rssi_;
std::optional<std::vector<uint8_t>> manufacturer_data_;
std::optional<std::vector<uint8_t>> manufacturer_data_head_;
std::optional<flutter::EncodableList> manufacturer_data_list_;
std::optional<flutter::EncodableList> 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<uint8_t>* data,
const std::vector<uint8_t>* 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<uint8_t>* data() const;
@@ -236,12 +234,39 @@ class UniversalManufacturerDataFilter {
friend class UniversalBlePlatformChannel;
friend class UniversalBleCallbackChannel;
friend class PigeonCodecSerializer;
std::optional<int64_t> company_identifier_;
int64_t company_identifier_;
std::optional<std::vector<uint8_t>> data_;
std::optional<std::vector<uint8_t>> 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<uint8_t>& data);
int64_t company_identifier() const;
void set_company_identifier(int64_t value_arg);
const std::vector<uint8_t>& data() const;
void set_data(const std::vector<uint8_t>& 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<uint8_t> data_;
};
class PigeonCodecSerializer : public flutter::StandardCodecSerializer {
public:
PigeonCodecSerializer();
+1 -1
View File
@@ -158,4 +158,4 @@ namespace universal_ble
return rove.dwMajorVersion == 10 && rove.dwBuildNumber >= 22000;
}
} // namespace SimpleBLE
} // namespace universal_ble
+158
View File
@@ -0,0 +1,158 @@
#include <iomanip>
#include <iostream>
#include <sstream>
#include <windows.h>
#include <stdio.h>
#include <sdkddkver.h>
#include <vector>
#include "helper/utils.h"
#include "generated/universal_ble.g.h"
#include <unordered_set>
namespace universal_ble
{
std::vector<UniversalManufacturerDataFilter> manufacturerScanFilter = std::vector<UniversalManufacturerDataFilter>();
std::vector<winrt::guid> serviceFilterUUIDS = std::vector<winrt::guid>();
std::vector<std::string> namePrefixFilter = std::vector<std::string>();
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<UniversalManufacturerDataFilter>(std::get<flutter::CustomEncodableValue>(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<std::string>(uuid)));
}
}
// Set Names Filter
if (!filter.with_name_prefix().empty())
{
for (const auto &name : filter.with_name_prefix())
{
namePrefixFilter.push_back(std::get<std::string>(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<std::string>(prefix)) == 0;
});
}
bool isServicesMatchingFilters(const flutter::EncodableList *services)
{
if (serviceFilterUUIDS.empty())
return true;
if (services == nullptr || services->empty())
return false;
std::unordered_set<winrt::guid> serviceGUIDs;
serviceGUIDs.reserve(services->size());
for (const auto &service : *services)
{
if (const auto *str = std::get_if<std::string>(&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<const UniversalManufacturerData &>(
std::get<flutter::CustomEncodableValue>(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
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
#include <exception>
#include <string>
#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
+40 -137
View File
@@ -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<UniversalBleCallbackChannel> callbackChannel;
std::unordered_map<std::string, winrt::event_token> characteristicsTokens{}; // TODO: Remove the map and store the token inside the characteristic object
std::vector<UniversalManufacturerDataFilter> manufacturerScanFilter = std::vector<UniversalManufacturerDataFilter>();
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<std::string>(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<std::string>(uuid)));
}
}
}
// Set ManufacturerData filter
const auto &manufacturerData = filter->with_manufacturer_data();
for (const flutter::EncodableValue &data : manufacturerData)
{
UniversalManufacturerDataFilter manufacturerDataFilter = std::any_cast<UniversalManufacturerDataFilter>(std::get<flutter::CustomEncodableValue>(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<uint8_t> 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<uint8_t>(companyId & 0xFF);
auto mostSignificantBit = static_cast<uint8_t>(companyId >> 8);
if (isLittleEndian())
{
prefix[0] = leastSignificantBit;
prefix[1] = mostSignificantBit;
}
else
{
prefix[0] = mostSignificantBit;
prefix[1] = leastSignificantBit;
}
std::vector<uint8_t> 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<int64_t>(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<BluetoothLEManufacturerData> deviceManufactureData)
{
if (manufacturerScanFilter.empty())
return true;
for (auto &&filter : manufacturerScanFilter)
{
const int64_t *company_identifier = filter.company_identifier();
const std::vector<uint8_t> *data_filter = filter.data();
const std::vector<uint8_t> *mask = filter.mask();
if (company_identifier == nullptr)
continue;
uint16_t companyId = static_cast<uint16_t>(*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
-1
View File
@@ -124,7 +124,6 @@ namespace universal_ble
winrt::fire_and_forget WriteAsync(GattCharacteristic characteristic, GattWriteOption writeOption,
const std::vector<uint8_t> &value,
std::function<void(std::optional<FlutterError> reply)> result);
bool filterByManufacturerData(IVector<BluetoothLEManufacturerData> deviceManufactureData);
winrt::fire_and_forget PairAsync(std::string device_id, std::function<void(ErrorOr<bool> reply)> result);
winrt::fire_and_forget CustomPairAsync(std::string device_id, std::function<void(ErrorOr<bool> reply)> result);
void PairingRequestedHandler(DeviceInformationCustomPairing sender, DevicePairingRequestedEventArgs eventArgs);