diff --git a/CHANGELOG.md b/CHANGELOG.md index e44d5bc..187b1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 1.2.0 * Add `autoConnect` parameter to `connect()` method for automatic reconnection support on Android and iOS/macOS * Add `serviceData` in `BleDevice` +* Add `AndroidScanMode` and `reportDelayMillis` to `AndroidOptions` for scanning ## 1.1.0 * Add readRssi method diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt index 388b5a5..5bd811a 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt @@ -96,6 +96,20 @@ enum class UniversalBleLogLevel(val raw: Int) { } } +/** Scan config */ +enum class AndroidScanMode(val raw: Int) { + BALANCED(0), + LOW_LATENCY(1), + LOW_POWER(2), + OPPORTUNISTIC(3); + + companion object { + fun ofRaw(raw: Int): AndroidScanMode? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** Unified error codes for all platforms */ enum class UniversalBleErrorCode(val raw: Int) { UNKNOWN_ERROR(0), @@ -309,6 +323,77 @@ data class UniversalBleDescriptor ( override fun hashCode(): Int = toList().hashCode() } +/** + * Android options to scan devices + * [requestLocationPermission] is used to request location permission on Android 12+ (API 31+). + * [scanMode] is used to set the scan mode for for Bluetooth LE scan. + * Set [reportDelayMillis] timestamp for Bluetooth LE scan. If set to 0, you will be notified of scan results immediately. + * If > 0, scan results are queued up and delivered after the requested delay or 5000 milliseconds (whichever is higher). + * Note scan results may be delivered sooner if the internal buffers fill up. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class AndroidOptions ( + val requestLocationPermission: Boolean? = null, + val scanMode: AndroidScanMode? = null, + val reportDelayMillis: Long? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): AndroidOptions { + val requestLocationPermission = pigeonVar_list[0] as Boolean? + val scanMode = pigeonVar_list[1] as AndroidScanMode? + val reportDelayMillis = pigeonVar_list[2] as Long? + return AndroidOptions(requestLocationPermission, scanMode, reportDelayMillis) + } + } + fun toList(): List { + return listOf( + requestLocationPermission, + scanMode, + reportDelayMillis, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is AndroidOptions) { + return false + } + if (this === other) { + return true + } + return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class UniversalScanConfig ( + val android: AndroidOptions? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): UniversalScanConfig { + val android = pigeonVar_list[0] as AndroidOptions? + return UniversalScanConfig(android) + } + } + fun toList(): List { + return listOf( + android, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is UniversalScanConfig) { + return false + } + if (this === other) { + return true + } + return UniversalBlePigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + /** * Scan Filters * @@ -421,40 +506,55 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { } 130.toByte() -> { return (readValue(buffer) as Long?)?.let { - UniversalBleErrorCode.ofRaw(it.toInt()) + AndroidScanMode.ofRaw(it.toInt()) } } 131.toByte() -> { - return (readValue(buffer) as? List)?.let { - UniversalBleScanResult.fromList(it) + return (readValue(buffer) as Long?)?.let { + UniversalBleErrorCode.ofRaw(it.toInt()) } } 132.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalBleService.fromList(it) + UniversalBleScanResult.fromList(it) } } 133.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalBleCharacteristic.fromList(it) + UniversalBleService.fromList(it) } } 134.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalBleDescriptor.fromList(it) + UniversalBleCharacteristic.fromList(it) } } 135.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalScanFilter.fromList(it) + UniversalBleDescriptor.fromList(it) } } 136.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalManufacturerDataFilter.fromList(it) + AndroidOptions.fromList(it) } } 137.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalScanConfig.fromList(it) + } + } + 138.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalScanFilter.fromList(it) + } + } + 139.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalManufacturerDataFilter.fromList(it) + } + } + 140.toByte() -> { return (readValue(buffer) as? List)?.let { UniversalManufacturerData.fromList(it) } @@ -468,38 +568,50 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { stream.write(129) writeValue(stream, value.raw.toLong()) } - is UniversalBleErrorCode -> { + is AndroidScanMode -> { stream.write(130) writeValue(stream, value.raw.toLong()) } - is UniversalBleScanResult -> { + is UniversalBleErrorCode -> { stream.write(131) - writeValue(stream, value.toList()) + writeValue(stream, value.raw.toLong()) } - is UniversalBleService -> { + is UniversalBleScanResult -> { stream.write(132) writeValue(stream, value.toList()) } - is UniversalBleCharacteristic -> { + is UniversalBleService -> { stream.write(133) writeValue(stream, value.toList()) } - is UniversalBleDescriptor -> { + is UniversalBleCharacteristic -> { stream.write(134) writeValue(stream, value.toList()) } - is UniversalScanFilter -> { + is UniversalBleDescriptor -> { stream.write(135) writeValue(stream, value.toList()) } - is UniversalManufacturerDataFilter -> { + is AndroidOptions -> { stream.write(136) writeValue(stream, value.toList()) } - is UniversalManufacturerData -> { + is UniversalScanConfig -> { stream.write(137) writeValue(stream, value.toList()) } + is UniversalScanFilter -> { + stream.write(138) + writeValue(stream, value.toList()) + } + is UniversalManufacturerDataFilter -> { + stream.write(139) + writeValue(stream, value.toList()) + } + is UniversalManufacturerData -> { + stream.write(140) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -517,7 +629,7 @@ interface UniversalBlePlatformChannel { fun requestPermissions(withAndroidFineLocation: Boolean, callback: (Result) -> Unit) fun enableBluetooth(callback: (Result) -> Unit) fun disableBluetooth(callback: (Result) -> Unit) - fun startScan(filter: UniversalScanFilter?) + fun startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) fun stopScan() fun isScanning(): Boolean fun connect(deviceId: String, autoConnect: Boolean?) @@ -640,8 +752,9 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler { message, reply -> val args = message as List val filterArg = args[0] as UniversalScanFilter? + val configArg = args[1] as UniversalScanConfig? val wrapped: List = try { - api.startScan(filterArg) + api.startScan(filterArg, configArg) listOf(null) } catch (exception: Throwable) { UniversalBlePigeonUtils.wrapError(exception) diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt index de08e6a..6a643d1 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -12,6 +12,8 @@ import android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR import android.bluetooth.le.ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES import android.bluetooth.le.ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.os.Build import android.util.Log import android.util.SparseArray import java.nio.ByteBuffer @@ -224,6 +226,20 @@ fun gattStatusToUniversalBleErrorCode(code: Int): UniversalBleErrorCode { } } +fun AndroidScanMode.parse(): Int? { + return when (this) { + AndroidScanMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED + AndroidScanMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY + AndroidScanMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER + AndroidScanMode.OPPORTUNISTIC -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ScanSettings.SCAN_MODE_OPPORTUNISTIC + } else { + UniversalBleLogger.logError("Scan mode OPPORTUNISTIC is not supported on this Android version.") + null + } + } +} + fun Int.parseBluetoothStatusCodeError(): UniversalBleErrorCode? { if (this == BluetoothStatusCodes.SUCCESS) return null return when (this) { diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index 9e7ce04..7a07623 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -160,7 +160,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), bluetoothDisableRequestFuture = callback } - override fun startScan(filter: UniversalScanFilter?) { + override fun startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) { if (!isBluetoothAvailable()) throw createFlutterError( UniversalBleErrorCode.BLUETOOTH_NOT_ENABLED, "Bluetooth not enabled" @@ -171,6 +171,14 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), builder.setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED) builder.setLegacy(false) } + config?.android?.let { androidConfig -> + androidConfig.scanMode?.parse()?.let { scanMode -> + builder.setScanMode(scanMode) + } + androidConfig.reportDelayMillis?.let { reportDelay -> + builder.setReportDelay(reportDelay) + } + } val settings = builder.build() val usesCustomFilters = filter?.usesCustomFilters() ?: false @@ -1151,7 +1159,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } else if (newState == BluetoothGatt.STATE_DISCONNECTED) { val deviceId = gatt.device.address val shouldAutoConnect = autoConnectDevices.contains(deviceId) - + // Always clean up internal state (futures, etc.) cleanUpConnection(deviceId) @@ -1161,7 +1169,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), deviceId, false, status.parseHciErrorCode() ) {} } - + if (!shouldAutoConnect) { // Only close GATT resources when autoConnect is disabled gatt.removeCache() diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index c370e81..b9589a9 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -141,6 +141,14 @@ enum UniversalBleLogLevel: Int { case verbose = 5 } +/// Scan config +enum AndroidScanMode: Int { + case balanced = 0 + case lowLatency = 1 + case lowPower = 2 + case opportunistic = 3 +} + /// Unified error codes for all platforms enum UniversalBleErrorCode: Int { case unknownError = 0 @@ -346,6 +354,71 @@ struct UniversalBleDescriptor: Hashable { } } +/// Android options to scan devices +/// [requestLocationPermission] is used to request location permission on Android 12+ (API 31+). +/// [scanMode] is used to set the scan mode for for Bluetooth LE scan. +/// Set [reportDelayMillis] timestamp for Bluetooth LE scan. If set to 0, you will be notified of scan results immediately. +/// If > 0, scan results are queued up and delivered after the requested delay or 5000 milliseconds (whichever is higher). +/// Note scan results may be delivered sooner if the internal buffers fill up. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct AndroidOptions: Hashable { + var requestLocationPermission: Bool? = nil + var scanMode: AndroidScanMode? = nil + var reportDelayMillis: Int64? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> AndroidOptions? { + let requestLocationPermission: Bool? = nilOrValue(pigeonVar_list[0]) + let scanMode: AndroidScanMode? = nilOrValue(pigeonVar_list[1]) + let reportDelayMillis: Int64? = nilOrValue(pigeonVar_list[2]) + + return AndroidOptions( + requestLocationPermission: requestLocationPermission, + scanMode: scanMode, + reportDelayMillis: reportDelayMillis + ) + } + func toList() -> [Any?] { + return [ + requestLocationPermission, + scanMode, + reportDelayMillis, + ] + } + static func == (lhs: AndroidOptions, rhs: AndroidOptions) -> Bool { + return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashUniversalBle(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct UniversalScanConfig: Hashable { + var android: AndroidOptions? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> UniversalScanConfig? { + let android: AndroidOptions? = nilOrValue(pigeonVar_list[0]) + + return UniversalScanConfig( + android: android + ) + } + func toList() -> [Any?] { + return [ + android + ] + } + static func == (lhs: UniversalScanConfig, rhs: UniversalScanConfig) -> Bool { + return deepEqualsUniversalBle(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashUniversalBle(value: toList(), hasher: &hasher) + } +} + /// Scan Filters /// /// Generated class from Pigeon that represents data sent in messages. @@ -455,22 +528,32 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader { case 130: let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) if let enumResultAsInt = enumResultAsInt { - return UniversalBleErrorCode(rawValue: enumResultAsInt) + return AndroidScanMode(rawValue: enumResultAsInt) } return nil case 131: - return UniversalBleScanResult.fromList(self.readValue() as! [Any?]) + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return UniversalBleErrorCode(rawValue: enumResultAsInt) + } + return nil case 132: - return UniversalBleService.fromList(self.readValue() as! [Any?]) + return UniversalBleScanResult.fromList(self.readValue() as! [Any?]) case 133: - return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?]) + return UniversalBleService.fromList(self.readValue() as! [Any?]) case 134: - return UniversalBleDescriptor.fromList(self.readValue() as! [Any?]) + return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?]) case 135: - return UniversalScanFilter.fromList(self.readValue() as! [Any?]) + return UniversalBleDescriptor.fromList(self.readValue() as! [Any?]) case 136: - return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + return AndroidOptions.fromList(self.readValue() as! [Any?]) case 137: + return UniversalScanConfig.fromList(self.readValue() as! [Any?]) + case 138: + return UniversalScanFilter.fromList(self.readValue() as! [Any?]) + case 139: + return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + case 140: return UniversalManufacturerData.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) @@ -483,30 +566,39 @@ private class UniversalBlePigeonCodecWriter: FlutterStandardWriter { if let value = value as? UniversalBleLogLevel { super.writeByte(129) super.writeValue(value.rawValue) - } else if let value = value as? UniversalBleErrorCode { + } else if let value = value as? AndroidScanMode { super.writeByte(130) super.writeValue(value.rawValue) - } else if let value = value as? UniversalBleScanResult { + } else if let value = value as? UniversalBleErrorCode { super.writeByte(131) - super.writeValue(value.toList()) - } else if let value = value as? UniversalBleService { + super.writeValue(value.rawValue) + } else if let value = value as? UniversalBleScanResult { super.writeByte(132) super.writeValue(value.toList()) - } else if let value = value as? UniversalBleCharacteristic { + } else if let value = value as? UniversalBleService { super.writeByte(133) super.writeValue(value.toList()) - } else if let value = value as? UniversalBleDescriptor { + } else if let value = value as? UniversalBleCharacteristic { super.writeByte(134) super.writeValue(value.toList()) - } else if let value = value as? UniversalScanFilter { + } else if let value = value as? UniversalBleDescriptor { super.writeByte(135) super.writeValue(value.toList()) - } else if let value = value as? UniversalManufacturerDataFilter { + } else if let value = value as? AndroidOptions { super.writeByte(136) super.writeValue(value.toList()) - } else if let value = value as? UniversalManufacturerData { + } else if let value = value as? UniversalScanConfig { super.writeByte(137) super.writeValue(value.toList()) + } else if let value = value as? UniversalScanFilter { + super.writeByte(138) + super.writeValue(value.toList()) + } else if let value = value as? UniversalManufacturerDataFilter { + super.writeByte(139) + super.writeValue(value.toList()) + } else if let value = value as? UniversalManufacturerData { + super.writeByte(140) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -537,7 +629,7 @@ protocol UniversalBlePlatformChannel { func requestPermissions(withAndroidFineLocation: Bool, completion: @escaping (Result) -> Void) func enableBluetooth(completion: @escaping (Result) -> Void) func disableBluetooth(completion: @escaping (Result) -> Void) - func startScan(filter: UniversalScanFilter?) throws + func startScan(filter: UniversalScanFilter?, config: UniversalScanConfig?) throws func stopScan() throws func isScanning() throws -> Bool func connect(deviceId: String, autoConnect: Bool?) throws @@ -644,8 +736,9 @@ class UniversalBlePlatformChannelSetup { startScanChannel.setMessageHandler { message, reply in let args = message as! [Any?] let filterArg: UniversalScanFilter? = nilOrValue(args[0]) + let configArg: UniversalScanConfig? = nilOrValue(args[1]) do { - try api.startScan(filter: filterArg) + try api.startScan(filter: filterArg, config: configArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index 1d86595..90b25b1 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -91,7 +91,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral completion(Result.failure(createFlutterError(code: .notSupported))) } - func startScan(filter: UniversalScanFilter?) throws { + func startScan(filter: UniversalScanFilter?, config _: UniversalScanConfig?) throws { // If filter has any other filter other than official one let usesCustomFilters = filter?.usesCustomFilters ?? false diff --git a/example/lib/data/mock_universal_ble.dart b/example/lib/data/mock_universal_ble.dart index a00813e..0c7d7a5 100644 --- a/example/lib/data/mock_universal_ble.dart +++ b/example/lib/data/mock_universal_ble.dart @@ -52,7 +52,8 @@ class MockUniversalBle extends UniversalBlePlatform { } @override - Future connect(String deviceId, {bool autoConnect = false, Duration? connectionTimeout}) async { + Future connect(String deviceId, + {bool autoConnect = false, Duration? connectionTimeout}) async { updateConnection(deviceId, true); _connectionStateMap[deviceId] = BleConnectionState.connected; } diff --git a/example/lib/home/scanner_screen.dart b/example/lib/home/scanner_screen.dart index 54bec23..064cb6c 100644 --- a/example/lib/home/scanner_screen.dart +++ b/example/lib/home/scanner_screen.dart @@ -84,7 +84,11 @@ class _ScannerScreenState extends State { _isScanning = true; }); try { - PlatformConfig? platformConfig; + PlatformConfig platformConfig = PlatformConfig( + android: AndroidOptions( + scanMode: AndroidScanMode.lowLatency, + ), + ); if (kIsWeb && _webServicesController.text.isNotEmpty) { List webServices = _webServicesController.text .split(',') @@ -96,10 +100,9 @@ class _ScannerScreenState extends State { return s.trim(); } }).toList(); - platformConfig = PlatformConfig( - web: WebOptions(optionalServices: webServices), - ); + platformConfig.web = WebOptions(optionalServices: webServices); } + await UniversalBle.startScan( scanFilter: scanFilter, platformConfig: platformConfig, diff --git a/example/lib/home/widgets/drawer.dart b/example/lib/home/widgets/drawer.dart index 0ebabec..dc4a1b0 100644 --- a/example/lib/home/widgets/drawer.dart +++ b/example/lib/home/widgets/drawer.dart @@ -9,7 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; class AppDrawer extends StatefulWidget { final QueueType? queueType; final Function(QueueType)? onQueueTypeChanged; - + const AppDrawer({ super.key, this.queueType, @@ -87,7 +87,8 @@ class _AppDrawerState extends State { ), children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( children: [ _buildQueueOption( diff --git a/example/lib/home/widgets/scan_filter_widget.dart b/example/lib/home/widgets/scan_filter_widget.dart index d1ed207..adfbc98 100644 --- a/example/lib/home/widgets/scan_filter_widget.dart +++ b/example/lib/home/widgets/scan_filter_widget.dart @@ -66,7 +66,8 @@ class _ScanFilterWidgetState extends State { .where((s) => s.isNotEmpty) .toList(); for (String manufacturer in manufacturerData) { - final companyIdentifier = companyService.parseCompanyIdentifier(manufacturer); + final companyIdentifier = + companyService.parseCompanyIdentifier(manufacturer); if (companyIdentifier == null) { throw Exception( diff --git a/example/lib/widgets/company_info_widget.dart b/example/lib/widgets/company_info_widget.dart index 750a029..a55d9eb 100644 --- a/example/lib/widgets/company_info_widget.dart +++ b/example/lib/widgets/company_info_widget.dart @@ -2,23 +2,23 @@ import 'package:flutter/material.dart'; import 'package:universal_ble_example/data/company_identifier_service.dart'; /// A reusable widget that displays company information for a given company ID. -/// +/// /// This widget fetches the company name from the CompanyIdentifierService /// and displays it in a consistent format. If no company name is found, /// the widget returns an empty SizedBox. class CompanyInfoWidget extends StatelessWidget { /// The company ID to look up final int companyId; - + /// Optional text style for the "Company:" label final TextStyle? labelStyle; - + /// Optional text style for the company name final TextStyle? nameStyle; - + /// Optional padding around the widget final EdgeInsets? padding; - + /// Optional color scheme. If not provided, will be obtained from Theme final ColorScheme? colorScheme; @@ -33,8 +33,9 @@ class CompanyInfoWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final companyName = CompanyIdentifierService.instance.getCompanyName(companyId); - + final companyName = + CompanyIdentifierService.instance.getCompanyName(companyId); + if (companyName == null) { return const SizedBox.shrink(); } diff --git a/lib/src/extensions/ble_device_extension.dart b/lib/src/extensions/ble_device_extension.dart index 11bda12..25e0e66 100644 --- a/lib/src/extensions/ble_device_extension.dart +++ b/lib/src/extensions/ble_device_extension.dart @@ -1,4 +1,3 @@ -import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; import 'package:universal_ble/src/utils/cache_handler.dart'; import 'package:universal_ble/universal_ble.dart'; @@ -19,7 +18,8 @@ extension BleDeviceExtension on BleDevice { /// Connects to the device. /// [autoConnect] enables automatic reconnection when the device becomes available. Future connect({bool autoConnect = false, Duration? timeout}) => - UniversalBle.connect(deviceId, autoConnect: autoConnect, timeout: timeout); + UniversalBle.connect(deviceId, + autoConnect: autoConnect, timeout: timeout); /// Disconnects from the device. Future disconnect() => UniversalBle.disconnect(deviceId); diff --git a/lib/src/extensions/ble_service_extension.dart b/lib/src/extensions/ble_service_extension.dart index b1206ec..378b4f3 100644 --- a/lib/src/extensions/ble_service_extension.dart +++ b/lib/src/extensions/ble_service_extension.dart @@ -1,4 +1,3 @@ -import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; import 'package:universal_ble/universal_ble.dart'; /// Extension methods for [BleService] objects. diff --git a/lib/src/models/platform_config.dart b/lib/src/models/platform_config.dart index d0d0b0c..7f957a7 100644 --- a/lib/src/models/platform_config.dart +++ b/lib/src/models/platform_config.dart @@ -1,4 +1,8 @@ +import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; + /// Platform specific config to scan devices +/// +/// If more than 1 platform supports a certain parameter then it should be in the high level APIs instead of platform specific options. class PlatformConfig { WebOptions? web; AndroidOptions? android; @@ -22,10 +26,3 @@ class WebOptions { this.optionalManufacturerData = const [], }); } - -/// Android options to scan devices -class AndroidOptions { - bool requestLocationPermission; - - AndroidOptions({this.requestLocationPermission = true}); -} diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index ab07edc..aa5e87a 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -50,6 +50,14 @@ enum UniversalBleLogLevel { verbose, } +/// Scan config +enum AndroidScanMode { + balanced, + lowLatency, + lowPower, + opportunistic, +} + /// Unified error codes for all platforms enum UniversalBleErrorCode { unknownError, @@ -334,6 +342,104 @@ class UniversalBleDescriptor { int get hashCode => Object.hashAll(_toList()); } +/// Android options to scan devices +/// [requestLocationPermission] is used to request location permission on Android 12+ (API 31+). +/// [scanMode] is used to set the scan mode for for Bluetooth LE scan. +/// Set [reportDelayMillis] timestamp for Bluetooth LE scan. If set to 0, you will be notified of scan results immediately. +/// If > 0, scan results are queued up and delivered after the requested delay or 5000 milliseconds (whichever is higher). +/// Note scan results may be delivered sooner if the internal buffers fill up. +class AndroidOptions { + AndroidOptions({ + this.requestLocationPermission, + this.scanMode, + this.reportDelayMillis, + }); + + bool? requestLocationPermission; + + AndroidScanMode? scanMode; + + int? reportDelayMillis; + + List _toList() { + return [ + requestLocationPermission, + scanMode, + reportDelayMillis, + ]; + } + + Object encode() { + return _toList(); + } + + static AndroidOptions decode(Object result) { + result as List; + return AndroidOptions( + requestLocationPermission: result[0] as bool?, + scanMode: result[1] as AndroidScanMode?, + reportDelayMillis: result[2] as int?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! AndroidOptions || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class UniversalScanConfig { + UniversalScanConfig({ + this.android, + }); + + AndroidOptions? android; + + List _toList() { + return [ + android, + ]; + } + + Object encode() { + return _toList(); + } + + static UniversalScanConfig decode(Object result) { + result as List; + return UniversalScanConfig( + android: result[0] as AndroidOptions?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! UniversalScanConfig || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + /// Scan Filters class UniversalScanFilter { UniversalScanFilter({ @@ -496,30 +602,39 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is UniversalBleLogLevel) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is UniversalBleErrorCode) { + } else if (value is AndroidScanMode) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is UniversalBleScanResult) { + } else if (value is UniversalBleErrorCode) { buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else if (value is UniversalBleService) { + writeValue(buffer, value.index); + } else if (value is UniversalBleScanResult) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is UniversalBleCharacteristic) { + } else if (value is UniversalBleService) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is UniversalBleDescriptor) { + } else if (value is UniversalBleCharacteristic) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is UniversalScanFilter) { + } else if (value is UniversalBleDescriptor) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is UniversalManufacturerDataFilter) { + } else if (value is AndroidOptions) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is UniversalManufacturerData) { + } else if (value is UniversalScanConfig) { buffer.putUint8(137); writeValue(buffer, value.encode()); + } else if (value is UniversalScanFilter) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else if (value is UniversalManufacturerDataFilter) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); + } else if (value is UniversalManufacturerData) { + buffer.putUint8(140); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -533,20 +648,27 @@ class _PigeonCodec extends StandardMessageCodec { return value == null ? null : UniversalBleLogLevel.values[value]; case 130: final value = readValue(buffer) as int?; - return value == null ? null : UniversalBleErrorCode.values[value]; + return value == null ? null : AndroidScanMode.values[value]; case 131: - return UniversalBleScanResult.decode(readValue(buffer)!); + final value = readValue(buffer) as int?; + return value == null ? null : UniversalBleErrorCode.values[value]; case 132: - return UniversalBleService.decode(readValue(buffer)!); + return UniversalBleScanResult.decode(readValue(buffer)!); case 133: - return UniversalBleCharacteristic.decode(readValue(buffer)!); + return UniversalBleService.decode(readValue(buffer)!); case 134: - return UniversalBleDescriptor.decode(readValue(buffer)!); + return UniversalBleCharacteristic.decode(readValue(buffer)!); case 135: - return UniversalScanFilter.decode(readValue(buffer)!); + return UniversalBleDescriptor.decode(readValue(buffer)!); case 136: - return UniversalManufacturerDataFilter.decode(readValue(buffer)!); + return AndroidOptions.decode(readValue(buffer)!); case 137: + return UniversalScanConfig.decode(readValue(buffer)!); + case 138: + return UniversalScanFilter.decode(readValue(buffer)!); + case 139: + return UniversalManufacturerDataFilter.decode(readValue(buffer)!); + case 140: return UniversalManufacturerData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -707,7 +829,8 @@ class UniversalBlePlatformChannel { } } - Future startScan(UniversalScanFilter? filter) async { + Future startScan( + UniversalScanFilter? filter, UniversalScanConfig? config) async { final pigeonVar_channelName = 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( @@ -716,7 +839,7 @@ class UniversalBlePlatformChannel { binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = - pigeonVar_channel.send([filter]); + pigeonVar_channel.send([filter, config]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); diff --git a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart index f8330b5..92811b7 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -49,6 +49,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { await _executeWithErrorHandling( () => _channel.startScan( scanFilter.toUniversalScanFilter(), + platformConfig?.toUniversalScanConfig(), ), ); } @@ -69,8 +70,10 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { } @override - Future connect(String deviceId, {Duration? connectionTimeout, bool autoConnect = false}) => - _executeWithErrorHandling(() => _channel.connect(deviceId, autoConnect: autoConnect)); + Future connect(String deviceId, + {Duration? connectionTimeout, bool autoConnect = false}) => + _executeWithErrorHandling( + () => _channel.connect(deviceId, autoConnect: autoConnect)); @override Future disconnect(String deviceId) => @@ -329,3 +332,11 @@ extension _BleLogLevelExtension on BleLogLevel { BleLogLevel.verbose => UniversalBleLogLevel.verbose, }; } + +extension _PlatformConfigExtension on PlatformConfig? { + UniversalScanConfig? toUniversalScanConfig() { + return UniversalScanConfig( + android: this?.android, + ); + } +} diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index fbb14be..fdcf8ff 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -53,7 +53,8 @@ abstract class UniversalBlePlatform { Future isScanning(); - Future connect(String deviceId, {Duration? connectionTimeout, bool autoConnect = false}); + Future connect(String deviceId, + {Duration? connectionTimeout, bool autoConnect = false}); Future disconnect(String deviceId); diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 876c139..6e8d686 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -4,7 +4,6 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart'; import 'package:universal_ble/src/utils/universal_logger.dart'; -import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; import 'package:universal_ble/universal_ble.dart'; class UniversalBleWeb extends UniversalBlePlatform { diff --git a/lib/universal_ble.dart b/lib/universal_ble.dart index 3c3d40c..aaaeec6 100644 --- a/lib/universal_ble.dart +++ b/lib/universal_ble.dart @@ -5,3 +5,9 @@ export 'package:universal_ble/src/universal_ble_platform_interface.dart'; export 'package:universal_ble/src/universal_ble.dart'; export 'package:universal_ble/src/models/model_exports.dart'; export 'package:universal_ble/src/extensions/exports.dart'; +export 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart' + show + AndroidOptions, + AndroidScanMode, + UniversalBleErrorCode, + UniversalBleLogLevel; diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index f8b4a5f..b06e84e 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -35,7 +35,7 @@ abstract class UniversalBlePlatformChannel { @async bool disableBluetooth(); - void startScan(UniversalScanFilter? filter); + void startScan(UniversalScanFilter? filter, UniversalScanConfig? config); void stopScan(); @@ -171,6 +171,37 @@ class UniversalBleDescriptor { UniversalBleDescriptor(this.uuid); } +/// Scan config + +enum AndroidScanMode { + balanced, + lowLatency, + lowPower, + opportunistic, +} + +/// Android options to scan devices +/// [requestLocationPermission] is used to request location permission on Android 12+ (API 31+). +/// [scanMode] is used to set the scan mode for for Bluetooth LE scan. +/// Set [reportDelayMillis] timestamp for Bluetooth LE scan. If set to 0, you will be notified of scan results immediately. +/// If > 0, scan results are queued up and delivered after the requested delay or 5000 milliseconds (whichever is higher). +/// Note scan results may be delivered sooner if the internal buffers fill up. +class AndroidOptions { + bool? requestLocationPermission; + AndroidScanMode? scanMode; + int? reportDelayMillis; + AndroidOptions({ + this.requestLocationPermission, + this.scanMode, + this.reportDelayMillis, + }); +} + +class UniversalScanConfig { + AndroidOptions? android; + UniversalScanConfig(this.android); +} + /// Scan Filters class UniversalScanFilter { final List withServices; diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index fc54e8b..819a67b 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -331,6 +331,127 @@ UniversalBleDescriptor UniversalBleDescriptor::FromEncodableList(const Encodable return decoded; } +// AndroidOptions + +AndroidOptions::AndroidOptions() {} + +AndroidOptions::AndroidOptions( + const bool* request_location_permission, + const AndroidScanMode* scan_mode, + const int64_t* report_delay_millis) + : request_location_permission_(request_location_permission ? std::optional(*request_location_permission) : std::nullopt), + scan_mode_(scan_mode ? std::optional(*scan_mode) : std::nullopt), + report_delay_millis_(report_delay_millis ? std::optional(*report_delay_millis) : std::nullopt) {} + +const bool* AndroidOptions::request_location_permission() const { + return request_location_permission_ ? &(*request_location_permission_) : nullptr; +} + +void AndroidOptions::set_request_location_permission(const bool* value_arg) { + request_location_permission_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void AndroidOptions::set_request_location_permission(bool value_arg) { + request_location_permission_ = value_arg; +} + + +const AndroidScanMode* AndroidOptions::scan_mode() const { + return scan_mode_ ? &(*scan_mode_) : nullptr; +} + +void AndroidOptions::set_scan_mode(const AndroidScanMode* value_arg) { + scan_mode_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void AndroidOptions::set_scan_mode(const AndroidScanMode& value_arg) { + scan_mode_ = value_arg; +} + + +const int64_t* AndroidOptions::report_delay_millis() const { + return report_delay_millis_ ? &(*report_delay_millis_) : nullptr; +} + +void AndroidOptions::set_report_delay_millis(const int64_t* value_arg) { + report_delay_millis_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void AndroidOptions::set_report_delay_millis(int64_t value_arg) { + report_delay_millis_ = value_arg; +} + + +EncodableList AndroidOptions::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(request_location_permission_ ? EncodableValue(*request_location_permission_) : EncodableValue()); + list.push_back(scan_mode_ ? CustomEncodableValue(*scan_mode_) : EncodableValue()); + list.push_back(report_delay_millis_ ? EncodableValue(*report_delay_millis_) : EncodableValue()); + return list; +} + +AndroidOptions AndroidOptions::FromEncodableList(const EncodableList& list) { + AndroidOptions decoded; + auto& encodable_request_location_permission = list[0]; + if (!encodable_request_location_permission.IsNull()) { + decoded.set_request_location_permission(std::get(encodable_request_location_permission)); + } + auto& encodable_scan_mode = list[1]; + if (!encodable_scan_mode.IsNull()) { + decoded.set_scan_mode(std::any_cast(std::get(encodable_scan_mode))); + } + auto& encodable_report_delay_millis = list[2]; + if (!encodable_report_delay_millis.IsNull()) { + decoded.set_report_delay_millis(std::get(encodable_report_delay_millis)); + } + return decoded; +} + +// UniversalScanConfig + +UniversalScanConfig::UniversalScanConfig() {} + +UniversalScanConfig::UniversalScanConfig(const AndroidOptions* android) + : android_(android ? std::make_unique(*android) : nullptr) {} + +UniversalScanConfig::UniversalScanConfig(const UniversalScanConfig& other) + : android_(other.android_ ? std::make_unique(*other.android_) : nullptr) {} + +UniversalScanConfig& UniversalScanConfig::operator=(const UniversalScanConfig& other) { + android_ = other.android_ ? std::make_unique(*other.android_) : nullptr; + return *this; +} + +const AndroidOptions* UniversalScanConfig::android() const { + return android_.get(); +} + +void UniversalScanConfig::set_android(const AndroidOptions* value_arg) { + android_ = value_arg ? std::make_unique(*value_arg) : nullptr; +} + +void UniversalScanConfig::set_android(const AndroidOptions& value_arg) { + android_ = std::make_unique(value_arg); +} + + +EncodableList UniversalScanConfig::ToEncodableList() const { + EncodableList list; + list.reserve(1); + list.push_back(android_ ? CustomEncodableValue(*android_) : EncodableValue()); + return list; +} + +UniversalScanConfig UniversalScanConfig::FromEncodableList(const EncodableList& list) { + UniversalScanConfig decoded; + auto& encodable_android = list[0]; + if (!encodable_android.IsNull()) { + decoded.set_android(std::any_cast(std::get(encodable_android))); + } + return decoded; +} + // UniversalScanFilter UniversalScanFilter::UniversalScanFilter( @@ -512,27 +633,38 @@ EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( case 130: { const auto& encodable_enum_arg = ReadValue(stream); const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); - return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); } case 131: { - return CustomEncodableValue(UniversalBleScanResult::FromEncodableList(std::get(ReadValue(stream)))); + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); } case 132: { - return CustomEncodableValue(UniversalBleService::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleScanResult::FromEncodableList(std::get(ReadValue(stream)))); } case 133: { - return CustomEncodableValue(UniversalBleCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleService::FromEncodableList(std::get(ReadValue(stream)))); } case 134: { - return CustomEncodableValue(UniversalBleDescriptor::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); } case 135: { - return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleDescriptor::FromEncodableList(std::get(ReadValue(stream)))); } case 136: { - return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(AndroidOptions::FromEncodableList(std::get(ReadValue(stream)))); } case 137: { + return CustomEncodableValue(UniversalScanConfig::FromEncodableList(std::get(ReadValue(stream)))); + } + case 138: { + return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); + } + case 139: { + return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + } + case 140: { return CustomEncodableValue(UniversalManufacturerData::FromEncodableList(std::get(ReadValue(stream)))); } default: @@ -549,43 +681,58 @@ void PigeonInternalCodecSerializer::WriteValue( WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); return; } - if (custom_value->type() == typeid(UniversalBleErrorCode)) { + if (custom_value->type() == typeid(AndroidScanMode)) { stream->WriteByte(130); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(UniversalBleErrorCode)) { + stream->WriteByte(131); WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); return; } if (custom_value->type() == typeid(UniversalBleScanResult)) { - stream->WriteByte(131); + stream->WriteByte(132); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalBleService)) { - stream->WriteByte(132); + stream->WriteByte(133); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalBleCharacteristic)) { - stream->WriteByte(133); + stream->WriteByte(134); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalBleDescriptor)) { - stream->WriteByte(134); + stream->WriteByte(135); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } + if (custom_value->type() == typeid(AndroidOptions)) { + stream->WriteByte(136); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(UniversalScanConfig)) { + stream->WriteByte(137); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } if (custom_value->type() == typeid(UniversalScanFilter)) { - stream->WriteByte(135); + stream->WriteByte(138); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalManufacturerDataFilter)) { - stream->WriteByte(136); + stream->WriteByte(139); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalManufacturerData)) { - stream->WriteByte(137); + stream->WriteByte(140); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } @@ -741,7 +888,9 @@ void UniversalBlePlatformChannel::SetUp( const auto& args = std::get(message); const auto& encodable_filter_arg = args.at(0); const auto* filter_arg = encodable_filter_arg.IsNull() ? nullptr : &(std::any_cast(std::get(encodable_filter_arg))); - std::optional output = api->StartScan(filter_arg); + const auto& encodable_config_arg = args.at(1); + const auto* config_arg = encodable_config_arg.IsNull() ? nullptr : &(std::any_cast(std::get(encodable_config_arg))); + std::optional output = api->StartScan(filter_arg, config_arg); if (output.has_value()) { reply(WrapError(output.value())); return; diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index e7296c2..2c1f82e 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -66,6 +66,14 @@ enum class UniversalBleLogLevel { kVerbose = 5 }; +// Scan config +enum class AndroidScanMode { + kBalanced = 0, + kLowLatency = 1, + kLowPower = 2, + kOpportunistic = 3 +}; + // Unified error codes for all platforms enum class UniversalBleErrorCode { kUnknownError = 0, @@ -275,6 +283,78 @@ class UniversalBleDescriptor { }; +// Android options to scan devices +// [requestLocationPermission] is used to request location permission on Android 12+ (API 31+). +// [scanMode] is used to set the scan mode for for Bluetooth LE scan. +// Set [reportDelayMillis] timestamp for Bluetooth LE scan. If set to 0, you will be notified of scan results immediately. +// If > 0, scan results are queued up and delivered after the requested delay or 5000 milliseconds (whichever is higher). +// Note scan results may be delivered sooner if the internal buffers fill up. +// +// Generated class from Pigeon that represents data sent in messages. +class AndroidOptions { + public: + // Constructs an object setting all non-nullable fields. + AndroidOptions(); + + // Constructs an object setting all fields. + explicit AndroidOptions( + const bool* request_location_permission, + const AndroidScanMode* scan_mode, + const int64_t* report_delay_millis); + + const bool* request_location_permission() const; + void set_request_location_permission(const bool* value_arg); + void set_request_location_permission(bool value_arg); + + const AndroidScanMode* scan_mode() const; + void set_scan_mode(const AndroidScanMode* value_arg); + void set_scan_mode(const AndroidScanMode& value_arg); + + const int64_t* report_delay_millis() const; + void set_report_delay_millis(const int64_t* value_arg); + void set_report_delay_millis(int64_t value_arg); + + private: + static AndroidOptions FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class UniversalScanConfig; + friend class UniversalBlePlatformChannel; + friend class UniversalBleCallbackChannel; + friend class PigeonInternalCodecSerializer; + std::optional request_location_permission_; + std::optional scan_mode_; + std::optional report_delay_millis_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class UniversalScanConfig { + public: + // Constructs an object setting all non-nullable fields. + UniversalScanConfig(); + + // Constructs an object setting all fields. + explicit UniversalScanConfig(const AndroidOptions* android); + + ~UniversalScanConfig() = default; + UniversalScanConfig(const UniversalScanConfig& other); + UniversalScanConfig& operator=(const UniversalScanConfig& other); + UniversalScanConfig(UniversalScanConfig&& other) = default; + UniversalScanConfig& operator=(UniversalScanConfig&& other) noexcept = default; + const AndroidOptions* android() const; + void set_android(const AndroidOptions* value_arg); + void set_android(const AndroidOptions& value_arg); + + private: + static UniversalScanConfig FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class UniversalBlePlatformChannel; + friend class UniversalBleCallbackChannel; + friend class PigeonInternalCodecSerializer; + std::unique_ptr android_; +}; + + // Scan Filters // // Generated class from Pigeon that represents data sent in messages. @@ -399,7 +479,9 @@ class UniversalBlePlatformChannel { std::function reply)> result) = 0; virtual void EnableBluetooth(std::function reply)> result) = 0; virtual void DisableBluetooth(std::function reply)> result) = 0; - virtual std::optional StartScan(const UniversalScanFilter* filter) = 0; + virtual std::optional StartScan( + const UniversalScanFilter* filter, + const UniversalScanConfig* config) = 0; virtual std::optional StopScan() = 0; virtual ErrorOr IsScanning() = 0; virtual std::optional Connect( diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index 7c164e5..938ff88 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -134,7 +134,7 @@ void UniversalBlePlugin::RequestPermissions( } std::optional -UniversalBlePlugin::StartScan(const UniversalScanFilter *filter) { +UniversalBlePlugin::StartScan(const UniversalScanFilter *filter, const UniversalScanConfig *config) { if (!bluetooth_radio_ || bluetooth_radio_.State() != RadioState::On) { return create_flutter_error(UniversalBleErrorCode::kBluetoothNotAvailable, diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 2e033c3..bdf8a58 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -174,7 +174,7 @@ private: std::optional SetLogLevel(const UniversalBleLogLevel &log_level) override; std::optional - StartScan(const UniversalScanFilter *filter) override; + StartScan(const UniversalScanFilter *filter, const UniversalScanConfig *config) override; std::optional StopScan() override; ErrorOr IsScanning() override; std::optional Connect(const std::string &device_id, const bool *auto_connect) override;