From 89abb3bf0335137f1b7168b5d14c93a4e21816d1 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 12 Dec 2025 18:56:43 +0530 Subject: [PATCH] Implement Logging (#199) * Implement Logging * Add timestamp in OnValueChanged * improve web and linux logging * Implement windows and format pigeon * Fix windows build and unity log level * Log commands with timestamp * Fix code doc * Remove unnecessary import * Update docs * Fix crash on windows 11 because of failed pairing * Update changelog * Fix native code formatting --- CHANGELOG.md | 3 + README.md | 17 + .../com/navideck/universal_ble/SafeScanner.kt | 4 +- .../navideck/universal_ble/UniversalBle.g.kt | 81 +++- .../universal_ble/UniversalBleLogger.kt | 49 +++ .../universal_ble/UniversalBlePlugin.kt | 54 ++- darwin/Classes/UniversalBle.g.swift | 72 +++- .../UniversalBleAsyncServiceDiscovery.swift | 2 +- darwin/Classes/UniversalBleLogger.swift | 53 +++ darwin/Classes/UniversalBlePlugin.swift | 33 +- example/lib/main.dart | 5 +- .../peripheral_detail_page.dart | 9 +- example/pubspec.lock | 8 +- lib/src/extensions/ble_device_extension.dart | 4 +- lib/src/models/ble_log_level.dart | 8 + lib/src/models/model_exports.dart | 1 + lib/src/universal_ble.dart | 8 + .../universal_ble_linux.dart | 25 ++ .../universal_ble_pigeon/universal_ble.g.dart | 80 +++- .../universal_ble_pigeon_channel.dart | 23 +- lib/src/universal_ble_platform_interface.dart | 18 +- .../universal_ble_web/universal_ble_web.dart | 22 ++ lib/src/utils/universal_logger.dart | 70 +++- pigeon/universal_ble.dart | 12 + test/ble_characteristic_test.dart | 1 + windows/CMakeLists.txt | 2 + windows/src/generated/universal_ble.g.cpp | 70 +++- windows/src/generated/universal_ble.g.h | 11 + windows/src/helper/universal_ble_logger.cpp | 84 +++++ windows/src/helper/universal_ble_logger.h | 28 ++ windows/src/universal_ble_plugin.cpp | 156 +++++--- windows/src/universal_ble_plugin.h | 349 +++++++++--------- 32 files changed, 1016 insertions(+), 346 deletions(-) create mode 100644 android/src/main/kotlin/com/navideck/universal_ble/UniversalBleLogger.kt create mode 100644 darwin/Classes/UniversalBleLogger.swift create mode 100644 lib/src/models/ble_log_level.dart create mode 100644 windows/src/helper/universal_ble_logger.cpp create mode 100644 windows/src/helper/universal_ble_logger.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c36dd1..9568284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ ## 1.0.0 +* BREAKING CHANGE: `OnValueChange` callback also includes timestamp +* Fix Windows 11 crash on cancelling pairing * Unified error codes for all platforms * Add `isScanning` api * Add `requestPermissions` api and auto ask permission on `startScan` @@ -6,6 +8,7 @@ * Improve Windows disconnection event handling and cleanup * Add `withDescriptors` parameter in `discoverServices` API * Add `timestamp` in `BleDevice` +* Add `setLogLevel` api for configuring logs ## 0.21.1 * Fix device name resolution on Windows diff --git a/README.md b/README.md index 86688b6..7c8c704 100644 --- a/README.md +++ b/README.md @@ -745,6 +745,23 @@ class UniversalBleMock extends UniversalBlePlatform { UniversalBle.setInstance(UniversalBleMock()); ``` +## Logging + +Configure logging to help debug Ble operations + +### Usage + +Set the log level during app initialization, default level is `none` + +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + // Enable verbose logging to see all BLE operations + await UniversalBle.setLogLevel(BleLogLevel.verbose); + runApp(MyApp()); +} +``` + ## Low level API For more granular control, you can use the [Low-Level API](README.low_level.md). This API is "Device ID"-based, offering greater flexibility by enabling direct calls without the need for object instances. diff --git a/android/src/main/kotlin/com/navideck/universal_ble/SafeScanner.kt b/android/src/main/kotlin/com/navideck/universal_ble/SafeScanner.kt index 0f16674..a14d775 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/SafeScanner.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/SafeScanner.kt @@ -18,13 +18,13 @@ private const val TAG = "UniversalBlePlugin" /** * A safe wrapper for Bluetooth LE scanning operations that prevents excessive scanning. - * + * * This class manages BLE scanning while adhering to Android's scanning frequency limits by: * - Tracking scan start times over a 30-second window * - Limiting to 5 scan operations within this window * - Automatically scheduling delayed scans when frequency limits are exceeded * - Providing safe start/stop scan operations with error handling - * + * * The scanner will automatically delay new scan requests if the frequency limit is reached, * and will retry once sufficient time has passed. This helps prevent scan failure errors * and ensures compliance with Android's scanning restrictions. 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 16984aa..ffc3f1f 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 @@ -81,6 +81,21 @@ class FlutterError ( val details: Any? = null ) : Throwable() +enum class UniversalBleLogLevel(val raw: Int) { + NONE(0), + ERROR(1), + WARNING(2), + INFO(3), + DEBUG(4), + VERBOSE(5); + + companion object { + fun ofRaw(raw: Int): UniversalBleLogLevel? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** Unified error codes for all platforms */ enum class UniversalBleErrorCode(val raw: Int) { UNKNOWN_ERROR(0), @@ -398,40 +413,45 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { return when (type) { 129.toByte() -> { return (readValue(buffer) as Long?)?.let { - UniversalBleErrorCode.ofRaw(it.toInt()) + UniversalBleLogLevel.ofRaw(it.toInt()) } } 130.toByte() -> { - return (readValue(buffer) as? List)?.let { - UniversalBleScanResult.fromList(it) + return (readValue(buffer) as Long?)?.let { + UniversalBleErrorCode.ofRaw(it.toInt()) } } 131.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalBleService.fromList(it) + UniversalBleScanResult.fromList(it) } } 132.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalBleCharacteristic.fromList(it) + UniversalBleService.fromList(it) } } 133.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalBleDescriptor.fromList(it) + UniversalBleCharacteristic.fromList(it) } } 134.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalScanFilter.fromList(it) + UniversalBleDescriptor.fromList(it) } } 135.toByte() -> { return (readValue(buffer) as? List)?.let { - UniversalManufacturerDataFilter.fromList(it) + UniversalScanFilter.fromList(it) } } 136.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalManufacturerDataFilter.fromList(it) + } + } + 137.toByte() -> { return (readValue(buffer) as? List)?.let { UniversalManufacturerData.fromList(it) } @@ -441,38 +461,42 @@ private open class UniversalBlePigeonCodec : StandardMessageCodec() { } override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { - is UniversalBleErrorCode -> { + is UniversalBleLogLevel -> { stream.write(129) writeValue(stream, value.raw.toLong()) } - is UniversalBleScanResult -> { + is UniversalBleErrorCode -> { stream.write(130) - writeValue(stream, value.toList()) + writeValue(stream, value.raw.toLong()) } - is UniversalBleService -> { + is UniversalBleScanResult -> { stream.write(131) writeValue(stream, value.toList()) } - is UniversalBleCharacteristic -> { + is UniversalBleService -> { stream.write(132) writeValue(stream, value.toList()) } - is UniversalBleDescriptor -> { + is UniversalBleCharacteristic -> { stream.write(133) writeValue(stream, value.toList()) } - is UniversalScanFilter -> { + is UniversalBleDescriptor -> { stream.write(134) writeValue(stream, value.toList()) } - is UniversalManufacturerDataFilter -> { + is UniversalScanFilter -> { stream.write(135) writeValue(stream, value.toList()) } - is UniversalManufacturerData -> { + is UniversalManufacturerDataFilter -> { stream.write(136) writeValue(stream, value.toList()) } + is UniversalManufacturerData -> { + stream.write(137) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -504,6 +528,7 @@ interface UniversalBlePlatformChannel { fun unPair(deviceId: String) fun getSystemDevices(withServices: List, callback: (Result>) -> Unit) fun getConnectionState(deviceId: String): Long + fun setLogLevel(logLevel: UniversalBleLogLevel) companion object { /** The codec used by UniversalBlePlatformChannel. */ @@ -876,6 +901,24 @@ interface UniversalBlePlatformChannel { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val logLevelArg = args[0] as UniversalBleLogLevel + val wrapped: List = try { + api.setLogLevel(logLevelArg) + listOf(null) + } catch (exception: Throwable) { + UniversalBlePigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } @@ -942,12 +985,12 @@ class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger, } } } - fun onValueChanged(deviceIdArg: String, characteristicIdArg: String, valueArg: ByteArray, callback: (Result) -> Unit) + fun onValueChanged(deviceIdArg: String, characteristicIdArg: String, valueArg: ByteArray, timestampArg: Long?, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(deviceIdArg, characteristicIdArg, valueArg)) { + channel.send(listOf(deviceIdArg, characteristicIdArg, valueArg, timestampArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleLogger.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleLogger.kt new file mode 100644 index 0000000..2774c20 --- /dev/null +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleLogger.kt @@ -0,0 +1,49 @@ +package com.navideck.universal_ble + +import android.util.Log +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +object UniversalBleLogger { + private const val TAG = "UniversalBle" + private var currentLogLevel: UniversalBleLogLevel = UniversalBleLogLevel.NONE + private val timeFormatter = SimpleDateFormat("HH:mm:ss.SSS", Locale.US) + + fun setLogLevel(logLevel: UniversalBleLogLevel) { + currentLogLevel = logLevel + } + + fun logError(message: String) { + if (!allows(UniversalBleLogLevel.ERROR)) return + Log.e(TAG, withTimestamp(message)) + } + + fun logWarning(message: String) { + if (!allows(UniversalBleLogLevel.WARNING)) return + Log.w(TAG, withTimestamp(message)) + } + + fun logInfo(message: String) { + if (!allows(UniversalBleLogLevel.INFO)) return + Log.i(TAG, withTimestamp(message)) + } + + fun logDebug(message: String) { + if (!allows(UniversalBleLogLevel.DEBUG)) return + Log.d(TAG, withTimestamp(message)) + } + + fun logVerbose(message: String) { + if (!allows(UniversalBleLogLevel.VERBOSE)) return + Log.v(TAG, withTimestamp(message)) + } + + private fun allows(level: UniversalBleLogLevel): Boolean { + return currentLogLevel != UniversalBleLogLevel.NONE && level.ordinal <= currentLogLevel.ordinal + } + + private fun withTimestamp(message: String): String { + return "[${timeFormatter.format(Date())}] $message" + } +} 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 199ae53..edd15c5 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -24,7 +24,6 @@ import android.content.IntentFilter import android.os.Build import android.os.Handler import android.os.Looper -import android.util.Log import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -35,8 +34,6 @@ import java.util.concurrent.TimeUnit import androidx.core.content.edit -private const val TAG = "UniversalBlePlugin" - @SuppressLint("MissingPermission") class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), FlutterPlugin, ActivityAware, PluginRegistry.ActivityResultListener, @@ -178,7 +175,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), // Set custom scan filter only if required if (usesCustomFilters) { - Log.e(TAG, "Using Custom Filters") + UniversalBleLogger.logError("Using Custom Filters") universalBleFilterUtil.scanFilter = filter universalBleFilterUtil.serviceFilterUUIDS = filterServices } else { @@ -217,7 +214,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), deviceId.findGatt()?.let { val currentState = bluetoothManager.getConnectionState(it.device, BluetoothProfile.GATT) if (currentState == BluetoothGatt.STATE_CONNECTED) { - Log.e(TAG, "$deviceId Already connected") + UniversalBleLogger.logError("$deviceId Already connected") mainThreadHandler?.post { callbackChannel?.onConnectionChanged(deviceId, true, null) {} } @@ -266,7 +263,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), connectionState.toBleConnectionState().value } else { // Might be connected with device, but not with app - Log.e(TAG, "Device might be connected but not known to this app") + UniversalBleLogger.logError("Device might be connected but not known to this app") BleConnectionState.Disconnected.value } } catch (e: Exception) { @@ -274,6 +271,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } + override fun setLogLevel(logLevel: UniversalBleLogLevel) { + UniversalBleLogger.setLogLevel(logLevel) + } + override fun discoverServices( deviceId: String, withDescriptors: Boolean, @@ -348,6 +349,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), callback: (Result) -> Unit, ) { try { + UniversalBleLogger.logDebug("SET_NOTIFY -> $deviceId $service $characteristic input=$bleInputProperty") val gatt = deviceId.toBluetoothGatt() val gattCharacteristic: BluetoothGattCharacteristic? = gatt.getCharacteristic(service, characteristic) @@ -408,7 +410,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } } } else { - Log.d("UniversalBle", "CCCD Descriptor not found") + UniversalBleLogger.logDebug("CCCD Descriptor not found") } if (gatt.setCharacteristicNotification(gattCharacteristic, enable)) { @@ -456,6 +458,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), callback: (Result) -> Unit, ) { try { + UniversalBleLogger.logDebug("READ -> $deviceId $service $characteristic") val gatt = deviceId.toBluetoothGatt() val gattCharacteristic = gatt.getCharacteristic(service, characteristic) if (gattCharacteristic == null) { @@ -519,6 +522,9 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (status == BluetoothGatt.GATT_SUCCESS) { it.result(Result.success(value)) } else { + UniversalBleLogger.logError( + "READ_FAILED <- ${gatt.device.address} ${characteristic.uuid} status=$status" + ) it.result( Result.failure( createFlutterError( @@ -542,6 +548,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), callback: (Result) -> Unit, ) { try { + UniversalBleLogger.logDebug("WRITE -> $deviceId $service $characteristic len=${value.size} property=$bleOutputProperty") val gatt = deviceId.toBluetoothGatt() val gattCharacteristic = gatt.getCharacteristic(service, characteristic) if (gattCharacteristic == null) { @@ -638,6 +645,9 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), if (status == BluetoothGatt.GATT_SUCCESS) { it.result(Result.success(Unit)) } else { + UniversalBleLogger.logError( + "WRITE_FAILED <- ${gatt?.device?.address} ${characteristic.uuid} status=$status" + ) it.result( Result.failure( createFlutterError( @@ -653,6 +663,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), override fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) { + UniversalBleLogger.logDebug("REQUEST_MTU -> $deviceId expected=$expectedMtu") try { val gatt = deviceId.toBluetoothGatt() gatt.requestMtu(expectedMtu.toInt()) @@ -957,13 +968,13 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) } if (device == null) { - Log.e(TAG, "No device found in ACTION_BOND_STATE_CHANGED intent") + UniversalBleLogger.logError("No device found in ACTION_BOND_STATE_CHANGED intent") return } // get pairing failed error when (intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)) { BluetoothDevice.BOND_BONDING -> { - Log.v(TAG, "${device.address} BOND_BONDING") + UniversalBleLogger.logVerbose("${device.address} BOND_BONDING") } BOND_BONDED -> { @@ -975,7 +986,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } BluetoothDevice.BOND_NONE -> { - Log.e(TAG, "${device.address} BOND_NONE") + UniversalBleLogger.logError("${device.address} BOND_NONE") onBondStateUpdate(device.address, false) } } @@ -986,12 +997,12 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), private val scanCallback = object : ScanCallback() { override fun onScanFailed(errorCode: Int) { - Log.e(TAG, "OnScanFailed: ${errorCode.parseScanErrorMessage()}") + UniversalBleLogger.logError("OnScanFailed: ${errorCode.parseScanErrorMessage()}") } override fun onScanResult(callbackType: Int, result: ScanResult) { - // Log.v(TAG, "onScanResult: $result") + // UniversalBleLogger.logVerbose("onScanResult: $result") var serviceUuids: Array = arrayOf() result.device.uuids?.forEach { serviceUuids += it.uuid @@ -1029,14 +1040,17 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), } override fun onBatchScanResults(results: MutableList?) { - Log.v(TAG, "onBatchScanResults: $results") + UniversalBleLogger.logVerbose("onBatchScanResults: $results") } } - override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - Log.d( - TAG, + override fun onConnectionStateChange( + gatt: BluetoothGatt, + status: Int, + newState: Int, + ) { + UniversalBleLogger.logDebug( "onConnectionStateChange-> Status: $status ${status.parseHciErrorCode()}, NewState: $newState" ) @@ -1053,7 +1067,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), gatt.device.address, false, status.parseHciErrorCode() ) {} } - Log.d(TAG, "Closing gatt for ${gatt.device.name}") + UniversalBleLogger.logDebug("Closing gatt for ${gatt.device.name}") gatt.close() } } @@ -1063,11 +1077,15 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), characteristic: BluetoothGattCharacteristic, value: ByteArray, ) { + UniversalBleLogger.logVerbose( + "NOTIFY <- ${gatt.device.address} ${characteristic.uuid} len=${value.size}" + ) mainThreadHandler?.post { callbackChannel?.onValueChanged( deviceIdArg = gatt.device.address, characteristicIdArg = characteristic.uuid.toString(), - valueArg = value + valueArg = value, + timestampArg = System.currentTimeMillis() ) {} } } diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift index c29c725..bfbf983 100644 --- a/darwin/Classes/UniversalBle.g.swift +++ b/darwin/Classes/UniversalBle.g.swift @@ -132,6 +132,15 @@ func deepHashUniversalBle(value: Any?, hasher: inout Hasher) { +enum UniversalBleLogLevel: Int { + case none = 0 + case error = 1 + case warning = 2 + case info = 3 + case debug = 4 + case verbose = 5 +} + /// Unified error codes for all platforms enum UniversalBleErrorCode: Int { case unknownError = 0 @@ -436,22 +445,28 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader { case 129: let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) if let enumResultAsInt = enumResultAsInt { - return UniversalBleErrorCode(rawValue: enumResultAsInt) + return UniversalBleLogLevel(rawValue: enumResultAsInt) } return nil case 130: - 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 131: - return UniversalBleService.fromList(self.readValue() as! [Any?]) + return UniversalBleScanResult.fromList(self.readValue() as! [Any?]) case 132: - return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?]) + return UniversalBleService.fromList(self.readValue() as! [Any?]) case 133: - return UniversalBleDescriptor.fromList(self.readValue() as! [Any?]) + return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?]) case 134: - return UniversalScanFilter.fromList(self.readValue() as! [Any?]) + return UniversalBleDescriptor.fromList(self.readValue() as! [Any?]) case 135: - return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + return UniversalScanFilter.fromList(self.readValue() as! [Any?]) case 136: + return UniversalManufacturerDataFilter.fromList(self.readValue() as! [Any?]) + case 137: return UniversalManufacturerData.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) @@ -461,30 +476,33 @@ private class UniversalBlePigeonCodecReader: FlutterStandardReader { private class UniversalBlePigeonCodecWriter: FlutterStandardWriter { override func writeValue(_ value: Any) { - if let value = value as? UniversalBleErrorCode { + if let value = value as? UniversalBleLogLevel { super.writeByte(129) super.writeValue(value.rawValue) - } else if let value = value as? UniversalBleScanResult { + } else if let value = value as? UniversalBleErrorCode { super.writeByte(130) - super.writeValue(value.toList()) - } else if let value = value as? UniversalBleService { + super.writeValue(value.rawValue) + } else if let value = value as? UniversalBleScanResult { super.writeByte(131) super.writeValue(value.toList()) - } else if let value = value as? UniversalBleCharacteristic { + } else if let value = value as? UniversalBleService { super.writeByte(132) super.writeValue(value.toList()) - } else if let value = value as? UniversalBleDescriptor { + } else if let value = value as? UniversalBleCharacteristic { super.writeByte(133) super.writeValue(value.toList()) - } else if let value = value as? UniversalScanFilter { + } else if let value = value as? UniversalBleDescriptor { super.writeByte(134) super.writeValue(value.toList()) - } else if let value = value as? UniversalManufacturerDataFilter { + } else if let value = value as? UniversalScanFilter { super.writeByte(135) super.writeValue(value.toList()) - } else if let value = value as? UniversalManufacturerData { + } else if let value = value as? UniversalManufacturerDataFilter { super.writeByte(136) super.writeValue(value.toList()) + } else if let value = value as? UniversalManufacturerData { + super.writeByte(137) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -529,6 +547,7 @@ protocol UniversalBlePlatformChannel { func unPair(deviceId: String) throws func getSystemDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) func getConnectionState(deviceId: String) throws -> Int64 + func setLogLevel(logLevel: UniversalBleLogLevel) throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -847,6 +866,21 @@ class UniversalBlePlatformChannelSetup { } else { getConnectionStateChannel.setMessageHandler(nil) } + let setLogLevelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setLogLevelChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let logLevelArg = args[0] as! UniversalBleLogLevel + do { + try api.setLogLevel(logLevel: logLevelArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + setLogLevelChannel.setMessageHandler(nil) + } } } /// Native -> Flutter @@ -856,7 +890,7 @@ protocol UniversalBleCallbackChannelProtocol { func onAvailabilityChanged(state stateArg: Int64, completion: @escaping (Result) -> Void) func onPairStateChange(deviceId deviceIdArg: String, isPaired isPairedArg: Bool, error errorArg: String?, completion: @escaping (Result) -> Void) func onScanResult(result resultArg: UniversalBleScanResult, completion: @escaping (Result) -> Void) - func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, completion: @escaping (Result) -> Void) + func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, timestamp timestampArg: Int64?, completion: @escaping (Result) -> Void) func onConnectionChanged(deviceId deviceIdArg: String, connected connectedArg: Bool, error errorArg: String?, completion: @escaping (Result) -> Void) } class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { @@ -923,10 +957,10 @@ class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { } } } - func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, completion: @escaping (Result) -> Void) { + func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, timestamp timestampArg: Int64?, completion: @escaping (Result) -> Void) { let channelName: String = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged\(messageChannelSuffix)" let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([deviceIdArg, characteristicIdArg, valueArg] as [Any?]) { response in + channel.sendMessage([deviceIdArg, characteristicIdArg, valueArg, timestampArg] as [Any?]) { response in guard let listResponse = response as? [Any?] else { completion(.failure(createConnectionError(withChannelName: channelName))) return diff --git a/darwin/Classes/UniversalBleAsyncServiceDiscovery.swift b/darwin/Classes/UniversalBleAsyncServiceDiscovery.swift index 6f61311..0f2f2e5 100644 --- a/darwin/Classes/UniversalBleAsyncServiceDiscovery.swift +++ b/darwin/Classes/UniversalBleAsyncServiceDiscovery.swift @@ -30,7 +30,7 @@ class UniversalBleAsyncServiceDiscovery: NSObject { /// Starts the service discovery process func startDiscovery() { guard !isDiscoveryInProgress else { - print("Service discovery already in progress for device: \(deviceId)") + UniversalBleLogger.shared.logWarning("Service discovery already in progress for device: \(deviceId)") return } isDiscoveryInProgress = true diff --git a/darwin/Classes/UniversalBleLogger.swift b/darwin/Classes/UniversalBleLogger.swift new file mode 100644 index 0000000..4b7bb36 --- /dev/null +++ b/darwin/Classes/UniversalBleLogger.swift @@ -0,0 +1,53 @@ +import Foundation + +final class UniversalBleLogger { + static let shared = UniversalBleLogger() + + private init() {} + + private lazy var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss.SSS" + return formatter + }() + + private var currentLogLevel: UniversalBleLogLevel = .none + + func setLogLevel(_ logLevel: UniversalBleLogLevel) { + currentLogLevel = logLevel + } + + func logError(_ message: String) { + guard allows(.error) else { return } + print("UniversalBle:ERROR \(withTimestamp(message))") + } + + func logWarning(_ message: String) { + guard allows(.warning) else { return } + print("UniversalBle:WARN \(withTimestamp(message))") + } + + func logInfo(_ message: String) { + guard allows(.info) else { return } + print("UniversalBle:INFO \(withTimestamp(message))") + } + + func logDebug(_ message: String) { + guard allows(.debug) else { return } + print("UniversalBle:DEBUG \(withTimestamp(message))") + } + + func logVerbose(_ message: String) { + guard allows(.verbose) else { return } + print("UniversalBle:VERBOSE \(withTimestamp(message))") + } + + private func allows(_ level: UniversalBleLogLevel) -> Bool { + return currentLogLevel != .none && level.rawValue <= currentLogLevel.rawValue + } + + private func withTimestamp(_ message: String) -> String { + let time = dateFormatter.string(from: Date()) + return "[\(time)] \(message)" + } +} diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift index b99f89a..2609cf3 100644 --- a/darwin/Classes/UniversalBlePlugin.swift +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -93,7 +93,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral var withServices: [CBUUID] = try filter?.withServices.compactMap { $0 }.toCBUUID() ?? [] if usesCustomFilters { - print("Using Custom Filters") + UniversalBleLogger.shared.logInfo("Using Custom Filters") universalBleFilterUtil.scanFilter = filter universalBleFilterUtil.scanFilterServicesUUID = withServices withServices = [] @@ -132,6 +132,10 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral return isManageScanning } + func setLogLevel(logLevel: UniversalBleLogLevel) throws { + UniversalBleLogger.shared.setLogLevel(logLevel) + } + func connect(deviceId: String) throws { let peripheral = try deviceId.getPeripheral(manager: manager) peripheral.delegate = self @@ -218,7 +222,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral // Check if discovery is already in progress if activeServiceDiscoveries[deviceId] != nil { - print("Services discovery already in progress for :\(deviceId), waiting for completion.") + UniversalBleLogger.shared.logWarning("Services discovery already in progress for :\(deviceId), waiting for completion.") discoverServicesFutures.append(DiscoverServicesFuture(deviceId: deviceId, result: completion)) return } @@ -247,6 +251,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64, completion: @escaping (Result) -> Void) { + UniversalBleLogger.shared.logDebug("SET_NOTIFY -> \(deviceId) \(service) \(characteristic) input=\(bleInputProperty)") guard let peripheral = deviceId.findPeripheral(manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(deviceId)"))) return @@ -273,6 +278,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } func readValue(deviceId: String, service: String, characteristic: String, completion: @escaping (Result) -> Void) { + UniversalBleLogger.shared.logDebug("READ -> \(deviceId) \(service) \(characteristic)") guard let peripheral = deviceId.findPeripheral(manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) return @@ -290,6 +296,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: Int64, completion: @escaping (Result) -> Void) { + UniversalBleLogger.shared.logDebug("WRITE -> \(deviceId) \(service) \(characteristic) len=\(value.data.count) property=\(bleOutputProperty)") guard let peripheral = deviceId.findPeripheral(manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) return @@ -324,6 +331,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } func requestMtu(deviceId: String, expectedMtu _: Int64, completion: @escaping (Result) -> Void) { + UniversalBleLogger.shared.logDebug("REQUEST_MTU -> \(deviceId)") guard let peripheral = deviceId.findPeripheral(manager: manager) else { completion(Result.failure(createFlutterError(code: .deviceNotFound, message: "Unknown deviceId:\(self)"))) return @@ -349,7 +357,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral func getSystemDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) { var servicesFilter = withServices if servicesFilter.isEmpty { - print("No services filter was set for getting system connected devices. Using default services...") + UniversalBleLogger.shared.logInfo("No services filter was set for getting system connected devices. Using default services...") // Add several generic services servicesFilter = ["1800", "1801", "180A", "180D", "1810", "181B", "1808", "181D", "1816", "1814", "181A", "1802", "1803", "1804", "1815", "1805", "1807", "1806", "1848", "185E", "180F", "1812", "180E", "1813"] @@ -461,6 +469,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral characteristicWriteFutures.removeAll { future in if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { if let flutterError = error?.toFlutterError() { + UniversalBleLogger.shared.logError("WRITE_FAILED <- \(peripheral.uuid.uuidString) \(characteristic.uuid.uuidStr): \(flutterError.message ?? "")") future.result(Result.failure(flutterError)) } else { future.result(Result.success({}())) @@ -475,6 +484,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral characteristicNotifyFutures.removeAll { future in if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { if let flutterError = error?.toFlutterError() { + UniversalBleLogger.shared.logError("SET_NOTIFY_FAILED <- \(peripheral.uuid.uuidString) \(characteristic.uuid.uuidStr): \(flutterError.message ?? "")") future.result(Result.failure(flutterError)) } else { future.result(Result.success({}())) @@ -486,10 +496,24 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral } public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + if let error { + UniversalBleLogger.shared.logError("NOTIFY_ERROR <- \(peripheral.uuid.uuidString) \(characteristic.uuid.uuidStr): \(error.localizedDescription)") + } + + if characteristic.isNotifying, let characteristicValue = characteristic.value { + let preview = characteristicValue.prefix(8).map { String(format: "%02X", $0) }.joined() + UniversalBleLogger.shared.logVerbose("NOTIFY <- \(peripheral.uuid.uuidString) \(characteristic.uuid.uuidStr) len=\(characteristicValue.count) data=\(preview)") + } + // Update callbackChannel if notifying if characteristic.isNotifying { if let characteristicValue = characteristic.value { - callbackChannel.onValueChanged(deviceId: peripheral.uuid.uuidString, characteristicId: characteristic.uuid.uuidStr, value: FlutterStandardTypedData(bytes: characteristicValue)) { _ in } + callbackChannel.onValueChanged( + deviceId: peripheral.uuid.uuidString, + characteristicId: characteristic.uuid.uuidStr, + value: FlutterStandardTypedData(bytes: characteristicValue), + timestamp: Int64(Date().timeIntervalSince1970 * 1000) + ) { _ in } } } @@ -501,6 +525,7 @@ private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentral characteristicReadFutures.removeAll { future in if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { if let flutterError = error?.toFlutterError() { + UniversalBleLogger.shared.logError("READ_FAILED <- \(peripheral.uuid.uuidString) \(characteristic.uuid.uuidStr): \(flutterError.message ?? "")") future.result(Result.failure(flutterError)) } else { if let characteristicValue = characteristic.value { diff --git a/example/lib/main.dart b/example/lib/main.dart index f31ddb1..01f1d47 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:universal_ble/universal_ble.dart'; import 'package:universal_ble_example/home/home.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await UniversalBle.setLogLevel(BleLogLevel.verbose); runApp( MaterialApp( title: 'Universal BLE', diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index 893331f..d940b3f 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -81,11 +81,14 @@ class _PeripheralDetailPageState extends State { } } - void _handleValueChange( - String deviceId, String characteristicId, Uint8List value) { + void _handleValueChange(String deviceId, String characteristicId, + Uint8List value, int? timestamp) { String s = String.fromCharCodes(value); String data = '$s\nraw : ${value.toString()}'; - debugPrint('_handleValueChange $characteristicId, $s'); + DateTime? timestampDateTime = timestamp != null + ? DateTime.fromMillisecondsSinceEpoch(timestamp) + : null; + debugPrint('_handleValueChange ($timestampDateTime) $characteristicId, $s'); _addLog("Value", data); } diff --git a/example/pubspec.lock b/example/pubspec.lock index 97d521a..82cd526 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -214,10 +214,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" path: dependency: transitive description: @@ -315,10 +315,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" typed_data: dependency: transitive description: diff --git a/lib/src/extensions/ble_device_extension.dart b/lib/src/extensions/ble_device_extension.dart index 863cfbd..55ca002 100644 --- a/lib/src/extensions/ble_device_extension.dart +++ b/lib/src/extensions/ble_device_extension.dart @@ -93,7 +93,7 @@ extension BleDeviceExtension on BleDevice { /// /// [service] is the UUID of the service. /// [preferCached] indicates whether to use cached services. If cache is empty, discoverServices() will be called. - /// might throw [NotFoundException] + /// might throw [UniversalBleException] Future getService( String service, { bool preferCached = true, @@ -128,7 +128,7 @@ extension BleDeviceExtension on BleDevice { /// [service] is the UUID of the service. /// [characteristic] is the UUID of the characteristic. /// [preferCached] indicates whether to use cached services. If cache is empty, discoverServices() will be called. - /// might throw [NotFoundException] + /// might throw [UniversalBleException] Future getCharacteristic( String characteristic, { required String service, diff --git a/lib/src/models/ble_log_level.dart b/lib/src/models/ble_log_level.dart new file mode 100644 index 0000000..77ca88b --- /dev/null +++ b/lib/src/models/ble_log_level.dart @@ -0,0 +1,8 @@ +enum BleLogLevel { + none, + error, + warning, + info, + debug, + verbose; +} diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart index 2a2d267..ac71575 100644 --- a/lib/src/models/model_exports.dart +++ b/lib/src/models/model_exports.dart @@ -1,3 +1,4 @@ +export 'package:universal_ble/src/models/ble_log_level.dart'; 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'; diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index 86e3775..8c40039 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -24,6 +24,14 @@ class UniversalBle { _bleCommandQueue.timeout = duration; } + /// Set log level for both Dart and native implementations. + /// Only effective in debug builds. + static Future setLogLevel(BleLogLevel logLevel) async { + if (!kDebugMode) return; + UniversalLogger.setLogLevel(logLevel); + await _platform.setLogLevel(logLevel); + } + /// Set how commands will be executed. By default, all commands are executed in a global queue (`QueueType.global`), /// with each command waiting for the previous one to finish. /// diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index a116126..c026b7c 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -244,6 +244,10 @@ class UniversalBleLinux extends UniversalBlePlatform { @override Future setNotifiable(String deviceId, String service, String characteristic, BleInputProperty bleInputProperty) async { + UniversalLogger.logDebug( + "SET_NOTIFY -> $deviceId $service $characteristic input=${bleInputProperty.name}", + withTimestamp: true, + ); final char = _getCharacteristic(deviceId, service, characteristic); String characteristicKey = "${deviceId}_${service}_$characteristic"; @@ -265,10 +269,15 @@ class UniversalBleLinux extends UniversalBlePlatform { for (String property in properties) { switch (property) { case BluezProperty.value: + UniversalLogger.logVerbose( + "NOTIFY <- $deviceId $service $characteristic len=${char.value.length} data=${char.value}", + withTimestamp: true, + ); updateCharacteristicValue( deviceId, characteristic, Uint8List.fromList(char.value), + DateTime.now().millisecondsSinceEpoch, ); break; default: @@ -293,11 +302,19 @@ class UniversalBleLinux extends UniversalBlePlatform { String characteristic, { final Duration? timeout, }) async { + UniversalLogger.logDebug( + "READ -> $deviceId $service $characteristic", + withTimestamp: true, + ); try { final c = _getCharacteristic(deviceId, service, characteristic); final data = await c.readValue(); return Uint8List.fromList(data); } on BlueZFailedException catch (e) { + UniversalLogger.logError( + "READ_FAILED <- $deviceId $service $characteristic ${e.message}", + withTimestamp: true, + ); throw e.toUniversalBleException( defaultCode: UniversalBleErrorCode.readFailed, ); @@ -311,6 +328,10 @@ class UniversalBleLinux extends UniversalBlePlatform { String characteristic, Uint8List value, BleOutputProperty bleOutputProperty) async { + UniversalLogger.logDebug( + "WRITE -> $deviceId $service $characteristic len=${value.length} property=${bleOutputProperty.name}", + withTimestamp: true, + ); try { final c = _getCharacteristic(deviceId, service, characteristic); if (bleOutputProperty == BleOutputProperty.withResponse) { @@ -325,6 +346,10 @@ class UniversalBleLinux extends UniversalBlePlatform { ); } } on BlueZFailedException catch (e) { + UniversalLogger.logError( + "WRITE_FAILED <- $deviceId $service $characteristic ${e.message}", + withTimestamp: true, + ); throw e.toUniversalBleException( defaultCode: UniversalBleErrorCode.writeFailed, ); diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart index caeb0d8..4b650ad 100644 --- a/lib/src/universal_ble_pigeon/universal_ble.g.dart +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -41,6 +41,15 @@ bool _deepEquals(Object? a, Object? b) { return a == b; } +enum UniversalBleLogLevel { + none, + error, + warning, + info, + debug, + verbose, +} + /// Unified error codes for all platforms enum UniversalBleErrorCode { unknownError, @@ -478,30 +487,33 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is UniversalBleErrorCode) { + } else if (value is UniversalBleLogLevel) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is UniversalBleScanResult) { + } else if (value is UniversalBleErrorCode) { buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else if (value is UniversalBleService) { + writeValue(buffer, value.index); + } else if (value is UniversalBleScanResult) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is UniversalBleCharacteristic) { + } else if (value is UniversalBleService) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is UniversalBleDescriptor) { + } else if (value is UniversalBleCharacteristic) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is UniversalScanFilter) { + } else if (value is UniversalBleDescriptor) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is UniversalManufacturerDataFilter) { + } else if (value is UniversalScanFilter) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is UniversalManufacturerData) { + } else if (value is UniversalManufacturerDataFilter) { buffer.putUint8(136); writeValue(buffer, value.encode()); + } else if (value is UniversalManufacturerData) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -512,20 +524,23 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: final value = readValue(buffer) as int?; - return value == null ? null : UniversalBleErrorCode.values[value]; + return value == null ? null : UniversalBleLogLevel.values[value]; case 130: - return UniversalBleScanResult.decode(readValue(buffer)!); + final value = readValue(buffer) as int?; + return value == null ? null : UniversalBleErrorCode.values[value]; case 131: - return UniversalBleService.decode(readValue(buffer)!); + return UniversalBleScanResult.decode(readValue(buffer)!); case 132: - return UniversalBleCharacteristic.decode(readValue(buffer)!); + return UniversalBleService.decode(readValue(buffer)!); case 133: - return UniversalBleDescriptor.decode(readValue(buffer)!); + return UniversalBleCharacteristic.decode(readValue(buffer)!); case 134: - return UniversalScanFilter.decode(readValue(buffer)!); + return UniversalBleDescriptor.decode(readValue(buffer)!); case 135: - return UniversalManufacturerDataFilter.decode(readValue(buffer)!); + return UniversalScanFilter.decode(readValue(buffer)!); case 136: + return UniversalManufacturerDataFilter.decode(readValue(buffer)!); + case 137: return UniversalManufacturerData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -1061,6 +1076,30 @@ class UniversalBlePlatformChannel { return (pigeonVar_replyList[0] as int?)!; } } + + Future setLogLevel(UniversalBleLogLevel logLevel) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([logLevel]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } /// Native -> Flutter @@ -1073,8 +1112,8 @@ abstract class UniversalBleCallbackChannel { void onScanResult(UniversalBleScanResult result); - void onValueChanged( - String deviceId, String characteristicId, Uint8List value); + void onValueChanged(String deviceId, String characteristicId, Uint8List value, + int? timestamp); void onConnectionChanged(String deviceId, bool connected, String? error); @@ -1192,9 +1231,10 @@ abstract class UniversalBleCallbackChannel { final Uint8List? arg_value = (args[2] as Uint8List?); assert(arg_value != null, 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null, expected non-null Uint8List.'); + final int? arg_timestamp = (args[3] as int?); try { - api.onValueChanged( - arg_deviceId!, arg_characteristicId!, arg_value!); + api.onValueChanged(arg_deviceId!, arg_characteristicId!, arg_value!, + arg_timestamp); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); 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 997d75e..c249789 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -164,6 +164,10 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { ); } + @override + Future setLogLevel(BleLogLevel logLevel) => _executeWithErrorHandling( + () => _channel.setLogLevel(logLevel.toUniversalBleLogLevel())); + /// To set listeners void _setupListeners() { UniversalBleCallbackChannel.setUp(_UniversalBleCallbackHandler( @@ -253,8 +257,12 @@ class _UniversalBleCallbackHandler extends UniversalBleCallbackChannel { @override void onValueChanged( - String deviceId, String characteristicId, Uint8List value) => - valueChanged(deviceId, characteristicId, value); + String deviceId, + String characteristicId, + Uint8List value, + int? timestamp, + ) => + valueChanged(deviceId, characteristicId, value, timestamp); @override void onPairStateChange(String deviceId, bool isPaired, String? error) => @@ -298,3 +306,14 @@ extension _ScanFilterExtension on ScanFilter? { ); } } + +extension _BleLogLevelExtension on BleLogLevel { + UniversalBleLogLevel toUniversalBleLogLevel() => switch (this) { + BleLogLevel.none => UniversalBleLogLevel.none, + BleLogLevel.error => UniversalBleLogLevel.error, + BleLogLevel.warning => UniversalBleLogLevel.warning, + BleLogLevel.info => UniversalBleLogLevel.info, + BleLogLevel.debug => UniversalBleLogLevel.debug, + BleLogLevel.verbose => UniversalBleLogLevel.verbose, + }; +} diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index 5a3ae15..9f6c576 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:universal_ble/src/utils/cache_handler.dart'; import 'package:universal_ble/src/utils/universal_ble_stream_controller.dart'; +import 'package:universal_ble/src/utils/universal_logger.dart'; import 'package:universal_ble/universal_ble.dart'; abstract class UniversalBlePlatform { @@ -85,6 +86,9 @@ abstract class UniversalBlePlatform { List? withServices, ); + Future setLogLevel(BleLogLevel logLevel) async => + UniversalLogger.setLogLevel(logLevel); + bool receivesAdvertisements(String deviceId) => true; /// Streams @@ -142,6 +146,7 @@ abstract class UniversalBlePlatform { String deviceId, String characteristicId, Uint8List value, + int? timestamp, ) { characteristicId = BleUuidParser.string(characteristicId); _valueStreamController.add(( @@ -150,7 +155,7 @@ abstract class UniversalBlePlatform { value: value, )); try { - onValueChange?.call(deviceId, characteristicId, value); + onValueChange?.call(deviceId, characteristicId, value, timestamp); } catch (_) {} } @@ -176,10 +181,17 @@ abstract class UniversalBlePlatform { // Callback types typedef OnConnectionChange = void Function( - String deviceId, bool isConnected, String? error); + String deviceId, + bool isConnected, + String? error, +); typedef OnValueChange = void Function( - String deviceId, String characteristicId, Uint8List value); + String deviceId, + String characteristicId, + Uint8List value, + int? timestamp, +); typedef OnScanResult = void Function(BleDevice scanResult); diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index 2a6bff5..734e5cf 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -162,6 +162,10 @@ class UniversalBleWeb extends UniversalBlePlatform { String characteristic, BleInputProperty bleInputProperty, ) async { + UniversalLogger.logDebug( + "SET_NOTIFY -> $deviceId $service $characteristic input=${bleInputProperty.name}", + withTimestamp: true, + ); final bleCharacteristic = await _getBleCharacteristic( deviceId: deviceId, serviceId: service, @@ -185,10 +189,20 @@ class UniversalBleWeb extends UniversalBlePlatform { await bleCharacteristic.startNotifications(); _characteristicStreamList[characteristicKey] = bleCharacteristic.value.listen((ByteData event) { + final preview = event.buffer + .asUint8List() + .take(8) + .map((e) => e.toRadixString(16).padLeft(2, '0')) + .join(); + UniversalLogger.logVerbose( + "NOTIFY <- $deviceId $characteristic len=${event.lengthInBytes} data=$preview", + withTimestamp: true, + ); updateCharacteristicValue( deviceId, characteristic, event.buffer.asUint8List(), + DateTime.now().millisecondsSinceEpoch, ); }); } else { @@ -205,6 +219,10 @@ class UniversalBleWeb extends UniversalBlePlatform { Uint8List value, BleOutputProperty bleOutputProperty, ) async { + UniversalLogger.logDebug( + "WRITE -> $deviceId $service $characteristic len=${value.length} property=${bleOutputProperty.name}", + withTimestamp: true, + ); final bleCharacteristic = await _getBleCharacteristic( deviceId: deviceId, serviceId: service, @@ -234,6 +252,10 @@ class UniversalBleWeb extends UniversalBlePlatform { String characteristic, { final Duration? timeout, }) async { + UniversalLogger.logDebug( + "READ -> $deviceId $service $characteristic", + withTimestamp: true, + ); var bleCharacteristic = await _getBleCharacteristic( deviceId: deviceId, serviceId: service, diff --git a/lib/src/utils/universal_logger.dart b/lib/src/utils/universal_logger.dart index b693cad..171c5bb 100644 --- a/lib/src/utils/universal_logger.dart +++ b/lib/src/utils/universal_logger.dart @@ -1,24 +1,80 @@ import 'dart:developer'; +import 'package:universal_ble/src/models/ble_log_level.dart'; class UniversalLogger { - static void logInfo(String message) { + static BleLogLevel _currentLogLevel = BleLogLevel.none; + + static BleLogLevel get currentLogLevel => _currentLogLevel; + + static void setLogLevel(BleLogLevel logLevel) { + _currentLogLevel = logLevel; + } + + static void logError( + String message, { + bool withTimestamp = false, + }) { + if (!_allows(BleLogLevel.error)) return; + if (withTimestamp) { + final ts = DateTime.now().toIso8601String(); + message = "[$ts] $message"; + } + log( + '\x1B[31m$message\x1B[0m', + name: 'UniversalBle:ERROR', + ); + } + + static void logWarning(String message, {bool withTimestamp = false}) { + if (!_allows(BleLogLevel.warning)) return; + if (withTimestamp) { + final ts = DateTime.now().toIso8601String(); + message = "[$ts] $message"; + } + log( + '\x1B[33m$message\x1B[0m', + name: 'UniversalBle:WARN', + ); + } + + static void logInfo(String message, {bool withTimestamp = false}) { + if (!_allows(BleLogLevel.info)) return; + if (withTimestamp) { + final ts = DateTime.now().toIso8601String(); + message = "[$ts] $message"; + } log( message.toString(), name: 'UniversalBle:INFO', ); } - static void logError(String message) { + static void logDebug(String message, {bool withTimestamp = false}) { + if (!_allows(BleLogLevel.debug)) return; + if (withTimestamp) { + final ts = DateTime.now().toIso8601String(); + message = "[$ts] $message"; + } log( - '\x1B[31m$message\x1B[31m', - name: 'UniversalBle:ERROR', + message.toString(), + name: 'UniversalBle:DEBUG', ); } - static void logWarning(String message) { + static void logVerbose(String message, {bool withTimestamp = false}) { + if (!_allows(BleLogLevel.verbose)) return; + if (withTimestamp) { + final ts = DateTime.now().toIso8601String(); + message = "[$ts] $message"; + } log( - '\x1B[33m$message\x1B[33m', - name: 'UniversalBle:WARN', + message.toString(), + name: 'UniversalBle:VERBOSE', ); } + + static bool _allows(BleLogLevel level) { + return level.index <= _currentLogLevel.index && + _currentLogLevel != BleLogLevel.none; + } } diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart index 2dd3e57..fb3ef67 100644 --- a/pigeon/universal_ble.dart +++ b/pigeon/universal_ble.dart @@ -90,6 +90,8 @@ abstract class UniversalBlePlatformChannel { ); int getConnectionState(String deviceId); + + void setLogLevel(UniversalBleLogLevel logLevel); } /// Native -> Flutter @@ -105,6 +107,7 @@ abstract class UniversalBleCallbackChannel { String deviceId, String characteristicId, Uint8List value, + int? timestamp, ); void onConnectionChanged( @@ -134,6 +137,15 @@ class UniversalBleScanResult { }); } +enum UniversalBleLogLevel { + none, + error, + warning, + info, + debug, + verbose, +} + class UniversalBleService { String uuid; List? characteristics; diff --git a/test/ble_characteristic_test.dart b/test/ble_characteristic_test.dart index d13f31b..70ddc87 100644 --- a/test/ble_characteristic_test.dart +++ b/test/ble_characteristic_test.dart @@ -106,6 +106,7 @@ class _UniversalBleMock extends UniversalBlePlatformMock { deviceId, characteristic, Uint8List.fromList([1, 2, 3]), + DateTime.now().millisecondsSinceEpoch, ); }); } diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 3c2854e..18d66ae 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -46,6 +46,8 @@ list(APPEND PLUGIN_SOURCES "src/universal_ble_filter_util.h" "src/universal_ble_thread_safe.h" "src/enum_parser.h" + "src/helper/universal_ble_logger.cpp" + "src/helper/universal_ble_logger.h" ) add_library(${PLUGIN_NAME} SHARED diff --git a/windows/src/generated/universal_ble.g.cpp b/windows/src/generated/universal_ble.g.cpp index 6f63c0d..35944cf 100644 --- a/windows/src/generated/universal_ble.g.cpp +++ b/windows/src/generated/universal_ble.g.cpp @@ -487,27 +487,32 @@ EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( case 129: { 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 130: { - 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 131: { - return CustomEncodableValue(UniversalBleService::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleScanResult::FromEncodableList(std::get(ReadValue(stream)))); } case 132: { - return CustomEncodableValue(UniversalBleCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleService::FromEncodableList(std::get(ReadValue(stream)))); } case 133: { - return CustomEncodableValue(UniversalBleDescriptor::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); } case 134: { - return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalBleDescriptor::FromEncodableList(std::get(ReadValue(stream)))); } case 135: { - return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + return CustomEncodableValue(UniversalScanFilter::FromEncodableList(std::get(ReadValue(stream)))); } case 136: { + return CustomEncodableValue(UniversalManufacturerDataFilter::FromEncodableList(std::get(ReadValue(stream)))); + } + case 137: { return CustomEncodableValue(UniversalManufacturerData::FromEncodableList(std::get(ReadValue(stream)))); } default: @@ -519,43 +524,48 @@ void PigeonInternalCodecSerializer::WriteValue( const EncodableValue& value, flutter::ByteStreamWriter* stream) const { if (const CustomEncodableValue* custom_value = std::get_if(&value)) { - if (custom_value->type() == typeid(UniversalBleErrorCode)) { + if (custom_value->type() == typeid(UniversalBleLogLevel)) { stream->WriteByte(129); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(UniversalBleErrorCode)) { + stream->WriteByte(130); WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); return; } if (custom_value->type() == typeid(UniversalBleScanResult)) { - stream->WriteByte(130); + stream->WriteByte(131); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalBleService)) { - stream->WriteByte(131); + stream->WriteByte(132); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalBleCharacteristic)) { - stream->WriteByte(132); + stream->WriteByte(133); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalBleDescriptor)) { - stream->WriteByte(133); + stream->WriteByte(134); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalScanFilter)) { - stream->WriteByte(134); + stream->WriteByte(135); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalManufacturerDataFilter)) { - stream->WriteByte(135); + stream->WriteByte(136); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } if (custom_value->type() == typeid(UniversalManufacturerData)) { - stream->WriteByte(136); + stream->WriteByte(137); WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); return; } @@ -1151,6 +1161,34 @@ void UniversalBlePlatformChannel::SetUp( channel.SetMessageHandler(nullptr); } } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setLogLevel" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_log_level_arg = args.at(0); + if (encodable_log_level_arg.IsNull()) { + reply(WrapError("log_level_arg unexpectedly null.")); + return; + } + const auto& log_level_arg = std::any_cast(std::get(encodable_log_level_arg)); + std::optional output = api->SetLogLevel(log_level_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } } EncodableValue UniversalBlePlatformChannel::WrapError(std::string_view error_message) { @@ -1267,6 +1305,7 @@ void UniversalBleCallbackChannel::OnValueChanged( const std::string& device_id_arg, const std::string& characteristic_id_arg, const std::vector& value_arg, + const int64_t* timestamp_arg, std::function&& on_success, std::function&& on_error) { const std::string channel_name = "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged" + message_channel_suffix_; @@ -1275,6 +1314,7 @@ void UniversalBleCallbackChannel::OnValueChanged( EncodableValue(device_id_arg), EncodableValue(characteristic_id_arg), EncodableValue(value_arg), + timestamp_arg ? EncodableValue(*timestamp_arg) : EncodableValue(), }); channel.Send(encoded_api_arguments, [channel_name, on_success = std::move(on_success), on_error = std::move(on_error)](const uint8_t* reply, size_t reply_size) { std::unique_ptr response = GetCodec().DecodeMessage(reply, reply_size); diff --git a/windows/src/generated/universal_ble.g.h b/windows/src/generated/universal_ble.g.h index 81df4b6..ce2f342 100644 --- a/windows/src/generated/universal_ble.g.h +++ b/windows/src/generated/universal_ble.g.h @@ -57,6 +57,15 @@ template class ErrorOr { }; +enum class UniversalBleLogLevel { + kNone = 0, + kError = 1, + kWarning = 2, + kInfo = 3, + kDebug = 4, + kVerbose = 5 +}; + // Unified error codes for all platforms enum class UniversalBleErrorCode { kUnknownError = 0, @@ -425,6 +434,7 @@ class UniversalBlePlatformChannel { const flutter::EncodableList& with_services, std::function reply)> result) = 0; virtual ErrorOr GetConnectionState(const std::string& device_id) = 0; + virtual std::optional SetLogLevel(const UniversalBleLogLevel& log_level) = 0; // The codec used by UniversalBlePlatformChannel. static const flutter::StandardMessageCodec& GetCodec(); @@ -469,6 +479,7 @@ class UniversalBleCallbackChannel { const std::string& device_id, const std::string& characteristic_id, const std::vector& value, + const int64_t* timestamp, std::function&& on_success, std::function&& on_error); void OnConnectionChanged( diff --git a/windows/src/helper/universal_ble_logger.cpp b/windows/src/helper/universal_ble_logger.cpp new file mode 100644 index 0000000..eed0b68 --- /dev/null +++ b/windows/src/helper/universal_ble_logger.cpp @@ -0,0 +1,84 @@ +#include "universal_ble_logger.h" +#include +#include +#include + +namespace universal_ble { + +UniversalBleLogLevel UniversalBleLogger::current_level_ = + UniversalBleLogLevel::kNone; + +static std::string GetCurrentTimestampString() { + auto now = std::chrono::system_clock::now(); + auto time_t = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()) % + 1000; + + std::tm timeinfo; + localtime_s(&timeinfo, &time_t); + + std::ostringstream oss; + oss << std::put_time(&timeinfo, "%H:%M:%S"); + oss << "." << std::setfill('0') << std::setw(3) << ms.count(); + return "[" + oss.str() + "]"; +} + +void UniversalBleLogger::SetLogLevel(UniversalBleLogLevel level) { + current_level_ = level; +} + +UniversalBleLogLevel UniversalBleLogger::current_log_level() { + return current_level_; +} + +void UniversalBleLogger::LogError(const std::string &message) { + if (!Allows(UniversalBleLogLevel::kError)) + return; + std::cout << "UniversalBle:ERROR " << message << std::endl; +} + +void UniversalBleLogger::LogWarning(const std::string &message) { + if (!Allows(UniversalBleLogLevel::kWarning)) + return; + std::cout << "UniversalBle:WARN " << message << std::endl; +} + +void UniversalBleLogger::LogInfo(const std::string &message) { + if (!Allows(UniversalBleLogLevel::kInfo)) + return; + std::cout << "UniversalBle:INFO " << message << std::endl; +} + +void UniversalBleLogger::LogDebug(const std::string &message) { + if (!Allows(UniversalBleLogLevel::kDebug)) + return; + std::cout << "UniversalBle:DEBUG " << message << std::endl; +} + +void UniversalBleLogger::LogVerbose(const std::string &message) { + if (!Allows(UniversalBleLogLevel::kVerbose)) + return; + std::cout << "UniversalBle:VERBOSE " << message << std::endl; +} + +void UniversalBleLogger::LogDebugWithTimestamp(const std::string &message) { + if (!Allows(UniversalBleLogLevel::kDebug)) + return; + std::cout << "UniversalBle:DEBUG " << GetCurrentTimestampString() << " " << message + << std::endl; +} + +void UniversalBleLogger::LogVerboseWithTimestamp(const std::string &message) { + if (!Allows(UniversalBleLogLevel::kVerbose)) + return; + std::cout << "UniversalBle:VERBOSE " << GetCurrentTimestampString() << " " + << message << std::endl; +} + +bool UniversalBleLogger::Allows(UniversalBleLogLevel level) { + return current_level_ != UniversalBleLogLevel::kNone && + static_cast(level) <= static_cast(current_level_); +} + +} // namespace universal_ble diff --git a/windows/src/helper/universal_ble_logger.h b/windows/src/helper/universal_ble_logger.h new file mode 100644 index 0000000..3787fd5 --- /dev/null +++ b/windows/src/helper/universal_ble_logger.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include "../generated/universal_ble.g.h" + +namespace universal_ble { + +class UniversalBleLogger { +public: + static void SetLogLevel(UniversalBleLogLevel level); + static UniversalBleLogLevel current_log_level(); + + static void LogError(const std::string &message); + static void LogWarning(const std::string &message); + static void LogInfo(const std::string &message); + static void LogDebug(const std::string &message); + static void LogVerbose(const std::string &message); + static void LogDebugWithTimestamp(const std::string &message); + static void LogVerboseWithTimestamp(const std::string &message); + +private: + static UniversalBleLogLevel current_level_; + static bool Allows(UniversalBleLogLevel level); +}; + +} // namespace universal_ble diff --git a/windows/src/universal_ble_plugin.cpp b/windows/src/universal_ble_plugin.cpp index ccbe88a..acb8528 100644 --- a/windows/src/universal_ble_plugin.cpp +++ b/windows/src/universal_ble_plugin.cpp @@ -14,6 +14,7 @@ #include "enum_parser.h" #include "generated/universal_ble.g.h" +#include "helper/universal_ble_logger.h" #include "helper/universal_enum.h" #include "helper/utils.h" #include "pin_entry.h" @@ -165,7 +166,7 @@ UniversalBlePlugin::StartScan(const UniversalScanFilter *filter) { filter->with_name_prefix().size() > 0; if (uses_custom_filters) { - std::cout << "Using Custom Scan Filter" << std::endl; + UniversalBleLogger::LogInfo("Using Custom Scan Filter"); setScanFilter(*filter); } else { // Apply Services filter @@ -186,7 +187,7 @@ UniversalBlePlugin::StartScan(const UniversalScanFilter *filter) { bluetooth_le_watcher_.Start(); return std::nullopt; } catch (...) { - std::cout << "Unknown error StartScan" << std::endl; + UniversalBleLogger::LogError("Unknown error StartScan"); return create_flutter_error(UniversalBleErrorCode::kUnknownError, "Unknown error"); } @@ -205,8 +206,8 @@ std::optional UniversalBlePlugin::StopScan() { return std::nullopt; } catch (const hresult_error &err) { const int error_code = err.code(); - std::cout << "StopScanLog: " << to_string(err.message()) - << " ErrorCode: " << std::to_string(error_code) << std::endl; + UniversalBleLogger::LogError("StopScanLog: " + to_string(err.message()) + + " ErrorCode: " + std::to_string(error_code)); return create_flutter_error(UniversalBleErrorCode::kFailed, to_string(err.message()), std::to_string(error_code)); @@ -248,6 +249,12 @@ UniversalBlePlugin::GetConnectionState(const std::string &device_id) { } } +std::optional +UniversalBlePlugin::SetLogLevel(const UniversalBleLogLevel &log_level) { + UniversalBleLogger::SetLogLevel(log_level); + return std::nullopt; +} + std::optional UniversalBlePlugin::Connect(const std::string &device_id) { ConnectAsync(str_to_mac_address(device_id)); @@ -288,6 +295,8 @@ void UniversalBlePlugin::ReadValue( const std::string &device_id, const std::string &service, const std::string &characteristic, std::function> reply)> result) { + UniversalBleLogger::LogDebugWithTimestamp("READ -> " + device_id + " " + + service + " " + characteristic); try { const auto it = connected_devices_.find(str_to_mac_address(device_id)); if (it == connected_devices_.end()) { @@ -317,6 +326,10 @@ void UniversalBlePlugin::ReadValue( const auto read_value_result = sender.GetResults(); const auto status = read_value_result.Status(); if (status != GattCommunicationStatus::Success) { + UniversalBleLogger::LogError( + "READ_FAILED <- " + device_id + " " + service + " " + + characteristic + + " status=" + std::to_string(static_cast(status))); result(create_flutter_error_from_gatt_communication_status(status)); } else { result(to_bytevc(read_value_result.Value())); @@ -325,7 +338,7 @@ void UniversalBlePlugin::ReadValue( } catch (const FlutterError &err) { return result(err); } catch (...) { - std::cout << "ReadValueLog: Unknown error" << std::endl; + UniversalBleLogger::LogError("ReadValueLog: Unknown error"); return result(create_flutter_unknown_error()); } } @@ -335,6 +348,10 @@ void UniversalBlePlugin::WriteValue( const std::string &characteristic, const std::vector &value, int64_t ble_output_property, std::function reply)> result) { + UniversalBleLogger::LogDebugWithTimestamp( + "WRITE -> " + device_id + " " + service + " " + characteristic + + " len=" + std::to_string(value.size()) + + " property=" + std::to_string(ble_output_property)); try { const auto it = connected_devices_.find(str_to_mac_address(device_id)); if (it == connected_devices_.end()) { @@ -383,6 +400,10 @@ void UniversalBlePlugin::WriteValue( const auto status = sender.GetResults(); if (status != GattCommunicationStatus::Success) { + UniversalBleLogger::LogError( + "WRITE_FAILED <- " + device_id + " " + service + " " + + characteristic + + " status=" + std::to_string(static_cast(status))); result(create_flutter_error_from_gatt_communication_status(status)); } else { result(std::nullopt); @@ -391,7 +412,7 @@ void UniversalBlePlugin::WriteValue( } catch (const FlutterError &err) { result(err); } catch (...) { - std::cout << "WriteValue: Unknown error" << std::endl; + UniversalBleLogger::LogError("WriteValue: Unknown error"); result(create_flutter_unknown_error()); } } @@ -399,6 +420,9 @@ void UniversalBlePlugin::WriteValue( void UniversalBlePlugin::RequestMtu( const std::string &device_id, int64_t expected_mtu, std::function reply)> result) { + UniversalBleLogger::LogDebugWithTimestamp( + "REQUEST_MTU -> " + device_id + + " expected=" + std::to_string(expected_mtu)); try { const auto it = connected_devices_.find(str_to_mac_address(device_id)); if (it == connected_devices_.end()) { @@ -495,7 +519,7 @@ fire_and_forget UniversalBlePlugin::InitializeAsync() { } } if (!bluetooth_radio_) { - std::cout << "Bluetooth is not available" << std::endl; + UniversalBleLogger::LogError("Bluetooth is not available"); ui_thread_handler_.Post([] { callback_channel->OnAvailabilityChanged( static_cast(AvailabilityState::unsupported), SuccessCallback, @@ -509,7 +533,7 @@ fire_and_forget UniversalBlePlugin::PairAsync( const std::string &device_id, const std::function reply)> result) { try { - std::cout << "Trying to pair" << std::endl; + UniversalBleLogger::LogInfo("Trying to pair"); const auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync( str_to_mac_address(device_id)); @@ -519,7 +543,7 @@ fire_and_forget UniversalBlePlugin::PairAsync( co_return; } - std::cout << "Got device" << std::endl; + UniversalBleLogger::LogInfo("Got device"); const auto device_information = device.DeviceInformation(); if (device_information.Pairing().IsPaired()) @@ -530,25 +554,31 @@ fire_and_forget UniversalBlePlugin::PairAsync( else { const auto pair_result = co_await device_information.Pairing().PairAsync(); - std::cout << "PairLog: Received pairing status" << std::endl; + UniversalBleLogger::LogInfo("PairLog: Received pairing status"); bool is_paired = pair_result.Status() == DevicePairingResultStatus::Paired; result(is_paired); - const std::string *error_msg = nullptr; const auto error_str = device_pairing_result_to_string(pair_result.Status()); + std::optional captured_error; if (error_str.has_value()) { - error_msg = &error_str.value(); + captured_error = error_str.value(); } - ui_thread_handler_.Post([device_id, is_paired, error_msg] { + ui_thread_handler_.Post([device_id, is_paired, captured_error] { + const std::string *error_msg = nullptr; + std::string error_string; + if (captured_error.has_value()) { + error_string = captured_error.value(); + error_msg = &error_string; + } callback_channel->OnPairStateChange(device_id, is_paired, error_msg, SuccessCallback, ErrorCallback); }); } } catch (...) { result(false); - std::cout << "PairLog: Unknown error" << std::endl; + UniversalBleLogger::LogError("PairLog: Unknown error"); } } @@ -573,7 +603,7 @@ fire_and_forget UniversalBlePlugin::CustomPairAsync( const auto custom_pairing = device_information.Pairing().Custom(); const event_token token = custom_pairing.PairingRequested( {this, &UniversalBlePlugin::PairingRequestedHandler}); - std::cout << "PairLog: Trying to pair" << std::endl; + UniversalBleLogger::LogInfo("PairLog: Trying to pair"); const DevicePairingProtectionLevel protection_level = device_information.Pairing().ProtectionLevel(); // DevicePairingKinds => None, ConfirmOnly, DisplayPin, ProvidePin, @@ -581,25 +611,31 @@ fire_and_forget UniversalBlePlugin::CustomPairAsync( const auto pair_result = co_await custom_pairing.PairAsync( DevicePairingKinds::ConfirmOnly | DevicePairingKinds::ProvidePin, protection_level); - std::cout << "PairLog: Got Pair Result" << std::endl; + UniversalBleLogger::LogInfo("PairLog: Got Pair Result"); const DevicePairingResultStatus status = pair_result.Status(); custom_pairing.PairingRequested(token); bool is_paired = status == DevicePairingResultStatus::Paired; result(is_paired); - const std::string *error_msg = nullptr; const auto error_str = device_pairing_result_to_string(status); + std::optional captured_error; if (error_str.has_value()) { - error_msg = &error_str.value(); + captured_error = error_str.value(); } - ui_thread_handler_.Post([device_id, is_paired, error_msg] { + ui_thread_handler_.Post([device_id, is_paired, captured_error] { + const std::string *error_msg = nullptr; + std::string error_string; + if (captured_error.has_value()) { + error_string = captured_error.value(); + error_msg = &error_string; + } callback_channel->OnPairStateChange(device_id, is_paired, error_msg, SuccessCallback, ErrorCallback); }); } } catch (...) { result(false); - std::cout << "PairLog Error: Pairing Failed" << std::endl; + UniversalBleLogger::LogError("PairLog Error: Pairing Failed"); } } @@ -607,16 +643,16 @@ fire_and_forget UniversalBlePlugin::CustomPairAsync( void UniversalBlePlugin::PairingRequestedHandler( DeviceInformationCustomPairing sender, const DevicePairingRequestedEventArgs &event_args) { - std::cout << "PairLog: Got PairingRequest" << std::endl; + UniversalBleLogger::LogInfo("PairLog: Got PairingRequest"); const DevicePairingKinds kind = event_args.PairingKind(); if (kind != DevicePairingKinds::ProvidePin) { event_args.Accept(); return; } - std::cout << "PairLog: Trying to get pin from user" << std::endl; + UniversalBleLogger::LogInfo("PairLog: Trying to get pin from user"); const hstring pin = askForPairingPin(); - std::wcout << "PairLog: Got Pin: " << pin.c_str() << std::endl; + UniversalBleLogger::LogInfo("PairLog: Got Pin: " + to_string(pin)); event_args.Accept(pin); } @@ -678,10 +714,7 @@ void UniversalBlePlugin::PushUniversalScanResult( // Filter final result before sending to Flutter if (is_connectable && filterDevice(scan_result)) { - scan_result.set_timestamp( - std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count()); + scan_result.set_timestamp(GetCurrentTimestampMillis()); ui_thread_handler_.Post([scan_result] { callback_channel->OnScanResult(scan_result, SuccessCallback, ErrorCallback); @@ -762,7 +795,7 @@ void UniversalBlePlugin::SetupDeviceWatcher() { device_watcher_enumeration_completed_token_ = device_watcher_.EnumerationCompleted([this](DeviceWatcher sender, IInspectable args) { - std::cout << "DeviceWatcherEvent: EnumerationCompleted" << std::endl; + UniversalBleLogger::LogInfo("DeviceWatcherEvent: EnumerationCompleted"); DisposeDeviceWatcher(); // EnumerationCompleted }); @@ -913,7 +946,7 @@ void UniversalBlePlugin::BluetoothLeWatcherReceived( // Filter Device PushUniversalScanResult(universal_scan_result, args.IsConnectable()); } catch (...) { - std::cout << "ScanResultErrorInParsing" << std::endl; + UniversalBleLogger::LogError("ScanResultErrorInParsing"); } } @@ -936,8 +969,8 @@ fire_and_forget UniversalBlePlugin::ConnectAsync(uint64_t bluetooth_address) { BluetoothLEDevice device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(bluetooth_address); if (!device) { - std::cout << "ConnectionLog: ConnectionFailed: Failed to get device" - << std::endl; + UniversalBleLogger::LogError( + "ConnectionLog: ConnectionFailed: Failed to get device"); ui_thread_handler_.Post([bluetooth_address] { callback_channel->OnConnectionChanged( mac_address_to_str(bluetooth_address), false, @@ -947,14 +980,14 @@ fire_and_forget UniversalBlePlugin::ConnectAsync(uint64_t bluetooth_address) { co_return; } - std::cout << "ConnectionLog: Device found" << std::endl; + UniversalBleLogger::LogInfo("ConnectionLog: Device found"); auto services_result = co_await device.GetGattServicesAsync((BluetoothCacheMode::Uncached)); auto services_result_error = gatt_communication_status_to_error(services_result.Status()); if (services_result_error.has_value()) { - std::cout << "ConnectionFailed: Failed to get services: " - << services_result_error.value() << std::endl; + UniversalBleLogger::LogError("ConnectionFailed: Failed to get services: " + + services_result_error.value()); ui_thread_handler_.Post([bluetooth_address, services_result_error] { callback_channel->OnConnectionChanged( mac_address_to_str(bluetooth_address), false, @@ -963,7 +996,7 @@ fire_and_forget UniversalBlePlugin::ConnectAsync(uint64_t bluetooth_address) { co_return; } - std::cout << "ConnectionLog: Services discovered" << std::endl; + UniversalBleLogger::LogInfo("ConnectionLog: Services discovered"); std::unordered_map gatt_map; auto gatt_services = services_result.Services(); for (GattDeviceService &&service : gatt_services) { @@ -976,9 +1009,9 @@ fire_and_forget UniversalBlePlugin::ConnectAsync(uint64_t bluetooth_address) { gatt_communication_status_to_error(characteristics_result.Status()); if (characteristics_result_error.has_value()) { - std::cout << "Failed to get characteristics for service: " << service_uuid - << ", With Status: " << characteristics_result_error.value() - << std::endl; + UniversalBleLogger::LogError( + "Failed to get characteristics for service: " + service_uuid + + ", With Status: " + characteristics_result_error.value()); continue; // PostConnectionUpdate(bluetoothAddress, ConnectionState::disconnected); // co_return; @@ -1001,7 +1034,7 @@ fire_and_forget UniversalBlePlugin::ConnectAsync(uint64_t bluetooth_address) { device, connection_status_changed_token, gatt_map); auto pair = std::make_pair(bluetooth_address, std::move(device_agent)); connected_devices_.insert(std::move(pair)); - std::cout << "ConnectionLog: Connected" << std::endl; + UniversalBleLogger::LogInfo("ConnectionLog: Connected"); ui_thread_handler_.Post([bluetooth_address] { callback_channel->OnConnectionChanged(mac_address_to_str(bluetooth_address), true, nullptr, SuccessCallback, @@ -1089,13 +1122,14 @@ fire_and_forget UniversalBlePlugin::GetSystemDevicesAsync( result(results); } catch (const hresult_error &err) { int error_code = err.code(); - std::cout << "GetConnectedDeviceLog: " << to_string(err.message()) - << " ErrorCode: " << std::to_string(error_code) << std::endl; + UniversalBleLogger::LogError( + "GetConnectedDeviceLog: " + to_string(err.message()) + + " ErrorCode: " + std::to_string(error_code)); result(create_flutter_error(UniversalBleErrorCode::kFailed, to_string(err.message()), std::to_string(error_code))); } catch (...) { - std::cout << "Unknown error GetSystemDevicesAsyncAsync" << std::endl; + UniversalBleLogger::LogError("Unknown error GetSystemDevicesAsyncAsync"); result(create_flutter_error(UniversalBleErrorCode::kUnknownError, "Unknown error")); } @@ -1136,9 +1170,8 @@ fire_and_forget UniversalBlePlugin::DiscoverServicesAsync( } } } catch (...) { - std::cout << "DiscoverServicesAsync: failed to get descriptors for " - "characteristic: " - << std::endl; + UniversalBleLogger::LogError("DiscoverServicesAsync: failed to get " + "descriptors for characteristic"); } } universal_characteristics.push_back( @@ -1180,7 +1213,7 @@ fire_and_forget UniversalBlePlugin::IsPairedAsync( const bool is_paired = device.DeviceInformation().Pairing().IsPaired(); result(is_paired); } catch (...) { - std::cout << "IsPairedAsync: Error " << std::endl; + UniversalBleLogger::LogError("IsPairedAsync: Error"); result(create_flutter_error(UniversalBleErrorCode::kUnknownError, "Unknown error")); } @@ -1190,6 +1223,9 @@ fire_and_forget UniversalBlePlugin::SetNotifiableAsync( const std::string &device_id, const std::string &service, const std::string &characteristic, const int64_t ble_input_property, const std::function reply)> result) { + UniversalBleLogger::LogDebugWithTimestamp( + "SET_NOTIFY -> " + device_id + " " + service + " " + characteristic + + " input=" + std::to_string(ble_input_property)); try { const auto it = connected_devices_.find(str_to_mac_address(device_id)); if (it == connected_devices_.end()) { @@ -1236,6 +1272,9 @@ fire_and_forget UniversalBlePlugin::SetNotifiableAsync( .WriteClientCharacteristicConfigurationDescriptorAsync( descriptor_value); if (status != GattCommunicationStatus::Success) { + UniversalBleLogger::LogError("SET_NOTIFY_FAILED <- " + device_id + " " + + service + " " + characteristic + " status=" + + std::to_string(static_cast(status))); result(create_flutter_error_from_gatt_communication_status(status)); co_return; } @@ -1246,16 +1285,16 @@ fire_and_forget UniversalBlePlugin::SetNotifiableAsync( if (gatt_char.subscription_token.has_value()) { gatt_characteristic.ValueChanged(gatt_char.subscription_token.value()); gatt_char.subscription_token = std::nullopt; - std::cout << "Unsubscribed " << to_uuidstr(gatt_characteristic.Uuid()) - << std::endl; + UniversalBleLogger::LogInfo("Unsubscribed " + + to_uuidstr(gatt_characteristic.Uuid())); } } else { // If a notification for the given characteristic is already in progress, // swap the callbacks. if (gatt_char.subscription_token.has_value()) { - std::cout << "A notification for the given characteristic is already " - "in progress. Swapping callbacks." - << std::endl; + UniversalBleLogger::LogWarning( + "A notification for the given characteristic is already in " + "progress. Swapping callbacks."); gatt_characteristic.ValueChanged(gatt_char.subscription_token.value()); gatt_char.subscription_token = std::nullopt; } @@ -1269,7 +1308,7 @@ fire_and_forget UniversalBlePlugin::SetNotifiableAsync( } catch (const FlutterError &err) { result(err); } catch (...) { - std::cout << "SetNotifiableLog: Unknown error" << std::endl; + UniversalBleLogger::LogError("SetNotifiableLog: Unknown error"); result(create_flutter_unknown_error()); } } @@ -1278,10 +1317,19 @@ void UniversalBlePlugin::GattCharacteristicValueChanged( const GattCharacteristic &sender, const GattValueChangedEventArgs &args) { auto uuid = to_uuidstr(sender.Uuid()); auto bytes = to_bytevc(args.CharacteristicValue()); - ui_thread_handler_.Post([sender, uuid, bytes] { + auto device_id = + mac_address_to_str(sender.Service().Device().BluetoothAddress()); + + UniversalBleLogger::LogVerboseWithTimestamp( + "NOTIFY <- " + device_id + " " + uuid + + " len=" + std::to_string(bytes.size())); + + auto timestamp = GetCurrentTimestampMillis(); + ui_thread_handler_.Post([sender, bytes, timestamp] { + auto uuid = to_uuidstr(sender.Uuid()); callback_channel->OnValueChanged( mac_address_to_str(sender.Service().Device().BluetoothAddress()), uuid, - bytes, SuccessCallback, ErrorCallback); + bytes, ×tamp, SuccessCallback, ErrorCallback); }); } diff --git a/windows/src/universal_ble_plugin.h b/windows/src/universal_ble_plugin.h index 4a8386a..6cc2ab4 100644 --- a/windows/src/universal_ble_plugin.h +++ b/windows/src/universal_ble_plugin.h @@ -5,205 +5,210 @@ #include #include -#include -#include -#include -#include -#include -#include -#include #include #include +#include +#include +#include +#include +#include +#include +#include -#include -#include "helper/utils.h" -#include "helper/universal_enum.h" -#include "helper/universal_ble_base.h" #include "generated/universal_ble.g.h" +#include "helper/universal_ble_base.h" +#include "helper/universal_enum.h" +#include "helper/utils.h" #include "ui_thread_handler.hpp" #include "universal_ble_thread_safe.h" +#include -namespace universal_ble -{ - struct GattCharacteristicObject - { - GattCharacteristic obj = nullptr; - std::optional subscription_token; - }; +namespace universal_ble { +struct GattCharacteristicObject { + GattCharacteristic obj = nullptr; + std::optional subscription_token; +}; - struct GattServiceObject - { - GattDeviceService obj = nullptr; - std::unordered_map characteristics; - }; +struct GattServiceObject { + GattDeviceService obj = nullptr; + std::unordered_map characteristics; +}; - struct BluetoothDeviceAgent - { - BluetoothLEDevice device; - event_token connection_status_changed_token; - std::unordered_map gatt_map; +struct BluetoothDeviceAgent { + BluetoothLEDevice device; + event_token connection_status_changed_token; + std::unordered_map gatt_map; - BluetoothDeviceAgent(const BluetoothLEDevice &device, const event_token connection_status_changed_token, - const std::unordered_map &gatt_map) - : device(device), - connection_status_changed_token(connection_status_changed_token), - gatt_map(gatt_map) - { - } + BluetoothDeviceAgent( + const BluetoothLEDevice &device, + const event_token connection_status_changed_token, + const std::unordered_map &gatt_map) + : device(device), + connection_status_changed_token(connection_status_changed_token), + gatt_map(gatt_map) {} - ~BluetoothDeviceAgent() - { - device = nullptr; - } + ~BluetoothDeviceAgent() { device = nullptr; } - GattCharacteristicObject &FetchCharacteristic(const std::string &service_uuid, - const std::string &characteristic_uuid) - { - if (gatt_map.count(service_uuid) == 0) - { - throw create_flutter_error(UniversalBleErrorCode::kServiceNotFound, "Service not found"); - } - if (gatt_map[service_uuid].characteristics.count(characteristic_uuid) == 0) - { - throw create_flutter_error(UniversalBleErrorCode::kCharacteristicNotFound, "Characteristic not found"); - } - return gatt_map[service_uuid].characteristics.at(characteristic_uuid); - } - }; + GattCharacteristicObject & + FetchCharacteristic(const std::string &service_uuid, + const std::string &characteristic_uuid) { + if (gatt_map.count(service_uuid) == 0) { + throw create_flutter_error(UniversalBleErrorCode::kServiceNotFound, + "Service not found"); + } + if (gatt_map[service_uuid].characteristics.count(characteristic_uuid) == + 0) { + throw create_flutter_error(UniversalBleErrorCode::kCharacteristicNotFound, + "Characteristic not found"); + } + return gatt_map[service_uuid].characteristics.at(characteristic_uuid); + } +}; - class UniversalBlePlugin : public flutter::Plugin, public UniversalBlePlatformChannel - { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); +class UniversalBlePlugin : public flutter::Plugin, + public UniversalBlePlatformChannel { +public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); - UniversalBlePlugin(flutter::PluginRegistrarWindows *registrar); + UniversalBlePlugin(flutter::PluginRegistrarWindows *registrar); - ~UniversalBlePlugin(); + ~UniversalBlePlugin(); - // Disallow copy and assign. - UniversalBlePlugin(const UniversalBlePlugin&) = delete; - UniversalBlePlugin& operator=(const UniversalBlePlugin&) = delete; + // Disallow copy and assign. + UniversalBlePlugin(const UniversalBlePlugin &) = delete; + UniversalBlePlugin &operator=(const UniversalBlePlugin &) = delete; +private: + static void SuccessCallback() {} + static void ErrorCallback(const FlutterError &error) { + // Ignore ChannelConnection Error, This might occur because of HotReload + if (error.code() != "channel-error") { + std::cout << "ErrorCode: " << error.code() + << " Message: " << error.message() << std::endl; + } + } + static int64_t GetCurrentTimestampMillis() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + } - private: - static void SuccessCallback() {} - static void ErrorCallback(const FlutterError &error) - { - // Ignore ChannelConnection Error, This might occur because of HotReload - if (error.code() != "channel-error") - { - std::cout << "ErrorCode: " << error.code() << " Message: " << error.message() << std::endl; - } - } + flutter::PluginRegistrarWindows *registrar_; + bool initialized_ = false; - flutter::PluginRegistrarWindows *registrar_; - bool initialized_ = false; + UniversalBleUiThreadHandler ui_thread_handler_; + Radio bluetooth_radio_{nullptr}; + RadioState old_radio_state_ = RadioState::Unknown; + BluetoothLEAdvertisementWatcher bluetooth_le_watcher_{nullptr}; + DeviceWatcher device_watcher_{nullptr}; - UniversalBleUiThreadHandler ui_thread_handler_; - Radio bluetooth_radio_{nullptr}; - RadioState old_radio_state_ = RadioState::Unknown; - BluetoothLEAdvertisementWatcher bluetooth_le_watcher_{nullptr}; - DeviceWatcher device_watcher_{nullptr}; + std::unordered_map> + connected_devices_{}; + ThreadSafeMap device_watcher_devices_{}; + ThreadSafeMap scan_results_{}; + // Maps DeviceInformation.Id() -> MAC address string used as key in + // device_watcher_devices_ + ThreadSafeMap device_watcher_id_to_mac_{}; - std::unordered_map> connected_devices_{}; - ThreadSafeMap device_watcher_devices_{}; - ThreadSafeMap scan_results_{}; - // Maps DeviceInformation.Id() -> MAC address string used as key in device_watcher_devices_ - ThreadSafeMap device_watcher_id_to_mac_{}; + event_token bluetooth_le_watcher_received_token_; + event_token device_watcher_added_token_; + event_token device_watcher_updated_token_; + event_token device_watcher_removed_token_; + event_token device_watcher_enumeration_completed_token_; + event_token device_watcher_stopped_token_; + event_revoker radio_state_changed_revoker_; - event_token bluetooth_le_watcher_received_token_; - event_token device_watcher_added_token_; - event_token device_watcher_updated_token_; - event_token device_watcher_removed_token_; - event_token device_watcher_enumeration_completed_token_; - event_token device_watcher_stopped_token_; - event_revoker radio_state_changed_revoker_; + fire_and_forget InitializeAsync(); + fire_and_forget ConnectAsync(uint64_t bluetooth_address); + fire_and_forget SetNotifiableAsync( + const std::string &device_id, const std::string &service, + const std::string &characteristic, int64_t ble_input_property, + std::function reply)> result); + fire_and_forget PairAsync(const std::string &device_id, + std::function reply)> result); + fire_and_forget + CustomPairAsync(const std::string &device_id, + std::function reply)> result); + static fire_and_forget GetSystemDevicesAsync( + std::vector with_services, + std::function reply)> result); + static fire_and_forget + IsPairedAsync(const std::string &device_id, + std::function reply)> result); + fire_and_forget DiscoverServicesAsync( + const std::string &device_id, bool with_descriptors, + std::function reply)> result); + void + PairingRequestedHandler(DeviceInformationCustomPairing sender, + const DevicePairingRequestedEventArgs &event_args); - fire_and_forget InitializeAsync(); - fire_and_forget ConnectAsync(uint64_t bluetooth_address); - fire_and_forget SetNotifiableAsync( - const std::string& device_id, - const std::string& service, - const std::string& characteristic, - int64_t ble_input_property, - std::function reply)> result); - fire_and_forget PairAsync(const std::string& device_id, std::function reply)> result); - fire_and_forget CustomPairAsync(const std::string& device_id, std::function reply)> result); - static fire_and_forget GetSystemDevicesAsync( - std::vector with_services, - std::function reply)> result); - static fire_and_forget IsPairedAsync(const std::string& device_id, std::function reply)> result); - fire_and_forget DiscoverServicesAsync(const std::string &device_id, - bool with_descriptors, - std::function reply)> result); + void RadioStateChanged(const Radio &sender, const IInspectable &); + void SetupDeviceWatcher(); + void DisposeDeviceWatcher(); + void PushUniversalScanResult(UniversalBleScanResult scan_result, + bool is_connectable); + void BluetoothLeWatcherReceived( + const BluetoothLEAdvertisementWatcher &sender, + const BluetoothLEAdvertisementReceivedEventArgs &args); + void OnDeviceInfoReceived(const DeviceInformation &device_info); + void BluetoothLeDeviceConnectionStatusChanged(const BluetoothLEDevice &sender, + const IInspectable &args); + void CleanConnection(uint64_t bluetooth_address); + void + DisposeServices(const std::unique_ptr &device_agent); - void PairingRequestedHandler(DeviceInformationCustomPairing sender, const DevicePairingRequestedEventArgs& event_args); + void GattCharacteristicValueChanged(const GattCharacteristic &sender, + const GattValueChangedEventArgs &args); - void RadioStateChanged(const Radio& sender, const IInspectable&); - void SetupDeviceWatcher(); - void DisposeDeviceWatcher(); - void PushUniversalScanResult(UniversalBleScanResult scan_result, bool is_connectable); - void BluetoothLeWatcherReceived(const BluetoothLEAdvertisementWatcher& sender, const - BluetoothLEAdvertisementReceivedEventArgs& args); - void OnDeviceInfoReceived(const DeviceInformation& device_info); - void BluetoothLeDeviceConnectionStatusChanged(const BluetoothLEDevice& sender, const IInspectable& args); - void CleanConnection(uint64_t bluetooth_address); - void DisposeServices(const std::unique_ptr &device_agent); - - - void GattCharacteristicValueChanged(const GattCharacteristic& sender, const GattValueChangedEventArgs& args); - - // UniversalBlePlatformChannel implementation. - void GetBluetoothAvailabilityState(std::function reply)> result) override; - void EnableBluetooth(std::function reply)> result) override; - void DisableBluetooth(std::function reply)> result) override; - ErrorOr GetConnectionState(const std::string &device_id) override; - std::optional StartScan(const UniversalScanFilter *filter) override; - std::optional StopScan() override; - ErrorOr IsScanning() override; - std::optional Connect(const std::string &device_id) override; - std::optional Disconnect(const std::string &device_id) override; - void RequestPermissions( - bool with_android_fine_location, - std::function reply)> result) override; - void DiscoverServices( - const std::string &device_id, - bool with_descriptors, - std::function reply)> result) override; - void SetNotifiable( - const std::string &device_id, - const std::string &service, - const std::string &characteristic, - int64_t ble_input_property, - std::function reply)> result) override; - void ReadValue( - const std::string &device_id, - const std::string &service, - const std::string &characteristic, - std::function> reply)> result) override; - void WriteValue( - const std::string &device_id, - const std::string &service, - const std::string &characteristic, - const std::vector &value, - int64_t ble_output_property, - std::function reply)> result) override; - void RequestMtu( - const std::string &device_id, - int64_t expected_mtu, - std::function reply)> result) override; - void IsPaired( - const std::string &device_id, + // UniversalBlePlatformChannel implementation. + void GetBluetoothAvailabilityState( + std::function reply)> result) override; + void + EnableBluetooth(std::function reply)> result) override; + void + DisableBluetooth(std::function reply)> result) override; + ErrorOr GetConnectionState(const std::string &device_id) override; + std::optional + SetLogLevel(const UniversalBleLogLevel &log_level) override; + std::optional + StartScan(const UniversalScanFilter *filter) override; + std::optional StopScan() override; + ErrorOr IsScanning() override; + std::optional Connect(const std::string &device_id) override; + std::optional Disconnect(const std::string &device_id) override; + void RequestPermissions( + bool with_android_fine_location, + std::function reply)> result) override; + void + DiscoverServices(const std::string &device_id, bool with_descriptors, + std::function reply)> + result) override; + void SetNotifiable( + const std::string &device_id, const std::string &service, + const std::string &characteristic, int64_t ble_input_property, + std::function reply)> result) override; + void ReadValue( + const std::string &device_id, const std::string &service, + const std::string &characteristic, + std::function> reply)> result) override; + void WriteValue( + const std::string &device_id, const std::string &service, + const std::string &characteristic, const std::vector &value, + int64_t ble_output_property, + std::function reply)> result) override; + void RequestMtu(const std::string &device_id, int64_t expected_mtu, + std::function reply)> result) override; + void IsPaired(const std::string &device_id, + std::function reply)> result) override; + void Pair(const std::string &device_id, std::function reply)> result) override; - void Pair( - const std::string &device_id, - std::function reply)> result) override; - std::optional UnPair(const std::string &device_id) override; - void GetSystemDevices( - const flutter::EncodableList &with_services, - std::function reply)> result) override; - }; + std::optional UnPair(const std::string &device_id) override; + void + GetSystemDevices(const flutter::EncodableList &with_services, + std::function reply)> + result) override; +}; } // namespace universal_ble